• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keradus / PHP-CS-Fixer / 17642215709

11 Sep 2025 10:50AM UTC coverage: 94.689% (+0.003%) from 94.686%
17642215709

push

github

keradus
Merge remote-tracking branch 'upstream/master' into __modifier_keywords

10 of 10 new or added lines in 2 files covered. (100.0%)

77 existing lines in 10 files now uncovered.

28421 of 30015 relevant lines covered (94.69%)

45.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

18.64
/src/Doctrine/Annotation/Tokens.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14

15
namespace PhpCsFixer\Doctrine\Annotation;
16

17
use PhpCsFixer\Preg;
18
use PhpCsFixer\Tokenizer\Token as PhpToken;
19

20
/**
21
 * A list of Doctrine annotation tokens.
22
 *
23
 * @internal
24
 *
25
 * @extends \SplFixedArray<Token>
26
 *
27
 * `SplFixedArray` uses `T|null` in return types because value can be null if an offset is unset or if the size does not match the number of elements.
28
 * But our class takes care of it and always ensures correct size and indexes, so that these methods never return `null` instead of `Token`.
29
 *
30
 * @method Token                    offsetGet($offset)
31
 * @method \Traversable<int, Token> getIterator()
32
 * @method array<int, Token>        toArray()
33
 *
34
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
35
 */
36
final class Tokens extends \SplFixedArray
37
{
38
    /**
39
     * @param list<string> $ignoredTags
40
     *
41
     * @throws \InvalidArgumentException
42
     */
43
    public static function createFromDocComment(PhpToken $input, array $ignoredTags = []): self
44
    {
45
        if (!$input->isGivenKind(\T_DOC_COMMENT)) {
3✔
46
            throw new \InvalidArgumentException('Input must be a T_DOC_COMMENT token.');
×
47
        }
48

49
        $tokens = [];
3✔
50

51
        $content = $input->getContent();
3✔
52
        $ignoredTextPosition = 0;
3✔
53
        $currentPosition = 0;
3✔
54
        $token = null;
3✔
55
        while (false !== $nextAtPosition = strpos($content, '@', $currentPosition)) {
3✔
56
            if (0 !== $nextAtPosition && !Preg::match('/\s/', $content[$nextAtPosition - 1])) {
×
57
                $currentPosition = $nextAtPosition + 1;
×
58

59
                continue;
×
60
            }
61

62
            $lexer = new DocLexer();
×
63
            $lexer->setInput(substr($content, $nextAtPosition));
×
64

65
            $scannedTokens = [];
×
66
            $index = 0;
×
67
            $nbScannedTokensToUse = 0;
×
68
            $nbScopes = 0;
×
69
            while (null !== $token = $lexer->peek()) {
×
70
                if (0 === $index && !$token->isType(DocLexer::T_AT)) {
×
71
                    break;
×
72
                }
73

74
                if (1 === $index) {
×
75
                    if (!$token->isType(DocLexer::T_IDENTIFIER) || \in_array($token->getContent(), $ignoredTags, true)) {
×
76
                        break;
×
77
                    }
78

79
                    $nbScannedTokensToUse = 2;
×
80
                }
81

82
                if ($index >= 2 && 0 === $nbScopes && !$token->isType([DocLexer::T_NONE, DocLexer::T_OPEN_PARENTHESIS])) {
×
83
                    break;
×
84
                }
85

86
                $scannedTokens[] = $token;
×
87

88
                if ($token->isType(DocLexer::T_OPEN_PARENTHESIS)) {
×
89
                    ++$nbScopes;
×
90
                } elseif ($token->isType(DocLexer::T_CLOSE_PARENTHESIS)) {
×
91
                    if (0 === --$nbScopes) {
×
92
                        $nbScannedTokensToUse = \count($scannedTokens);
×
93

94
                        break;
×
95
                    }
96
                }
97

98
                ++$index;
×
99
            }
100

101
            if (0 !== $nbScopes) {
×
102
                break;
×
103
            }
104

105
            if (0 !== $nbScannedTokensToUse) {
×
106
                $ignoredTextLength = $nextAtPosition - $ignoredTextPosition;
×
107
                if (0 !== $ignoredTextLength) {
×
108
                    $tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition, $ignoredTextLength));
×
109
                }
110

111
                $lastTokenEndIndex = 0;
×
112
                foreach (\array_slice($scannedTokens, 0, $nbScannedTokensToUse) as $scannedToken) {
×
113
                    $token = $scannedToken->isType(DocLexer::T_STRING)
×
114
                        ? new Token(
×
115
                            $scannedToken->getType(),
×
116
                            '"'.str_replace('"', '""', $scannedToken->getContent()).'"',
×
117
                            $scannedToken->getPosition()
×
118
                        )
×
119
                        : $scannedToken;
×
120

121
                    $missingTextLength = $token->getPosition() - $lastTokenEndIndex;
×
122
                    if ($missingTextLength > 0) {
×
123
                        $tokens[] = new Token(DocLexer::T_NONE, substr(
×
124
                            $content,
×
125
                            $nextAtPosition + $lastTokenEndIndex,
×
126
                            $missingTextLength
×
127
                        ));
×
128
                    }
129

130
                    $tokens[] = new Token($token->getType(), $token->getContent());
×
131
                    $lastTokenEndIndex = $token->getPosition() + \strlen($token->getContent());
×
132
                }
133

134
                $currentPosition = $ignoredTextPosition = $nextAtPosition + $token->getPosition() + \strlen($token->getContent());
×
135
            } else {
136
                $currentPosition = $nextAtPosition + 1;
×
137
            }
138
        }
