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

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 hits per line

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

98.15
/src/Fixer/ControlStructure/YodaStyleFixer.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\Fixer\ControlStructure;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
22
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23
use PhpCsFixer\FixerDefinition\CodeSample;
24
use PhpCsFixer\FixerDefinition\FixerDefinition;
25
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
26
use PhpCsFixer\Tokenizer\CT;
27
use PhpCsFixer\Tokenizer\Token;
28
use PhpCsFixer\Tokenizer\Tokens;
29
use PhpCsFixer\Tokenizer\TokensAnalyzer;
30

31
/**
32
 * @phpstan-type _AutogeneratedInputConfiguration array{
33
 *  always_move_variable?: bool,
34
 *  equal?: bool|null,
35
 *  identical?: bool|null,
36
 *  less_and_greater?: bool|null,
37
 * }
38
 * @phpstan-type _AutogeneratedComputedConfiguration array{
39
 *  always_move_variable: bool,
40
 *  equal: bool|null,
41
 *  identical: bool|null,
42
 *  less_and_greater: bool|null,
43
 * }
44
 *
45
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
46
 *
47
 * @phpstan-import-type _PhpTokenKind from Token
48
 *
49
 * @author Bram Gotink <bram@gotink.me>
50
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
51
 *
52
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
53
 */
54
final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInterface
55
{
56
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
57
    use ConfigurableFixerTrait;
58

59
    /**
60
     * @var array<_PhpTokenKind, Token>
61
     */
62
    private array $candidatesMap;
63

64
    /**
65
     * @var array<_PhpTokenKind, null|bool>
66
     */
67
    private array $candidateTypesConfiguration;
68

69
    /**
70
     * @var list<_PhpTokenKind>
71
     */
72
    private array $candidateTypes;
73

74
    public function getDefinition(): FixerDefinitionInterface
75
    {
76
        return new FixerDefinition(
3✔
77
            'Write conditions in Yoda style (`true`), non-Yoda style (`[\'equal\' => false, \'identical\' => false, \'less_and_greater\' => false]`) or ignore those conditions (`null`) based on configuration.',
3✔
78
            [
3✔
79
                new CodeSample(
3✔
80
                    <<<'PHP'
3✔
81
                        <?php
82
                            if ($a === null) {
83
                                echo "null";
84
                            }
85

86
                        PHP
3✔
87
                ),
3✔
88
                new CodeSample(
3✔
89
                    <<<'PHP'
3✔
90
                        <?php
91
                            $b = $c != 1;  // equal
92
                            $a = 1 === $b; // identical
93
                            $c = $c > 3;   // less than
94

95
                        PHP,
3✔
96
                    [
3✔
97
                        'equal' => true,
3✔
98
                        'identical' => false,
3✔
99
                        'less_and_greater' => null,
3✔
100
                    ]
3✔
101
                ),
3✔
102
                new CodeSample(
3✔
103
                    <<<'PHP'
3✔
104
                        <?php
105
                        return $foo === count($bar);
106

107
                        PHP,
3✔
108
                    [
3✔
109
                        'always_move_variable' => true,
3✔
110
                    ]
3✔
111
                ),
3✔
112
                new CodeSample(
3✔
113
                    <<<'PHP'
3✔
114
                        <?php
115
                            // Enforce non-Yoda style.
116
                            if (null === $a) {
117
                                echo "null";
118
                            }
119

120
                        PHP,
3✔
121
                    [
3✔
122
                        'equal' => false,
3✔
123
                        'identical' => false,
3✔
124
                        'less_and_greater' => false,
3✔
125
                    ]
3✔
126
                ),
3✔
127
            ]
3✔
128
        );
3✔
129
    }
130

131
    /**
132
     * {@inheritdoc}
133
     *
134
     * Must run after IsNullFixer.
135
     */
136
    public function getPriority(): int
137
    {
138
        return 0;
1✔
139
    }
140

141
    public function isCandidate(Tokens $tokens): bool
142
    {
143
        return $tokens->isAnyTokenKindsFound($this->candidateTypes);
415✔
144
    }
145

146
    protected function configurePostNormalisation(): void
147
    {
148
        $this->resolveConfiguration();
426✔
149
    }
150

151
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
152
    {
153
        $this->fixTokens($tokens);
403✔
154
    }
155

156
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
157
    {
158
        return new FixerConfigurationResolver([
426✔
159
            (new FixerOptionBuilder('equal', 'Style for equal (`==`, `!=`) statements.'))
426✔
160
                ->setAllowedTypes(['bool', 'null'])
426✔
161
                ->setDefault(true)
426✔
162
                ->getOption(),
426✔
163
            (new FixerOptionBuilder('identical', 'Style for identical (`===`, `!==`) statements.'))
426✔
164
                ->setAllowedTypes(['bool', 'null'])
426✔
165
                ->setDefault(true)
426✔
166
                ->getOption(),
426✔
167
            (new FixerOptionBuilder('less_and_greater', 'Style for less and greater than (`<`, `<=`, `>`, `>=`) statements.'))
426✔
168
                ->setAllowedTypes(['bool', 'null'])
426✔
169
                ->setDefault(null)
426✔
170
                ->getOption(),
426✔
171
            (new FixerOptionBuilder('always_move_variable', 'Whether variables should always be on non assignable side when applying Yoda style.'))
426✔
172
                ->setAllowedTypes(['bool'])
426✔
173
                ->setDefault(false)
426✔
174
                ->getOption(),
426✔
175
        ]);
426✔
176
    }
177

178
    /**
179
     * Finds the end of the right-hand side of the comparison at the given
180
     * index.
181
     *
182
     * The right-hand side ends when an operator with a lower precedence is
183
     * encountered or when the block level for `()`, `{}` or `[]` goes below
184
     * zero.
185
     *
186
     * @param Tokens $tokens The token list
187
     * @param int    $index  The index of the comparison
188
     *
189
     * @return int The last index of the right-hand side of the comparison
190
     */
191
    private function findComparisonEnd(Tokens $tokens, int $index): int
192
    {
193
        ++$index;
403✔
194
        $count = \count($tokens);
403✔
195

196
        while ($index < $count) {
403✔
197
            $token = $tokens[$index];
403✔
198

199
            if ($token->isGivenKind([\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT])) {
403✔
200
                ++$index;
401✔
201

202
                continue;
401✔
203
            }
204

205
            if ($this->isOfLowerPrecedence($token)) {
403✔
206
                break;
378✔
207
            }
208

209
            $block = Tokens::detectBlockType($token);
403✔
210

211
            if (null === $block) {
403✔
212
                ++$index;
391✔
213

214
                continue;
391✔
215
            }
216

217
            if (!$block['isStart']) {
147✔
218
                break;
20✔
219
            }
220

221
            $index = $tokens->findBlockEnd($block['type'], $index) + 1;
135✔
222
        }
223

224
        $prev = $tokens->getPrevMeaningfulToken($index);
403✔
225

226
        return $tokens[$prev]->isGivenKind(\T_CLOSE_TAG) ? $tokens->getPrevMeaningfulToken($prev) : $prev;
403✔
227
    }
228

229
    /**
230
     * Finds the start of the left-hand side of the comparison at the given
231
     * index.
232
     *
233
     * The left-hand side ends when an operator with a lower precedence is
234
     * encountered or when the block level for `()`, `{}` or `[]` goes below
235
     * zero.
236
     *
237
     * @param Tokens $tokens The token list
238
     * @param int    $index  The index of the comparison
239
     *
240
     * @return int The first index of the left-hand side of the comparison
241
     */
242
    private function findComparisonStart(Tokens $tokens, int $index): int
243
    {
244
        --$index;
381✔
245
        $nonBlockFound = false;
381✔
246

247
        while (0 <= $index) {
381✔
248
            $token = $tokens[$index];
381✔
249

250
            if ($token->isGivenKind([\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT])) {
381✔
251
                --$index;
379✔
252

253
                continue;
379✔
254
            }
255

256
            if ($token->isGivenKind(CT::T_NAMED_ARGUMENT_COLON)) {
381✔
257
                break;
1✔
258
            }
259

260
            if ($this->isOfLowerPrecedence($token)) {
381✔
261
                break;
359✔
262
            }
263

264
            $block = Tokens::detectBlockType($token);
381✔
265

266
            if (null === $block) {
381✔
267
                --$index;
373✔
268
                $nonBlockFound = true;
373✔
269

270
                continue;
373✔
271
            }
272

273
            if (
274
                $block['isStart']
135✔
275
                || ($nonBlockFound && Tokens::BLOCK_TYPE_CURLY_BRACE === $block['type']) // closing of structure not related to the comparison
135✔
276
            ) {
277
                break;
32✔
278
            }
279

280
            $index = $tokens->findBlockStart($block['type'], $index) - 1;
113✔
281
        }
282

283
        return $tokens->getNextMeaningfulToken($index);
381✔
284
    }
285

286
    private function fixTokens(Tokens $tokens): Tokens
287
    {
288
        for ($i = \count($tokens) - 1; $i > 1; --$i) {
403✔
289
            if ($tokens[$i]->isGivenKind($this->candidateTypes)) {
403✔
290
                $yoda = $this->candidateTypesConfiguration[$tokens[$i]->getId()];
399✔
291
            } elseif (
292
                ($tokens[$i]->equals('<') && \in_array('<', $this->candidateTypes, true))
403✔
293
                || ($tokens[$i]->equals('>') && \in_array('>', $this->candidateTypes, true))
403✔
294
            ) {
295
                $yoda = $this->candidateTypesConfiguration[$tokens[$i]->getContent()];
5✔
296
            } else {
297
                continue;
403✔
298
            }
299

300
            $fixableCompareInfo = $this->getCompareFixableInfo($tokens, $i, $yoda);
403✔
301

302
            if (null === $fixableCompareInfo) {
403✔
303
                continue;
402✔
304
            }
305

306
            $i = $this->fixTokensCompare(
274✔
307
                $tokens,
274✔
308
                $fixableCompareInfo['left']['start'],
274✔
309
                $fixableCompareInfo['left']['end'],
274✔
310
                $i,
274✔
311
                $fixableCompareInfo['right']['start'],
274✔
312
                $fixableCompareInfo['right']['end']
274✔
313
            );
274✔
314
        }
315

316
        return $tokens;
403✔
317
    }
318

319
    /**
320
     * Fixes the comparison at the given index.
321
     *
322
     * A comparison is considered fixed when
323
     * - both sides are a variable (e.g. $a === $b)
324
     * - neither side is a variable (e.g. self::CONST === 3)
325
     * - only the right-hand side is a variable (e.g. 3 === self::$var)
326
     *
327
     * If the left-hand side and right-hand side of the given comparison are
328
     * swapped, this function runs recursively on the previous left-hand-side.
329
     *
330
     * @return int an upper bound for all non-fixed comparisons
331
     */
332
    private function fixTokensCompare(
333
        Tokens $tokens,
334
        int $startLeft,
335
        int $endLeft,
336
        int $compareOperatorIndex,
337
        int $startRight,
338
        int $endRight
339
    ): int {
340
        $type = $tokens[$compareOperatorIndex]->getId();
274✔
341
        $content = $tokens[$compareOperatorIndex]->getContent();
274✔
342

343
        if (\array_key_exists($type, $this->candidatesMap)) {
274✔
344
            $tokens[$compareOperatorIndex] = clone $this->candidatesMap[$type];
2✔
345
        } elseif (\array_key_exists($content, $this->candidatesMap)) {
272✔
346
            $tokens[$compareOperatorIndex] = clone $this->candidatesMap[$content];
4✔
347
        }
348

349
        $right = $this->fixTokensComparePart($tokens, $startRight, $endRight);
274✔
350
        $left = $this->fixTokensComparePart($tokens, $startLeft, $endLeft);
274✔
351

352
        for ($i = $startRight; $i <= $endRight; ++$i) {
274✔
353
            $tokens->clearAt($i);
274✔
354
        }
355

356
        for ($i = $startLeft; $i <= $endLeft; ++$i) {
274✔
357
            $tokens->clearAt($i);
274✔
358
        }
359

360
        $tokens->insertAt($startRight, $left);
274✔
361
        $tokens->insertAt($startLeft, $right);
274✔
362

363
        return $startLeft;
274✔
364
    }
365

366
    private function fixTokensComparePart(Tokens $tokens, int $start, int $end): Tokens
367
    {
368
        $newTokens = $tokens->generatePartialCode($start, $end);
274✔
369
        $newTokens = $this->fixTokens(Tokens::fromCode(\sprintf('<?php %s;', $newTokens)));
274✔
370
        $newTokens->clearAt(\count($newTokens) - 1);
274✔
371
        $newTokens->clearAt(0);
274✔
372
        $newTokens->clearEmptyTokens();
274✔
373

374
        return $newTokens;
274✔
375
    }
376

377
    /**
378
     * @return null|array{left: array{start: int, end: int}, right: array{start: int, end: int}}
379
     */
380
    private function getCompareFixableInfo(Tokens $tokens, int $index, bool $yoda): ?array
381
    {
382
        $right = $this->getRightSideCompareFixableInfo($tokens, $index);
403✔
383

384
        if (!$yoda && $this->isOfLowerPrecedenceAssignment($tokens[$tokens->getNextMeaningfulToken($right['end'])])) {
403✔
385
            return null;
22✔
386
        }
387

388
        $left = $this->getLeftSideCompareFixableInfo($tokens, $index);
381✔
389

390
        if ($this->isListStatement($tokens, $left['start'], $left['end']) || $this->isListStatement($tokens, $right['start'], $right['end'])) {
381✔
391
            return null; // do not fix lists assignment inside statements
6✔
392
        }
393

394
        /** @var bool $strict */
395
        $strict = $this->configuration['always_move_variable'];
375✔
396
        $leftSideIsVariable = $this->isVariable($tokens, $left['start'], $left['end'], $strict);
375✔
397
        $rightSideIsVariable = $this->isVariable($tokens, $right['start'], $right['end'], $strict);
375✔
398

399
        if (!($leftSideIsVariable xor $rightSideIsVariable)) {
375✔
400
            return null; // both are (not) variables, do not touch
85✔
401
        }
402

403
        if (!$strict) { // special handling for braces with not "always_move_variable"
294✔
404
            $leftSideIsVariable = $leftSideIsVariable && !$tokens[$left['start']]->equals('(');
239✔
405
            $rightSideIsVariable = $rightSideIsVariable && !$tokens[$right['start']]->equals('(');
239✔
406
        }
407

408
        return ($yoda && !$leftSideIsVariable) || (!$yoda && !$rightSideIsVariable)
294✔
409
            ? null
293✔
410
            : ['left' => $left, 'right' => $right];
294✔
411
    }
412

413
    /**
414
     * @return array{start: int, end: int}
415
     */
416
    private function getLeftSideCompareFixableInfo(Tokens $tokens, int $index): array
417
    {
418
        return [
381✔
419
            'start' => $this->findComparisonStart($tokens, $index),
381✔
420
            'end' => $tokens->getPrevMeaningfulToken($index),
381✔
421
        ];
381✔
422
    }
423

424
    /**
425
     * @return array{start: int, end: int}
426
     */
427
    private function getRightSideCompareFixableInfo(Tokens $tokens, int $index): array
428
    {
429
        return [
403✔
430
            'start' => $tokens->getNextMeaningfulToken($index),
403✔
431
            'end' => $this->findComparisonEnd($tokens, $index),
403✔
432
        ];
403✔
433
    }
434

435
    private function isListStatement(Tokens $tokens, int $index, int $end): bool
436
    {
437
        for ($i = $index; $i <= $end; ++$i) {
381✔
438
            if ($tokens[$i]->isGivenKind([\T_LIST, CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN, CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE])) {
381✔
439
                return true;
6✔
440
            }
441
        }
442

443
        return false;
381✔
444
    }
445

446
    /**
447
     * Checks whether the given token has a lower precedence than `T_IS_EQUAL`
448
     * or `T_IS_IDENTICAL`.
449
     *
450
     * @param Token $token The token to check
451
     *
452
     * @return bool Whether the token has a lower precedence
453
     */
454
    private function isOfLowerPrecedence(Token $token): bool
455
    {
456
        return $this->isOfLowerPrecedenceAssignment($token)
403✔
457
            || $token->isGivenKind([
403✔
458
                \T_BOOLEAN_AND,  // &&
403✔
459
                \T_BOOLEAN_OR,   // ||
403✔
460
                \T_CASE,         // case
403✔
461
                \T_DOUBLE_ARROW, // =>
403✔
462
                \T_ECHO,         // echo
403✔
463
                \T_GOTO,         // goto
403✔
464
                \T_LOGICAL_AND,  // and
403✔
465
                \T_LOGICAL_OR,   // or
403✔
466
                \T_LOGICAL_XOR,  // xor
403✔
467
                \T_OPEN_TAG,     // <?php
403✔
468
                \T_OPEN_TAG_WITH_ECHO,
403✔
469
                \T_PRINT,        // print
403✔
470
                \T_RETURN,       // return
403✔
471
                \T_THROW,        // throw
403✔
472
                \T_COALESCE,
403✔
473
                \T_YIELD,        // yield
403✔
474
                \T_YIELD_FROM,
403✔
475
                \T_REQUIRE,
403✔
476
                \T_REQUIRE_ONCE,
403✔
477
                \T_INCLUDE,
403✔
478
                \T_INCLUDE_ONCE,
403✔
479
            ])
403✔
480
            || $token->equalsAny([
403✔
481
                // bitwise and, or, xor
482
                '&', '|', '^',
403✔
483
                // ternary operators
484
                '?', ':',
403✔
485
                // end of PHP statement
486
                ',', ';',
403✔
487
            ]);
403✔
488
    }
489

490
    /**
491
     * Checks whether the given assignment token has a lower precedence than `T_IS_EQUAL`
492
     * or `T_IS_IDENTICAL`.
493
     */
494
    private function isOfLowerPrecedenceAssignment(Token $token): bool
495
    {
496
        return $token->equals('=') || $token->isGivenKind([
403✔
497
            \T_AND_EQUAL,      // &=
403✔
498
            \T_CONCAT_EQUAL,   // .=
403✔
499
            \T_DIV_EQUAL,      // /=
403✔
500
            \T_MINUS_EQUAL,    // -=
403✔
501
            \T_MOD_EQUAL,      // %=
403✔
502
            \T_MUL_EQUAL,      // *=
403✔
503
            \T_OR_EQUAL,       // |=
403✔
504
            \T_PLUS_EQUAL,     // +=
403✔
505
            \T_POW_EQUAL,      // **=
403✔
506
            \T_SL_EQUAL,       // <<=
403✔
507
            \T_SR_EQUAL,       // >>=
403✔
508
            \T_XOR_EQUAL,      // ^=
403✔
509
            \T_COALESCE_EQUAL, // ??=
403✔
510
        ]);
403✔
511
    }
512

513
    /**
514
     * Checks whether the tokens between the given start and end describe a
515
     * variable.
516
     *
517
     * @param Tokens $tokens The token list
518
     * @param int    $start  The first index of the possible variable
519
     * @param int    $end    The last index of the possible variable
520
     * @param bool   $strict Enable strict variable detection
521
     *
522
     * @return bool Whether the tokens describe a variable
523
     */
524
    private function isVariable(Tokens $tokens, int $start, int $end, bool $strict): bool
525
    {
526
        $tokenAnalyzer = new TokensAnalyzer($tokens);
375✔
527

528
        if ($start === $end) {
375✔
529
            return $tokens[$start]->isGivenKind(\T_VARIABLE);
357✔
530
        }
531

532
        if ($tokens[$start]->equals('(')) {
252✔
533
            return true;
26✔
534
        }
535

536
        if ($strict) {
230✔
537
            for ($index = $start; $index <= $end; ++$index) {
54✔
538
                if (
539
                    $tokens[$index]->isCast()
54✔
540
                    || $tokens[$index]->isGivenKind(\T_INSTANCEOF)
54✔
541
                    || $tokens[$index]->equals('!')
54✔
542
                    || $tokenAnalyzer->isBinaryOperator($index)
54✔
543
                ) {
544
                    return false;
46✔
545
                }
546
            }
547
        }
548

549
        $index = $start;
184✔
550

551
        // handle multiple braces around statement ((($a === 1)))
552
        while (
553
            $tokens[$index]->equals('(')
184✔
554
            && $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index) === $end
184✔
555
        ) {
UNCOV
556
            $index = $tokens->getNextMeaningfulToken($index);
×
557
            $end = $tokens->getPrevMeaningfulToken($end);
×
558
        }
559

560
        $expectString = false;
184✔
561

562
        while ($index <= $end) {
184✔
563
            $current = $tokens[$index];
184✔
564
            if ($current->isComment() || $current->isWhitespace() || $tokens->isEmptyAt($index)) {
184✔
UNCOV
565
                ++$index;
×
566

UNCOV
567
                continue;
×
568
            }
569

570
            // check if this is the last token
571
            if ($index === $end) {
184✔
572
                return $current->isGivenKind($expectString ? \T_STRING : \T_VARIABLE);
26✔
573
            }
574

575
            if ($current->isGivenKind([\T_LIST, CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN, CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE])) {
184✔
UNCOV
576
                return false;
×
577
            }
578

579
            $nextIndex = $tokens->getNextMeaningfulToken($index);
184✔
580
            $next = $tokens[$nextIndex];
184✔
581

582
            // self:: or ClassName::
583
            if ($current->isGivenKind(\T_STRING) && $next->isGivenKind(\T_DOUBLE_COLON)) {
184✔
584
                $index = $tokens->getNextMeaningfulToken($nextIndex);
8✔
585

586
                continue;
8✔
587
            }
588

589
            // \ClassName
590
            if ($current->isGivenKind(\T_NS_SEPARATOR) && $next->isGivenKind(\T_STRING)) {
178✔
591
                $index = $nextIndex;
2✔
592

593
                continue;
2✔
594
            }
595

596
            // ClassName\
597
            if ($current->isGivenKind(\T_STRING) && $next->isGivenKind(\T_NS_SEPARATOR)) {
178✔
598
                $index = $nextIndex;
2✔
599

600
                continue;
2✔
601
            }
602

603
            // $a-> or a-> (as in $b->a->c)
604
            if ($current->isGivenKind([\T_STRING, \T_VARIABLE]) && $next->isObjectOperator()) {
176✔
605
                $index = $tokens->getNextMeaningfulToken($nextIndex);
36✔
606
                $expectString = true;
36✔
607

608
                continue;
36✔
609
            }
610

611
            // $a[...], a[...] (as in $c->a[$b]), $a{...} or a{...} (as in $c->a{$b})
612
            if (
613
                $current->isGivenKind($expectString ? \T_STRING : \T_VARIABLE)
169✔
614
                && $next->equalsAny(['[', [CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN, '{']])
169✔
615
            ) {
616
                $index = $tokens->findBlockEnd(
23✔
617
                    $next->equals('[') ? Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE : Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE,
23✔
618
                    $nextIndex
23✔
619
                );
23✔
620

621
                if ($index === $end) {
23✔
622
                    return true;
14✔
623
                }
624

625
                $index = $tokens->getNextMeaningfulToken($index);
11✔
626

627
                if (!$tokens[$index]->equalsAny(['[', [CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN, '{']]) && !$tokens[$index]->isObjectOperator()) {
11✔
628
                    return false;
2✔
629
                }
630

631
                $index = $tokens->getNextMeaningfulToken($index);
9✔
632
                $expectString = true;
9✔
633

634
                continue;
9✔
635
            }
636

637
            // $a(...) or $a->b(...)
638
            if ($strict && $current->isGivenKind([\T_STRING, \T_VARIABLE]) && $next->equals('(')) {
150✔
639
                return false;
6✔
640
            }
641

642
            // {...} (as in $a->{$b})
643
            if ($expectString && $current->isGivenKind(CT::T_DYNAMIC_PROP_BRACE_OPEN)) {
144✔
644
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, $index);
11✔
645
                if ($index === $end) {
11✔
646
                    return true;
2✔
647
                }
648

649
                $index = $tokens->getNextMeaningfulToken($index);
9✔
650

651
                if (!$tokens[$index]->isObjectOperator()) {
9✔
652
                    return false;
3✔
653
                }
654

655
                $index = $tokens->getNextMeaningfulToken($index);
6✔
656
                $expectString = true;
6✔
657

658
                continue;
6✔
659
            }
660

661
            break;
133✔
662
        }
663

664
        return !$this->isConstant($tokens, $start, $end);
133✔
665
    }
666

667
    private function isConstant(Tokens $tokens, int $index, int $end): bool
668
    {
669
        $expectArrayOnly = false;
133✔
670
        $expectNumberOnly = false;
133✔
671
        $expectNothing = false;
133✔
672

673
        for (; $index <= $end; ++$index) {
133✔
674
            $token = $tokens[$index];
133✔
675

676
            if ($token->isComment() || $token->isWhitespace()) {
133✔
677
                continue;
14✔
678
            }
679

680
            if ($expectNothing) {
133✔
681
                return false;
10✔
682
            }
683

684
            if ($expectArrayOnly) {
133✔
685
                if ($token->equalsAny(['(', ')', [CT::T_ARRAY_SQUARE_BRACE_CLOSE]])) {
38✔
686
                    continue;
32✔
687
                }
688

689
                return false;
30✔
690
            }
691

692
            if ($token->isGivenKind([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
133✔
693
                $expectArrayOnly = true;
38✔
694

695
                continue;
38✔
696
            }
697

698
            if ($expectNumberOnly && !$token->isGivenKind([\T_LNUMBER, \T_DNUMBER])) {
97✔
UNCOV
699
                return false;
×
700
            }
701

702
            if ($token->equals('-')) {
97✔
703
                $expectNumberOnly = true;
6✔
704

705
                continue;
6✔
706
            }
707

708
            if (
709
                $token->isGivenKind([\T_LNUMBER, \T_DNUMBER, \T_CONSTANT_ENCAPSED_STRING])
97✔
710
                || $token->equalsAny([[\T_STRING, 'true'], [\T_STRING, 'false'], [\T_STRING, 'null']])
97✔
711
            ) {
712
                $expectNothing = true;
16✔
713

714
                continue;
16✔
715
            }
716

717
            return false;
87✔
718
        }
719

720
        return true;
14✔
721
    }
722

723
    private function resolveConfiguration(): void
724
    {
725
        $candidateTypes = [];
426✔
726
        $this->candidatesMap = [];
426✔
727

728
        if (null !== $this->configuration['equal']) {
426✔
729
            // `==`, `!=` and `<>`
730
            $candidateTypes[\T_IS_EQUAL] = $this->configuration['equal'];
426✔
731
            $candidateTypes[\T_IS_NOT_EQUAL] = $this->configuration['equal'];
426✔
732
        }
733

734
        if (null !== $this->configuration['identical']) {
426✔
735
            // `===` and `!==`
736
            $candidateTypes[\T_IS_IDENTICAL] = $this->configuration['identical'];
426✔
737
            $candidateTypes[\T_IS_NOT_IDENTICAL] = $this->configuration['identical'];
426✔
738
        }
739

740
        if (null !== $this->configuration['less_and_greater']) {
426✔
741
            // `<`, `<=`, `>` and `>=`
742
            $candidateTypes[\T_IS_SMALLER_OR_EQUAL] = $this->configuration['less_and_greater'];
10✔
743
            $this->candidatesMap[\T_IS_SMALLER_OR_EQUAL] = new Token([\T_IS_GREATER_OR_EQUAL, '>=']);
10✔
744

745
            $candidateTypes[\T_IS_GREATER_OR_EQUAL] = $this->configuration['less_and_greater'];
10✔
746
            $this->candidatesMap[\T_IS_GREATER_OR_EQUAL] = new Token([\T_IS_SMALLER_OR_EQUAL, '<=']);
10✔
747

748
            $candidateTypes['<'] = $this->configuration['less_and_greater'];
10✔
749
            $this->candidatesMap['<'] = new Token('>');
10✔
750

751
            $candidateTypes['>'] = $this->configuration['less_and_greater'];
10✔
752
            $this->candidatesMap['>'] = new Token('<');
10✔
753
        }
754

755
        $this->candidateTypesConfiguration = $candidateTypes;
426✔
756
        $this->candidateTypes = array_keys($candidateTypes);
426✔
757
    }
758
}
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