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

keradus / PHP-CS-Fixer / 16303177127

15 Jul 2025 06:22PM UTC coverage: 94.758% (-0.05%) from 94.806%
16303177127

push

github

keradus
bumped version

28199 of 29759 relevant lines covered (94.76%)

45.91 hits per line

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

99.03
/src/Tokenizer/TokensAnalyzer.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\Tokenizer;
16

17
use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
18
use PhpCsFixer\Tokenizer\Analyzer\GotoLabelAnalyzer;
19

20
/**
21
 * Analyzer of Tokens collection.
22
 *
23
 * Its role is to provide the ability to analyze collection.
24
 *
25
 * @internal
26
 *
27
 * @phpstan-type _ClassyElementType 'case'|'const'|'method'|'property'|'promoted_property'|'trait_import'
28
 *
29
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
30
 * @author Gregor Harlan <gharlan@web.de>
31
 */
32
final class TokensAnalyzer
33
{
34
    /**
35
     * Tokens collection instance.
36
     */
37
    private Tokens $tokens;
38

39
    /**
40
     * @readonly
41
     */
42
    private GotoLabelAnalyzer $gotoLabelAnalyzer;
43

44
    public function __construct(Tokens $tokens)
45
    {
46
        $this->tokens = $tokens;
364✔
47
        $this->gotoLabelAnalyzer = new GotoLabelAnalyzer();
364✔
48
    }
49

50
    /**
51
     * Get indices of methods and properties in classy code (classes, interfaces and traits).
52
     *
53
     * @return array<int, array{classIndex: int, token: Token, type: _ClassyElementType}>
54
     */
55
    public function getClassyElements(): array
56
    {
57
        $elements = [];
18✔
58

59
        for ($index = 1, $count = \count($this->tokens) - 2; $index < $count; ++$index) {
18✔
60
            if ($this->tokens[$index]->isClassy()) {
18✔
61
                [$index, $newElements] = $this->findClassyElements($index, $index);
18✔
62
                $elements += $newElements;
18✔
63
            }
64
        }
65

66
        ksort($elements);
18✔
67

68
        return $elements;
18✔
69
    }
70

71
    /**
72
     * Get indices of modifiers of a classy code (classes, interfaces and traits).
73
     *
74
     * @return array{
75
     *     final: int|null,
76
     *     abstract: int|null,
77
     *     readonly: int|null
78
     * }
79
     */
80
    public function getClassyModifiers(int $index): array
81
    {
82
        if (!$this->tokens[$index]->isClassy()) {
10✔
83
            throw new \InvalidArgumentException(\sprintf('Not an "classy" at given index %d.', $index));
1✔
84
        }
85

86
        $modifiers = ['final' => null, 'abstract' => null, 'readonly' => null];
9✔
87

88
        while (true) {
9✔
89
            $index = $this->tokens->getPrevMeaningfulToken($index);
9✔
90

91
            if ($this->tokens[$index]->isGivenKind(\T_FINAL)) {
9✔
92
                $modifiers['final'] = $index;
4✔
93
            } elseif ($this->tokens[$index]->isGivenKind(\T_ABSTRACT)) {
9✔
94
                $modifiers['abstract'] = $index;
4✔
95
            } elseif ($this->tokens[$index]->isGivenKind(FCT::T_READONLY)) {
9✔
96
                $modifiers['readonly'] = $index;
7✔
97
            } else { // no need to skip attributes as it is not possible on PHP8.2
98
                break;
9✔
99
            }
100
        }
101

102
        return $modifiers;
9✔
103
    }
104

105
    /**
106
     * Get indices of namespace uses.
107
     *
108
     * @param bool $perNamespace Return namespace uses per namespace
109
     *
110
     * @return ($perNamespace is true ? array<int, list<int>> : list<int>)
111
     */
112
    public function getImportUseIndexes(bool $perNamespace = false): array
113
    {
114
        $tokens = $this->tokens;
10✔
115

116
        $uses = [];
10✔
117
        $namespaceIndex = 0;
10✔
118

119
        for ($index = 0, $limit = $tokens->count(); $index < $limit; ++$index) {
10✔
120
            $token = $tokens[$index];
10✔
121

122
            if ($token->isGivenKind(\T_NAMESPACE)) {
10✔
123
                $nextTokenIndex = $tokens->getNextTokenOfKind($index, [';', '{']);
2✔
124
                $nextToken = $tokens[$nextTokenIndex];
2✔
125

126
                if ($nextToken->equals('{')) {
2✔
127
                    $index = $nextTokenIndex;
2✔
128
                }
129

130
                if ($perNamespace) {
2✔
131
                    ++$namespaceIndex;
1✔
132
                }
133

134
                continue;
2✔
135
            }
136

137
            if ($token->isGivenKind(\T_USE)) {
10✔
138
                $uses[$namespaceIndex][] = $index;
10✔
139
            }
140
        }
141

142
        if (!$perNamespace && isset($uses[$namespaceIndex])) {
10✔
143
            return $uses[$namespaceIndex];
6✔
144
        }
145

146
        return $uses;
4✔
147
    }
148

149
    /**
150
     * Check if there is an array at given index.
151
     */
152
    public function isArray(int $index): bool
153
    {
154
        return $this->tokens[$index]->isGivenKind([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN]);
17✔
155
    }
156

157
    /**
158
     * Check if the array at index is multiline.
159
     *
160
     * This only checks the root-level of the array.
161
     */
162
    public function isArrayMultiLine(int $index): bool
163
    {
164
        if (!$this->isArray($index)) {
13✔
165
            throw new \InvalidArgumentException(\sprintf('Not an array at given index %d.', $index));
4✔
166
        }
167

168
        $tokens = $this->tokens;
9✔
169

170
        // Skip only when it's an array, for short arrays we need the brace for correct
171
        // level counting
172
        if ($tokens[$index]->isGivenKind(\T_ARRAY)) {
9✔
173
            $index = $tokens->getNextMeaningfulToken($index);
7✔
174
        }
175

176
        return $this->isBlockMultiline($tokens, $index);
9✔
177
    }
178

179
    public function isBlockMultiline(Tokens $tokens, int $index): bool
180
    {
181
        $blockType = Tokens::detectBlockType($tokens[$index]);
14✔
182

183
        if (null === $blockType || !$blockType['isStart']) {
14✔
184
            throw new \InvalidArgumentException(\sprintf('Not an block start at given index %d.', $index));
1✔
185
        }
186

187
        $endIndex = $tokens->findBlockEnd($blockType['type'], $index);
13✔
188

189
        for (++$index; $index < $endIndex; ++$index) {
13✔
190
            $token = $tokens[$index];
12✔
191
            $blockType = Tokens::detectBlockType($token);
12✔
192

193
            if (null !== $blockType && $blockType['isStart']) {
12✔
194
                $index = $tokens->findBlockEnd($blockType['type'], $index);
1✔
195

196
                continue;
1✔
197
            }
198

199
            if (
200
                $token->isWhitespace()
12✔
201
                && !$tokens[$index - 1]->isGivenKind(\T_END_HEREDOC)
12✔
202
                && str_contains($token->getContent(), "\n")
12✔
203
            ) {
204
                return true;
6✔
205
            }
206
        }
207

208
        return false;
9✔
209
    }
210

211
    /**
212
     * @param int $index Index of the T_FUNCTION token
213
     *
214
     * @return array{visibility: null|T_PRIVATE|T_PROTECTED|T_PUBLIC, static: bool, abstract: bool, final: bool}
215
     */
216
    public function getMethodAttributes(int $index): array
217
    {
218
        if (!$this->tokens[$index]->isGivenKind(\T_FUNCTION)) {
8✔
219
            throw new \LogicException(\sprintf('No T_FUNCTION at given index %d, got "%s".', $index, $this->tokens[$index]->getName()));
×
220
        }
221

222
        $attributes = [
8✔
223
            'visibility' => null,
8✔
224
            'static' => false,
8✔
225
            'abstract' => false,
8✔
226
            'final' => false,
8✔
227
        ];
8✔
228

229
        for ($i = $index; $i >= 0; --$i) {
8✔
230
            $i = $this->tokens->getPrevMeaningfulToken($i);
8✔
231
            $token = $this->tokens[$i];
8✔
232

233
            if ($token->isGivenKind(\T_STATIC)) {
8✔
234
                $attributes['static'] = true;
2✔
235

236
                continue;
2✔
237
            }
238

239
            if ($token->isGivenKind(\T_FINAL)) {
8✔
240
                $attributes['final'] = true;
1✔
241

242
                continue;
1✔
243
            }
244

245
            if ($token->isGivenKind(\T_ABSTRACT)) {
8✔
246
                $attributes['abstract'] = true;
2✔
247

248
                continue;
2✔
249
            }
250

251
            // visibility
252

253
            if ($token->isGivenKind(\T_PRIVATE)) {
8✔
254
                $attributes['visibility'] = \T_PRIVATE;
1✔
255

256
                continue;
1✔
257
            }
258

259
            if ($token->isGivenKind(\T_PROTECTED)) {
8✔
260
                $attributes['visibility'] = \T_PROTECTED;
1✔
261

262
                continue;
1✔
263
            }
264

265
            if ($token->isGivenKind(\T_PUBLIC)) {
8✔
266
                $attributes['visibility'] = \T_PUBLIC;
3✔
267

268
                continue;
3✔
269
            }
270

271
            // found a meaningful token that is not part of
272
            // the function signature; stop looking
273
            break;
8✔
274
        }
275

276
        return $attributes;
8✔
277
    }
278

279
    /**
280
     * Check if there is an anonymous class under given index.
281
     */
282
    public function isAnonymousClass(int $index): bool
283
    {
284
        if (!$this->tokens[$index]->isClassy()) {
12✔
285
            throw new \LogicException(\sprintf('No classy token at given index %d.', $index));
×
286
        }
287

288
        if (!$this->tokens[$index]->isGivenKind(\T_CLASS)) {
12✔
289
            return false;
2✔
290
        }
291

292
        $index = $this->tokens->getPrevMeaningfulToken($index);
10✔
293

294
        if ($this->tokens[$index]->isGivenKind(FCT::T_READONLY)) {
10✔
295
            $index = $this->tokens->getPrevMeaningfulToken($index);
3✔
296
        }
297

298
        while ($this->tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
10✔
299
            $index = $this->tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
4✔
300
            $index = $this->tokens->getPrevMeaningfulToken($index);
4✔
301
        }
302

303
        return $this->tokens[$index]->isGivenKind(\T_NEW);
10✔
304
    }
305

306
    /**
307
     * Check if the function under given index is a lambda.
308
     */
309
    public function isLambda(int $index): bool
310
    {
311
        if (!$this->tokens[$index]->isGivenKind([\T_FUNCTION, \T_FN])) {
19✔
312
            throw new \LogicException(\sprintf('No T_FUNCTION or T_FN at given index %d, got "%s".', $index, $this->tokens[$index]->getName()));
1✔
313
        }
314

315
        $startParenthesisIndex = $this->tokens->getNextMeaningfulToken($index);
18✔
316
        $startParenthesisToken = $this->tokens[$startParenthesisIndex];
18✔
317

318
        // skip & for `function & () {}` syntax
319
        if ($startParenthesisToken->isGivenKind(CT::T_RETURN_REF)) {
18✔
320
            $startParenthesisIndex = $this->tokens->getNextMeaningfulToken($startParenthesisIndex);
1✔
321
            $startParenthesisToken = $this->tokens[$startParenthesisIndex];
1✔
322
        }
323

324
        return $startParenthesisToken->equals('(');
18✔
325
    }
326

327
    public function getLastTokenIndexOfArrowFunction(int $index): int
328
    {
329
        if (!$this->tokens[$index]->isGivenKind(\T_FN)) {
11✔
330
            throw new \InvalidArgumentException(\sprintf('Not an "arrow function" at given index %d.', $index));
1✔
331
        }
332

333
        $stopTokens = [')', ']', ',', ';', [\T_CLOSE_TAG]];
10✔
334
        $index = $this->tokens->getNextTokenOfKind($index, [[\T_DOUBLE_ARROW]]);
10✔
335

336
        while (true) {
10✔
337
            $index = $this->tokens->getNextMeaningfulToken($index);
10✔
338

339
            if ($this->tokens[$index]->equalsAny($stopTokens)) {
10✔
340
                break;
9✔
341
            }
342

343
            $blockType = Tokens::detectBlockType($this->tokens[$index]);
10✔
344

345
            if (null === $blockType) {
10✔
346
                continue;
9✔
347
            }
348

349
            if ($blockType['isStart']) {
6✔
350
                $index = $this->tokens->findBlockEnd($blockType['type'], $index);
4✔
351

352
                continue;
4✔
353
            }
354

355
            break;
2✔
356
        }
357

358
        return $this->tokens->getPrevMeaningfulToken($index);
10✔
359
    }
360

361
    /**
362
     * Check if the T_STRING under given index is a constant invocation.
363
     */
364
    public function isConstantInvocation(int $index): bool
365
    {
366
        if (!$this->tokens[$index]->isGivenKind(\T_STRING)) {
86✔
367
            throw new \LogicException(\sprintf('No T_STRING at given index %d, got "%s".', $index, $this->tokens[$index]->getName()));
1✔
368
        }
369

370
        $nextIndex = $this->tokens->getNextMeaningfulToken($index);
85✔
371

372
        if (
373
            $this->tokens[$nextIndex]->equalsAny(['(', '{'])
85✔
374
            || $this->tokens[$nextIndex]->isGivenKind([\T_DOUBLE_COLON, \T_ELLIPSIS, \T_NS_SEPARATOR, CT::T_RETURN_REF, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, \T_VARIABLE])
85✔
375
        ) {
376
            return false;
64✔
377
        }
378

379
        // handle foreach( FOO as $_ ) {}
380
        if ($this->tokens[$nextIndex]->isGivenKind(\T_AS)) {
66✔
381
            $prevIndex = $this->tokens->getPrevMeaningfulToken($index);
3✔
382

383
            if (!$this->tokens[$prevIndex]->equals('(')) {
3✔
384
                return false;
2✔
385
            }
386
        }
387

388
        $prevIndex = $this->tokens->getPrevMeaningfulToken($index);
66✔
389

390
        if ($this->tokens[$prevIndex]->isGivenKind(Token::getClassyTokenKinds())) {
66✔
391
            return false;
5✔
392
        }
393

394
        if ($this->tokens[$prevIndex]->isGivenKind([\T_AS, \T_CONST, \T_DOUBLE_COLON, \T_FUNCTION, \T_GOTO, CT::T_GROUP_IMPORT_BRACE_OPEN, CT::T_TYPE_COLON, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION]) || $this->tokens[$prevIndex]->isObjectOperator()) {
64✔
395
            return false;
25✔
396
        }
397

398
        if (
399
            $this->tokens[$prevIndex]->isGivenKind(\T_CASE)
54✔
400
            && $this->tokens->isAllTokenKindsFound([FCT::T_ENUM])
54✔
401
        ) {
402
            $enumSwitchIndex = $this->tokens->getPrevTokenOfKind($index, [[\T_SWITCH], [\T_ENUM]]);
1✔
403

404
            if (!$this->tokens[$enumSwitchIndex]->isGivenKind(\T_SWITCH)) {
1✔
405
                return false;
1✔
406
            }
407
        }
408

409
        while ($this->tokens[$prevIndex]->isGivenKind([CT::T_NAMESPACE_OPERATOR, \T_NS_SEPARATOR, \T_STRING, CT::T_ARRAY_TYPEHINT])) {
53✔
410
            $prevIndex = $this->tokens->getPrevMeaningfulToken($prevIndex);
24✔
411
        }
412

413
        if ($this->tokens[$prevIndex]->isGivenKind([CT::T_CONST_IMPORT, \T_EXTENDS, CT::T_FUNCTION_IMPORT, \T_IMPLEMENTS, \T_INSTANCEOF, \T_INSTEADOF, \T_NAMESPACE, \T_NEW, CT::T_NULLABLE_TYPE, CT::T_TYPE_COLON, \T_USE, CT::T_USE_TRAIT, CT::T_TYPE_INTERSECTION, CT::T_TYPE_ALTERNATION, \T_CONST, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE])) {
53✔
414
            return false;
30✔
415
        }
416

417
        // `FOO & $bar` could be:
418
        //   - function reference parameter: function baz(Foo & $bar) {}
419
        //   - bit operator: $x = FOO & $bar;
420
        if ($this->tokens[$nextIndex]->equals('&') && $this->tokens[$this->tokens->getNextMeaningfulToken($nextIndex)]->isGivenKind(\T_VARIABLE)) {
36✔
421
            $checkIndex = $this->tokens->getPrevTokenOfKind($prevIndex, [';', '{', '}', [\T_FUNCTION], [\T_OPEN_TAG], [\T_OPEN_TAG_WITH_ECHO]]);
2✔
422

423
            if ($this->tokens[$checkIndex]->isGivenKind(\T_FUNCTION)) {
2✔
424
                return false;
1✔
425
            }
426
        }
427

428
        // check for `extends`/`implements`/`use` list
429
        if ($this->tokens[$prevIndex]->equals(',')) {
35✔
430
            $checkIndex = $prevIndex;
6✔
431

432
            while ($this->tokens[$checkIndex]->equalsAny([',', [\T_AS], [CT::T_NAMESPACE_OPERATOR], [\T_NS_SEPARATOR], [\T_STRING]])) {
6✔
433
                $checkIndex = $this->tokens->getPrevMeaningfulToken($checkIndex);
6✔
434
            }
435

436
            if ($this->tokens[$checkIndex]->isGivenKind([\T_EXTENDS, CT::T_GROUP_IMPORT_BRACE_OPEN, \T_IMPLEMENTS, \T_USE, CT::T_USE_TRAIT])) {
6✔
437
                return false;
4✔
438
            }
439
        }
440

441
        // check for array in double quoted string: `"..$foo[bar].."`
442
        if ($this->tokens[$prevIndex]->equals('[') && $this->tokens[$nextIndex]->equals(']')) {
31✔
443
            $checkToken = $this->tokens[$this->tokens->getNextMeaningfulToken($nextIndex)];
4✔
444

445
            if ($checkToken->equals('"') || $checkToken->isGivenKind([\T_CURLY_OPEN, \T_DOLLAR_OPEN_CURLY_BRACES, \T_ENCAPSED_AND_WHITESPACE, \T_VARIABLE])) {
4✔
446
                return false;
1✔
447
            }
448
        }
449

450
        // check for attribute: `#[Foo]`
451
        if (AttributeAnalyzer::isAttribute($this->tokens, $index)) {
30✔
452
            return false;
1✔
453
        }
454

455
        // check for goto label
456
        if ($this->tokens[$nextIndex]->equals(':')) {
29✔
457
            if ($this->gotoLabelAnalyzer->belongsToGoToLabel($this->tokens, $nextIndex)) {
3✔
458
                return false;
1✔
459
            }
460
        }
461

462
        // check for non-capturing catches
463

464
        while ($this->tokens[$prevIndex]->isGivenKind([CT::T_NAMESPACE_OPERATOR, \T_NS_SEPARATOR, \T_STRING, CT::T_TYPE_ALTERNATION])) {
28✔
465
            $prevIndex = $this->tokens->getPrevMeaningfulToken($prevIndex);
×
466
        }
467

468
        if ($this->tokens[$prevIndex]->equals('(')) {
28✔
469
            $prevPrevIndex = $this->tokens->getPrevMeaningfulToken($prevIndex);
6✔
470

471
            if ($this->tokens[$prevPrevIndex]->isGivenKind(\T_CATCH)) {
6✔
472
                return false;
2✔
473
            }
474
        }
475

476
        return true;
26✔
477
    }
478

479
    /**
480
     * Checks if there is a unary successor operator under given index.
481
     */
482
    public function isUnarySuccessorOperator(int $index): bool
483
    {
484
        $tokens = $this->tokens;
88✔
485
        $token = $tokens[$index];
88✔
486

487
        if (!$token->isGivenKind([\T_INC, \T_DEC])) {
88✔
488
            return false;
77✔
489
        }
490

491
        $prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
12✔
492

493
        return $prevToken->equalsAny([
12✔
494
            ']',
12✔
495
            [\T_STRING],
12✔
496
            [\T_VARIABLE],
12✔
497
            [CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
12✔
498
            [CT::T_DYNAMIC_PROP_BRACE_CLOSE],
12✔
499
            [CT::T_DYNAMIC_VAR_BRACE_CLOSE],
12✔
500
        ]);
12✔
501
    }
502

503
    /**
504
     * Checks if there is a unary predecessor operator under given index.
505
     */
506
    public function isUnaryPredecessorOperator(int $index): bool
507
    {
508
        $tokens = $this->tokens;
90✔
509
        $token = $tokens[$index];
90✔
510

511
        // potential unary successor operator
512
        if ($token->isGivenKind([\T_INC, \T_DEC])) {
90✔
513
            return !$this->isUnarySuccessorOperator($index);
12✔
514
        }
515

516
        // always unary predecessor operator
517
        if ($token->equalsAny(['!', '~', '@', [\T_ELLIPSIS]])) {
82✔
518
            return true;
7✔
519
        }
520

521
        // potential binary operator
522
        if (!$token->equalsAny(['+', '-', '&', [CT::T_RETURN_REF]])) {
82✔
523
            return false;
65✔
524
        }
525

526
        $prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
32✔
527

528
        if (!$prevToken->equalsAny([
32✔
529
            ']',
32✔
530
            '}',
32✔
531
            ')',
32✔
532
            '"',
32✔
533
            '`',
32✔
534
            [CT::T_ARRAY_SQUARE_BRACE_CLOSE],
32✔
535
            [CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
32✔
536
            [CT::T_DYNAMIC_PROP_BRACE_CLOSE],
32✔
537
            [CT::T_DYNAMIC_VAR_BRACE_CLOSE],
32✔
538
            [\T_CLASS_C],
32✔
539
            [\T_CONSTANT_ENCAPSED_STRING],
32✔
540
            [\T_DEC],
32✔
541
            [\T_DIR],
32✔
542
            [\T_DNUMBER],
32✔
543
            [\T_FILE],
32✔
544
            [\T_FUNC_C],
32✔
545
            [\T_INC],
32✔
546
            [\T_LINE],
32✔
547
            [\T_LNUMBER],
32✔
548
            [\T_METHOD_C],
32✔
549
            [\T_NS_C],
32✔
550
            [\T_STRING],
32✔
551
            [\T_TRAIT_C],
32✔
552
            [\T_VARIABLE],
32✔
553
        ])) {
32✔
554
            return true;
9✔
555
        }
556

557
        if (!$token->equals('&') || !$prevToken->isGivenKind(\T_STRING)) {
25✔
558
            return false;
17✔
559
        }
560

561
        $prevToken = $tokens[$tokens->getPrevTokenOfKind($index, [
8✔
562
            ';',
8✔
563
            '{',
8✔
564
            '}',
8✔
565
            [\T_DOUBLE_ARROW],
8✔
566
            [\T_FN],
8✔
567
            [\T_FUNCTION],
8✔
568
            [\T_OPEN_TAG],
8✔
569
            [\T_OPEN_TAG_WITH_ECHO],
8✔
570
        ])];
8✔
571

572
        return $prevToken->isGivenKind([\T_FN, \T_FUNCTION]);
8✔
573
    }
574

575
    /**
576
     * Checks if there is a binary operator under given index.
577
     */
578
    public function isBinaryOperator(int $index): bool
579
    {
580
        $tokens = $this->tokens;
99✔
581
        $token = $tokens[$index];
99✔
582

583
        if ($token->isGivenKind([\T_INLINE_HTML, \T_ENCAPSED_AND_WHITESPACE, CT::T_TYPE_INTERSECTION])) {
99✔
584
            return false;
7✔
585
        }
586

587
        // potential unary predecessor operator
588
        if (\in_array($token->getContent(), ['+', '-', '&'], true)) {
99✔
589
            return !$this->isUnaryPredecessorOperator($index);
30✔
590
        }
591

592
        if ($token->isArray()) {
93✔
593
            return \in_array($token->getId(), [
91✔
594
                \T_AND_EQUAL,            // &=
91✔
595
                \T_BOOLEAN_AND,          // &&
91✔
596
                \T_BOOLEAN_OR,           // ||
91✔
597
                \T_CONCAT_EQUAL,         // .=
91✔
598
                \T_DIV_EQUAL,            // /=
91✔
599
                \T_DOUBLE_ARROW,         // =>
91✔
600
                \T_IS_EQUAL,             // ==
91✔
601
                \T_IS_GREATER_OR_EQUAL,  // >=
91✔
602
                \T_IS_IDENTICAL,         // ===
91✔
603
                \T_IS_NOT_EQUAL,         // !=, <>
91✔
604
                \T_IS_NOT_IDENTICAL,     // !==
91✔
605
                \T_IS_SMALLER_OR_EQUAL,  // <=
91✔
606
                \T_LOGICAL_AND,          // and
91✔
607
                \T_LOGICAL_OR,           // or
91✔
608
                \T_LOGICAL_XOR,          // xor
91✔
609
                \T_MINUS_EQUAL,          // -=
91✔
610
                \T_MOD_EQUAL,            // %=
91✔
611
                \T_MUL_EQUAL,            // *=
91✔
612
                \T_OR_EQUAL,             // |=
91✔
613
                \T_PLUS_EQUAL,           // +=
91✔
614
                \T_POW,                  // **
91✔
615
                \T_POW_EQUAL,            // **=
91✔
616
                \T_SL,                   // <<
91✔
617
                \T_SL_EQUAL,             // <<=
91✔
618
                \T_SR,                   // >>
91✔
619
                \T_SR_EQUAL,             // >>=
91✔
620
                \T_XOR_EQUAL,            // ^=
91✔
621
                \T_SPACESHIP,            // <=>
91✔
622
                \T_COALESCE,             // ??
91✔
623
                \T_COALESCE_EQUAL,       // ??=
91✔
624
            ], true);
91✔
625
        }
626

627
        if (\in_array($token->getContent(), ['=', '*', '/', '%', '<', '>', '|', '^', '.'], true)) {
75✔
628
            return true;
17✔
629
        }
630

631
        return false;
74✔
632
    }
633

634
    /**
635
     * Check if `T_WHILE` token at given index is `do { ... } while ();` syntax
636
     * and not `while () { ...}`.
637
     */
638
    public function isWhilePartOfDoWhile(int $index): bool
639
    {
640
        $tokens = $this->tokens;
1✔
641
        $token = $tokens[$index];
1✔
642

643
        if (!$token->isGivenKind(\T_WHILE)) {
1✔
644
            throw new \LogicException(\sprintf('No T_WHILE at given index %d, got "%s".', $index, $token->getName()));
×
645
        }
646

647
        $endIndex = $tokens->getPrevMeaningfulToken($index);
1✔
648
        if (!$tokens[$endIndex]->equals('}')) {
1✔
649
            return false;
1✔
650
        }
651

652
        $startIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $endIndex);
1✔
653
        $beforeStartIndex = $tokens->getPrevMeaningfulToken($startIndex);
1✔
654

655
        return $tokens[$beforeStartIndex]->isGivenKind(\T_DO);
1✔
656
    }
657

658
    /**
659
     * @throws \LogicException when provided index does not point to token containing T_CASE
660
     */
661
    public function isEnumCase(int $caseIndex): bool
662
    {
663
        $tokens = $this->tokens;
5✔
664
        $token = $tokens[$caseIndex];
5✔
665

666
        if (!$token->isGivenKind(\T_CASE)) {
5✔
667
            throw new \LogicException(\sprintf(
5✔
668
                'No T_CASE given at index %d, got %s instead.',
5✔
669
                $caseIndex,
5✔
670
                $token->getName() ?? $token->getContent()
5✔
671
            ));
5✔
672
        }
673

674
        if (!$tokens->isTokenKindFound(FCT::T_ENUM)) {
5✔
675
            return false;
1✔
676
        }
677

678
        $prevIndex = $tokens->getPrevTokenOfKind($caseIndex, [[\T_ENUM], [\T_SWITCH]]);
4✔
679

680
        return null !== $prevIndex && $tokens[$prevIndex]->isGivenKind(\T_ENUM);
4✔
681
    }
682

683
    public function isSuperGlobal(int $index): bool
684
    {
685
        $token = $this->tokens[$index];
62✔
686

687
        if (!$token->isGivenKind(\T_VARIABLE)) {
62✔
688
            return false;
47✔
689
        }
690

691
        return \in_array(strtoupper($token->getContent()), [
15✔
692
            '$_COOKIE',
15✔
693
            '$_ENV',
15✔
694
            '$_FILES',
15✔
695
            '$_GET',
15✔
696
            '$_POST',
15✔
697
            '$_REQUEST',
15✔
698
            '$_SERVER',
15✔
699
            '$_SESSION',
15✔
700
            '$GLOBALS',
15✔
701
        ], true);
15✔
702
    }
703

704
    /**
705
     * Find classy elements.
706
     *
707
     * Searches in tokens from the classy (start) index till the end (index) of the classy.
708
     * Returns an array; first value is the index until the method has analysed (int), second the found classy elements (array).
709
     *
710
     * @param int $classIndex classy index
711
     *
712
     * @return array{int, array<int, array{classIndex: int, token: Token, type: _ClassyElementType}>}
713
     */
714
    private function findClassyElements(int $classIndex, int $index): array
715
    {
716
        $elements = [];
18✔
717
        $curlyBracesLevel = 0;
18✔
718
        $bracesLevel = 0;
18✔
719
        ++$index; // skip the classy index itself
18✔
720

721
        for ($count = \count($this->tokens); $index < $count; ++$index) {
18✔
722
            $token = $this->tokens[$index];
18✔
723

724
            if ($token->isGivenKind(\T_ENCAPSED_AND_WHITESPACE)) {
18✔
725
                continue;
1✔
726
            }
727

728
            if ($token->isGivenKind(\T_CLASS)) { // anonymous class in class
18✔
729
                // check for nested anonymous classes inside the new call of an anonymous class,
730
                // for example `new class(function (){new class(function (){new class(function (){}){};}){};}){};` etc.
731
                // if class(XYZ) {} skip till `(` as XYZ might contain functions etc.
732

733
                $nestedClassIndex = $index;
5✔
734
                $index = $this->tokens->getNextMeaningfulToken($index);
5✔
735

736
                if ($this->tokens[$index]->equals('(')) {
5✔
737
                    ++$index; // move after `(`
3✔
738

739
                    for ($nestedBracesLevel = 1; $index < $count; ++$index) {
3✔
740
                        $token = $this->tokens[$index];
3✔
741

742
                        if ($token->equals('(')) {
3✔
743
                            ++$nestedBracesLevel;
1✔
744

745
                            continue;
1✔
746
                        }
747

748
                        if ($token->equals(')')) {
3✔
749
                            --$nestedBracesLevel;
3✔
750

751
                            if (0 === $nestedBracesLevel) {
3✔
752
                                [$index, $newElements] = $this->findClassyElements($nestedClassIndex, $index);
3✔
753
                                $elements += $newElements;
3✔
754

755
                                break;
3✔
756
                            }
757

758
                            continue;
1✔
759
                        }
760

761
                        if ($token->isGivenKind(\T_CLASS)) { // anonymous class in class
1✔
762
                            [$index, $newElements] = $this->findClassyElements($index, $index);
1✔
763
                            $elements += $newElements;
1✔
764
                        }
765
                    }
766
                } else {
767
                    [$index, $newElements] = $this->findClassyElements($nestedClassIndex, $nestedClassIndex);
3✔
768
                    $elements += $newElements;
3✔
769
                }
770

771
                continue;
5✔
772
            }
773

774
            if ($token->equals('(')) {
18✔
775
                ++$bracesLevel;
12✔
776

777
                continue;
12✔
778
            }
779

780
            if ($token->equals(')')) {
18✔
781
                --$bracesLevel;
12✔
782

783
                continue;
12✔
784
            }
785

786
            if ($token->equals('{')) {
18✔
787
                ++$curlyBracesLevel;
18✔
788

789
                continue;
18✔
790
            }
791

792
            if ($token->equals('}')) {
18✔
793
                --$curlyBracesLevel;
18✔
794

795
                if (0 === $curlyBracesLevel) {
18✔
796
                    break;
18✔
797
                }
798

799
                continue;
11✔
800
            }
801

802
            if (1 !== $curlyBracesLevel || !$token->isArray()) {
18✔
803
                continue;
18✔
804
            }
805

806
            if (0 === $bracesLevel && $token->isGivenKind(\T_VARIABLE)) {
18✔
807
                $elements[$index] = [
6✔
808
                    'classIndex' => $classIndex,
6✔
809
                    'token' => $token,
6✔
810
                    'type' => 'property',
6✔
811
                ];
6✔
812

813
                continue;
6✔
814
            }
815

816
            if ($token->isGivenKind(CT::T_PROPERTY_HOOK_BRACE_OPEN)) {
18✔
817
                $index = $this->tokens->getNextTokenOfKind($index, [[CT::T_PROPERTY_HOOK_BRACE_CLOSE]]);
1✔
818

819
                continue;
1✔
820
            }
821

822
            if ($token->isGivenKind(\T_FUNCTION)) {
18✔
823
                $elements[$index] = [
11✔
824
                    'classIndex' => $classIndex,
11✔
825
                    'token' => $token,
11✔
826
                    'type' => 'method',
11✔
827
                ];
11✔
828
                $functionNameIndex = $this->tokens->getNextMeaningfulToken($index);
11✔
829
                if ('__construct' === $this->tokens[$functionNameIndex]->getContent()) {
11✔
830
                    $openParenthesis = $this->tokens->getNextMeaningfulToken($functionNameIndex);
4✔
831
                    $closeParenthesis = $this->tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis);
4✔
832
                    foreach ($this->tokens->findGivenKind([CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, FCT::T_READONLY, \T_FINAL], $openParenthesis, $closeParenthesis) as $kindElements) {
4✔
833
                        foreach (array_keys($kindElements) as $promotedPropertyModifierIndex) {
4✔
834
                            /** @var int $promotedPropertyVariableIndex */
835
                            $promotedPropertyVariableIndex = $this->tokens->getNextTokenOfKind($promotedPropertyModifierIndex, [[\T_VARIABLE]]);
3✔
836
                            $elements[$promotedPropertyVariableIndex] = [
3✔
837
                                'classIndex' => $classIndex,
3✔
838
                                'token' => $this->tokens[$promotedPropertyVariableIndex],
3✔
839
                                'type' => 'promoted_property',
3✔
840
                            ];
3✔
841
                        }
842
                    }
843
                }
844
            } elseif ($token->isGivenKind(\T_CONST)) {
18✔
845
                $elements[$index] = [
7✔
846
                    'classIndex' => $classIndex,
7✔
847
                    'token' => $token,
7✔
848
                    'type' => 'const',
7✔
849
                ];
7✔
850
            } elseif ($token->isGivenKind(CT::T_USE_TRAIT)) {
18✔
851
                $elements[$index] = [
2✔
852
                    'classIndex' => $classIndex,
2✔
853
                    'token' => $token,
2✔
854
                    'type' => 'trait_import',
2✔
855
                ];
2✔
856
            } elseif ($token->isGivenKind(\T_CASE)) {
18✔
857
                $elements[$index] = [
2✔
858
                    'classIndex' => $classIndex,
2✔
859
                    'token' => $token,
2✔
860
                    'type' => 'case',
2✔
861
                ];
2✔
862
            }
863
        }
864

865
        return [$index, $elements];
18✔
866
    }
867
}
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