139

140
        if ($ignoredTextPosition < \strlen($content)) {
3✔
141
            $tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition));
3✔
142
        }
143

144
        return self::fromArray($tokens);
3✔
145
    }
146

147
    /**
148
     * Create token collection from array.
149
     *
150
     * @param array<int, Token> $array       the array to import
151
     * @param ?bool             $saveIndices save the numeric indices used in the original array, default is yes
152
     */
153
    public static function fromArray($array, $saveIndices = null): self
154
    {
155
        $tokens = new self(\count($array));
3✔
156

157
        if (null === $saveIndices || $saveIndices) {
3✔
158
            foreach ($array as $key => $val) {
3✔
159
                $tokens[$key] = $val;
3✔
160
            }
161
        } else {
162
            $index = 0;
×
163

164
            foreach ($array as $val) {
×
165
                $tokens[$index++] = $val;
×
166
            }
167
        }
168

169
        return $tokens;
3✔
170
    }
171

172
    /**
173
     * Returns the index of the closest next token that is neither a comment nor a whitespace token.
174
     */
175
    public function getNextMeaningfulToken(int $index): ?int
176
    {
177
        return $this->getMeaningfulTokenSibling($index, 1);
×
178
    }
179

180
    /**
181
     * Returns the index of the last token that is part of the annotation at the given index.
182
     */
183
    public function getAnnotationEnd(int $index): ?int
184
    {
185
        $currentIndex = null;
×
186

187
        if (isset($this[$index + 2])) {
×
188
            if ($this[$index + 2]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
×
189
                $currentIndex = $index + 2;
×
190
            } elseif (
191
                isset($this[$index + 3])
×
192
                && $this[$index + 2]->isType(DocLexer::T_NONE)
×
193
                && $this[$index + 3]->isType(DocLexer::T_OPEN_PARENTHESIS)
×
194
                && Preg::match('/^(\R\s*\*\s*)*\s*$/', $this[$index + 2]->getContent())
×
195
            ) {
196
                $currentIndex = $index + 3;
×
197
            }
198
        }
199

200
        if (null !== $currentIndex) {
×
201
            $level = 0;
×
202
            for ($max = \count($this); $currentIndex < $max; ++$currentIndex) {
×
203
                if ($this[$currentIndex]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
×
204
                    ++$level;
×
205
                } elseif ($this[$currentIndex]->isType(DocLexer::T_CLOSE_PARENTHESIS)) {
×
206
                    --$level;
×
207
                }
208

209
                if (0 === $level) {
×
210
                    return $currentIndex;
×
211
                }
212
            }
213

214
            return null;
×
215
        }
216

217
        return $index + 1;
×
218
    }
219

220
    /**
221
     * Returns the code from the tokens.
222
     */
223
    public function getCode(): string
224
    {
225
        $code = '';
1✔
226
        foreach ($this as $token) {
1✔
227
            $code .= $token->getContent();
1✔
228
        }
229

230
        return $code;
1✔
231
    }
232

233
    /**
234
     * Inserts a token at the given index.
235
     */
236
    public function insertAt(int $index, Token $token): void
237
    {
238
        $this->setSize($this->getSize() + 1);
×
239

240
        for ($i = $this->getSize() - 1; $i > $index; --$i) {
×
241
            $this[$i] = $this[$i - 1] ?? new Token();
×
242
        }
243

244
        $this[$index] = $token;
×
245
    }
246

247
    public function offsetSet($index, $token): void
248
    {
249
        if (!$token instanceof Token) {
3✔
250
            throw new \InvalidArgumentException(\sprintf('Token must be an instance of %s, "%s" given.', Token::class, get_debug_type($token)));
2✔
251
        }
252

253
        parent::offsetSet($index, $token);
3✔
254
    }
255

256
    /**
257
     * @param mixed $index
258
     *
259
     * @throws \OutOfBoundsException
260
     */
261
    public function offsetUnset($index): void
262
    {
UNCOV
263
        if (!isset($this[$index])) {
×
UNCOV
264
            throw new \OutOfBoundsException(\sprintf('Index "%s" is invalid or does not exist.', $index));
×
265
        }
266

UNCOV
267
        $max = \count($this) - 1;
×
UNCOV
268
        while ($index < $max) {
×
UNCOV
269
            $this[$index] = $this[$index + 1];
×
UNCOV
270
            ++$index;
×
271
        }
272

273
        parent::offsetUnset($index);
×
274

UNCOV
275
        $this->setSize($max);
×
276
    }
277

278
    private function getMeaningfulTokenSibling(int $index, int $direction): ?int
279
    {
280
        while (true) {
×
UNCOV
281
            $index += $direction;
×
282

283
            if (!$this->offsetExists($index)) {
×
UNCOV
284
                break;
×
285
            }
286

UNCOV
287
            if (!$this[$index]->isType(DocLexer::T_NONE)) {
×
UNCOV
288
                return $index;
×
289
            }
290
        }
291

UNCOV
292
        return null;
×
293
    }
294
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc