• 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

97.73
/src/Fixer/ControlStructure/NoUnneededControlParenthesesFixer.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\AllowedValueSubset;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
22
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
23
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
24
use PhpCsFixer\FixerDefinition\CodeSample;
25
use PhpCsFixer\FixerDefinition\FixerDefinition;
26
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
27
use PhpCsFixer\Tokenizer\CT;
28
use PhpCsFixer\Tokenizer\FCT;
29
use PhpCsFixer\Tokenizer\Token;
30
use PhpCsFixer\Tokenizer\Tokens;
31
use PhpCsFixer\Tokenizer\TokensAnalyzer;
32

33
/**
34
 * @phpstan-type _AutogeneratedInputConfiguration array{
35
 *  statements?: list<'break'|'clone'|'continue'|'echo_print'|'negative_instanceof'|'others'|'return'|'switch_case'|'yield'|'yield_from'>,
36
 * }
37
 * @phpstan-type _AutogeneratedComputedConfiguration array{
38
 *  statements: list<'break'|'clone'|'continue'|'echo_print'|'negative_instanceof'|'others'|'return'|'switch_case'|'yield'|'yield_from'>,
39
 * }
40
 *
41
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
42
 *
43
 * @phpstan-import-type _PhpTokenPrototypePartial from Token
44
 *
45
 * @author Sullivan Senechal <soullivaneuh@gmail.com>
46
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
47
 * @author Gregor Harlan <gharlan@web.de>
48
 *
49
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
50
 */
51
final class NoUnneededControlParenthesesFixer extends AbstractFixer implements ConfigurableFixerInterface
52
{
53
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
54
    use ConfigurableFixerTrait;
55

56
    /**
57
     * @var non-empty-list<int>
58
     */
59
    private const BLOCK_TYPES = [
60
        Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE,
61
        Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE,
62
        Tokens::BLOCK_TYPE_CURLY_BRACE,
63
        Tokens::BLOCK_TYPE_DESTRUCTURING_SQUARE_BRACE,
64
        Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE,
65
        Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE,
66
        Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE,
67
        Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
68
    ];
69

70
    private const BEFORE_TYPES = [
71
        ';',
72
        '{',
73
        [\T_OPEN_TAG],
74
        [\T_OPEN_TAG_WITH_ECHO],
75
        [\T_ECHO],
76
        [\T_PRINT],
77
        [\T_RETURN],
78
        [\T_THROW],
79
        [\T_YIELD],
80
        [\T_YIELD_FROM],
81
        [\T_BREAK],
82
        [\T_CONTINUE],
83
        // won't be fixed, but true in concept, helpful for fast check
84
        [\T_REQUIRE],
85
        [\T_REQUIRE_ONCE],
86
        [\T_INCLUDE],
87
        [\T_INCLUDE_ONCE],
88
    ];
89

90
    private const CONFIG_OPTIONS = [
91
        'break',
92
        'clone',
93
        'continue',
94
        'echo_print',
95
        'negative_instanceof',
96
        'others',
97
        'return',
98
        'switch_case',
99
        'yield',
100
        'yield_from',
101
    ];
102

103
    private const TOKEN_TYPE_CONFIG_MAP = [
104
        \T_BREAK => 'break',
105
        \T_CASE => 'switch_case',
106
        \T_CONTINUE => 'continue',
107
        \T_ECHO => 'echo_print',
108
        \T_PRINT => 'echo_print',
109
        \T_RETURN => 'return',
110
        \T_YIELD => 'yield',
111
        \T_YIELD_FROM => 'yield_from',
112
    ];
113

114
    // handled by the `include` rule
115
    private const TOKEN_TYPE_NO_CONFIG = [
116
        \T_REQUIRE,
117
        \T_REQUIRE_ONCE,
118
        \T_INCLUDE,
119
        \T_INCLUDE_ONCE,
120
    ];
121
    private const KNOWN_NEGATIVE_PRE_TYPES = [
122
        [CT::T_CLASS_CONSTANT],
123
        [CT::T_DYNAMIC_VAR_BRACE_CLOSE],
124
        [CT::T_RETURN_REF],
125
        [CT::T_USE_LAMBDA],
126
        [\T_ARRAY],
127
        [\T_CATCH],
128
        [\T_CLASS],
129
        [\T_DECLARE],
130
        [\T_ELSEIF],
131
        [\T_EMPTY],
132
        [\T_EXIT],
133
        [\T_EVAL],
134
        [\T_FN],
135
        [\T_FOREACH],
136
        [\T_FOR],
137
        [\T_FUNCTION],
138
        [\T_HALT_COMPILER],
139
        [\T_IF],
140
        [\T_ISSET],
141
        [\T_LIST],
142
        [\T_STRING],
143
        [\T_SWITCH],
144
        [\T_STATIC],
145
        [\T_UNSET],
146
        [\T_VARIABLE],
147
        [\T_WHILE],
148
        // handled by the `include` rule
149
        [\T_REQUIRE],
150
        [\T_REQUIRE_ONCE],
151
        [\T_INCLUDE],
152
        [\T_INCLUDE_ONCE],
153
        [FCT::T_MATCH],
154
    ];
155

156
    /**
157
     * @var list<_PhpTokenPrototypePartial>
158
     */
159
    private array $noopTypes;
160

161
    private TokensAnalyzer $tokensAnalyzer;
162

163
    public function __construct()
164
    {
165
        parent::__construct();
195✔
166

167
        $this->noopTypes = [
195✔
168
            '$',
195✔
169
            [\T_CONSTANT_ENCAPSED_STRING],
195✔
170
            [\T_DNUMBER],
195✔
171
            [\T_DOUBLE_COLON],
195✔
172
            [\T_LNUMBER],
195✔
173
            [\T_NS_SEPARATOR],
195✔
174
            [\T_STRING],
195✔
175
            [\T_VARIABLE],
195✔
176
            [\T_STATIC],
195✔
177
            // magic constants
178
            [\T_CLASS_C],
195✔
179
            [\T_DIR],
195✔
180
            [\T_FILE],
195✔
181
            [\T_FUNC_C],
195✔
182
            [\T_LINE],
195✔
183
            [\T_METHOD_C],
195✔
184
            [\T_NS_C],
195✔
185
            [\T_TRAIT_C],
195✔
186
        ];
195✔
187

188
        foreach (Token::getObjectOperatorKinds() as $kind) {
195✔
189
            $this->noopTypes[] = [$kind];
195✔
190
        }
191
    }
192

193
    public function getDefinition(): FixerDefinitionInterface
194
    {
195
        return new FixerDefinition(
3✔
196
            'Removes unneeded parentheses around control statements.',
3✔
197
            [
3✔
198
                new CodeSample(
3✔
199
                    <<<'PHP'
3✔
200
                        <?php
201
                        while ($x) { while ($y) { break (2); } }
202
                        clone($a);
203
                        while ($y) { continue (2); }
204
                        echo("foo");
205
                        print("foo");
206
                        return (1 + 2);
207
                        switch ($a) { case($x); }
208
                        yield(2);
209

210
                        PHP
3✔
211
                ),
3✔
212
                new CodeSample(
3✔
213
                    <<<'PHP'
3✔
214
                        <?php
215
                        while ($x) { while ($y) { break (2); } }
216

217
                        clone($a);
218

219
                        while ($y) { continue (2); }
220

221
                        PHP,
3✔
222
                    ['statements' => ['break', 'continue']]
3✔
223
                ),
3✔
224
            ]
3✔
225
        );
3✔
226
    }
227

228
    /**
229
     * {@inheritdoc}
230
     *
231
     * Must run before ConcatSpaceFixer, NewExpressionParenthesesFixer, NoTrailingWhitespaceFixer.
232
     * Must run after ModernizeTypesCastingFixer, NoAlternativeSyntaxFixer.
233
     */
234
    public function getPriority(): int
235
    {
236
        return 30;
1✔
237
    }
238

239
    public function isCandidate(Tokens $tokens): bool
240
    {
241
        return $tokens->isAnyTokenKindsFound(['(', CT::T_BRACE_CLASS_INSTANTIATION_OPEN]);
186✔
242
    }
243

244
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
245
    {
246
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
186✔
247

248
        foreach ($tokens as $openIndex => $token) {
186✔
249
            if ($token->equals('(')) {
186✔
250
                $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
186✔
251
            } elseif ($token->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_OPEN)) {
186✔
252
                $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_BRACE_CLASS_INSTANTIATION, $openIndex);
9✔
253
            } else {
254
                continue;
186✔
255
            }
256

257
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($openIndex);
186✔
258
            $afterCloseIndex = $tokens->getNextMeaningfulToken($closeIndex);
186✔
259

260
            // do a cheap check for negative case: `X()`
261

262
            if ($tokens->getNextMeaningfulToken($openIndex) === $closeIndex) {
186✔
263
                if ($tokens[$beforeOpenIndex]->isGivenKind(\T_EXIT)) {
65✔
264
                    $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'others');
1✔
265
                }
266

267
                continue;
65✔
268
            }
269

270
            // do a cheap check for negative case: `foo(1,2)`
271

272
            if ($tokens[$beforeOpenIndex]->equalsAny(self::KNOWN_NEGATIVE_PRE_TYPES)) {
184✔
273
                continue;
58✔
274
            }
275

276
            // check for the simple useless wrapped cases
277

278
            if ($this->isUselessWrapped($tokens, $beforeOpenIndex, $afterCloseIndex)) {
183✔
279
                $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, $this->getConfigType($tokens, $beforeOpenIndex));
100✔
280

281
                continue;
100✔
282
            }
283

284
            // handle `clone` statements
285

286
            if ($tokens[$beforeOpenIndex]->isGivenKind(\T_CLONE)) {
101✔
287
                if ($this->isWrappedCloneArgument($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
10✔
288
                    $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'clone');
7✔
289
                }
290

291
                continue;
10✔
292
            }
293

294
            // handle `instance of` statements
295

296
            $instanceOfIndex = $this->getIndexOfInstanceOfStatement($tokens, $openIndex, $closeIndex);
92✔
297

298
            if (null !== $instanceOfIndex) {
92✔
299
                if ($this->isWrappedInstanceOf($tokens, $instanceOfIndex, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
6✔
300
                    $this->removeUselessParenthesisPair(
5✔
301
                        $tokens,
5✔
302
                        $beforeOpenIndex,
5✔
303
                        $afterCloseIndex,
5✔
304
                        $openIndex,
5✔
305
                        $closeIndex,
5✔
306
                        $tokens[$beforeOpenIndex]->equals('!') ? 'negative_instanceof' : 'others'
5✔
307
                    );
5✔
308
                }
309

310
                continue;
6✔
311
            }
312

313
            // last checks deal with operators, do not swap around
314

315
            if ($this->isWrappedPartOfOperation($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
87✔
316
                $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, $this->getConfigType($tokens, $beforeOpenIndex));
71✔
317
            }
318
        }
319
    }
320

321
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
322
    {
323
        $defaults = array_filter(
195✔
324
            self::CONFIG_OPTIONS,
195✔
325
            static fn (string $option): bool => 'negative_instanceof' !== $option && 'others' !== $option && 'yield_from' !== $option
195✔
326
        );
195✔
327

328
        return new FixerConfigurationResolver([
195✔
329
            (new FixerOptionBuilder('statements', 'List of control statements to fix.'))
195✔
330
                ->setAllowedTypes(['string[]'])
195✔
331
                ->setAllowedValues([new AllowedValueSubset(self::CONFIG_OPTIONS)])
195✔
332
                ->setDefault(array_values($defaults))
195✔
333
                ->getOption(),
195✔
334
        ]);
195✔
335
    }
336

337
    private function isUselessWrapped(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
338
    {
339
        return
183✔
340
            $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
183✔
341
            || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
183✔
342
            || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
183✔
343
            || $this->isWrappedLanguageConstructArgument($tokens, $beforeOpenIndex, $afterCloseIndex)
183✔
344
            || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
183✔
345
    }
346

347
    private function isWrappedCloneArgument(Tokens $tokens, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
348
    {
349
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
10✔
350

351
        if (
352
            !(
353
                $tokens[$beforeOpenIndex]->equals('?') // For BC reasons
10✔
354
                || $this->isSimpleAssignment($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
355
                || $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
356
                || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
357
                || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
358
                || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
359
            )
360
        ) {
361
            return false;
1✔
362
        }
363

364
        $newCandidateIndex = $tokens->getNextMeaningfulToken($openIndex);
9✔
365

366
        if ($tokens[$newCandidateIndex]->isGivenKind(\T_NEW)) {
9✔
367
            $openIndex = $newCandidateIndex; // `clone (new X)`, `clone (new X())`, clone (new X(Y))`
2✔
368
        }
369

370
        return !$this->containsOperation($tokens, $openIndex, $closeIndex);
9✔
371
    }
372

373
    private function getIndexOfInstanceOfStatement(Tokens $tokens, int $openIndex, int $closeIndex): ?int
374
    {
375
        $instanceOfIndex = $tokens->findGivenKind(\T_INSTANCEOF, $openIndex, $closeIndex);
92✔
376

377
        return 1 === \count($instanceOfIndex) ? array_key_first($instanceOfIndex) : null;
92✔
378
    }
379

380
    private function isWrappedInstanceOf(Tokens $tokens, int $instanceOfIndex, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
381
    {
382
        if (
383
            $this->containsOperation($tokens, $openIndex, $instanceOfIndex)
6✔
384
            || $this->containsOperation($tokens, $instanceOfIndex, $closeIndex)
6✔
385
        ) {
UNCOV
386
            return false;
×
387
        }
388

389
        if ($tokens[$beforeOpenIndex]->equals('!')) {
6✔
390
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
391
        }
392

393
        return
6✔
394
            $this->isSimpleAssignment($tokens, $beforeOpenIndex, $afterCloseIndex)
6✔
395
            || $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
6✔
396
            || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
6✔
397
            || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
6✔
398
            || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
6✔
399
    }
400

401
    private function isWrappedPartOfOperation(Tokens $tokens, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
402
    {
403
        if ($this->containsOperation($tokens, $openIndex, $closeIndex)) {
87✔
404
            return false;
22✔
405
        }
406

407
        $boundariesMoved = false;
74✔
408

409
        if ($this->isPreUnaryOperation($tokens, $beforeOpenIndex)) {
74✔
410
            $beforeOpenIndex = $this->getBeforePreUnaryOperation($tokens, $beforeOpenIndex);
15✔
411
            $boundariesMoved = true;
15✔
412
        }
413

414
        if ($this->isAccess($tokens, $afterCloseIndex)) {
74✔
415
            $afterCloseIndex = $this->getAfterAccess($tokens, $afterCloseIndex);
10✔
416
            $boundariesMoved = true;
10✔
417

418
            if ($this->tokensAnalyzer->isUnarySuccessorOperator($afterCloseIndex)) { // post unary operation are only valid here
10✔
419
                $afterCloseIndex = $tokens->getNextMeaningfulToken($afterCloseIndex);
2✔
420
            }
421
        }
422

423
        if ($boundariesMoved) {
74✔
424
            if ($tokens[$beforeOpenIndex]->equalsAny(self::KNOWN_NEGATIVE_PRE_TYPES)) {
21✔
UNCOV
425
                return false;
×
426
            }
427

428
            if ($this->isUselessWrapped($tokens, $beforeOpenIndex, $afterCloseIndex)) {
21✔
429
                return true;
13✔
430
            }
431
        }
432

433
        // check if part of some operation sequence
434

435
        $beforeIsBinaryOperation = $this->tokensAnalyzer->isBinaryOperator($beforeOpenIndex);
63✔
436
        $afterIsBinaryOperation = $this->tokensAnalyzer->isBinaryOperator($afterCloseIndex);
63✔
437

438
        if ($beforeIsBinaryOperation && $afterIsBinaryOperation) {
63✔
439
            return true; // `+ (x) +`
19✔
440
        }
441

442
        $beforeToken = $tokens[$beforeOpenIndex];
48✔
443
        $afterToken = $tokens[$afterCloseIndex];
48✔
444

445
        $beforeIsBlockOpenOrComma = $beforeToken->equals(',') || null !== $this->getBlock($tokens, $beforeOpenIndex, true);
48✔
446
        $afterIsBlockEndOrComma = $afterToken->equals(',') || null !== $this->getBlock($tokens, $afterCloseIndex, false);
48✔
447

448
        if (($beforeIsBlockOpenOrComma && $afterIsBinaryOperation) || ($beforeIsBinaryOperation && $afterIsBlockEndOrComma)) {
48✔
449
            // $beforeIsBlockOpenOrComma && $afterIsBlockEndOrComma is covered by `isWrappedSequenceElement`
450
            // `[ (x) +` or `+ (X) ]` or `, (X) +` or `+ (X) ,`
451

452
            return true;
5✔
453
        }
454

455
        if ($tokens[$beforeOpenIndex]->equals('}')) {
44✔
456
            $beforeIsStatementOpen = !$this->closeCurlyBelongsToDynamicElement($tokens, $beforeOpenIndex);
4✔
457
        } else {
458
            $beforeIsStatementOpen = $beforeToken->equalsAny(self::BEFORE_TYPES) || $beforeToken->isGivenKind(\T_CASE);
42✔
459
        }
460

461
        $afterIsStatementEnd = $afterToken->equalsAny([';', [\T_CLOSE_TAG]]);
44✔
462

463
        return
44✔
464
            ($beforeIsStatementOpen && $afterIsBinaryOperation) // `<?php (X) +`
44✔
465
            || ($beforeIsBinaryOperation && $afterIsStatementEnd); // `+ (X);`
44✔
466
    }
467

468
    // bounded `print|yield|yield from|require|require_once|include|include_once (X)`
469
    private function isWrappedLanguageConstructArgument(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
470
    {
471
        if (!$tokens[$beforeOpenIndex]->isGivenKind([\T_PRINT, \T_YIELD, \T_YIELD_FROM, \T_REQUIRE, \T_REQUIRE_ONCE, \T_INCLUDE, \T_INCLUDE_ONCE])) {
119✔
472
            return false;
114✔
473
        }
474

475
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
10✔
476

477
        return $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
10✔
478
    }
479

480
    // any of `<?php|<?|<?=|;|throw|return|... (X) ;|T_CLOSE`
481
    private function isSingleStatement(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
482
    {
483
        if ($tokens[$beforeOpenIndex]->isGivenKind(\T_CASE)) {
183✔
484
            return $tokens[$afterCloseIndex]->equalsAny([':', ';']); // `switch case`
9✔
485
        }
486

487
        if (!$tokens[$afterCloseIndex]->equalsAny([';', [\T_CLOSE_TAG]])) {
177✔
488
            return false;
82✔
489
        }
490

491
        if ($tokens[$beforeOpenIndex]->equals('}')) {
120✔
492
            return !$this->closeCurlyBelongsToDynamicElement($tokens, $beforeOpenIndex);
6✔
493
        }
494

495
        return $tokens[$beforeOpenIndex]->equalsAny(self::BEFORE_TYPES);
117✔
496
    }
497

498
    private function isSimpleAssignment(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
499
    {
500
        return $tokens[$beforeOpenIndex]->equals('=') && $tokens[$afterCloseIndex]->equalsAny([';', [\T_CLOSE_TAG]]); // `= (X) ;`
15✔
501
    }
502

503
    private function isWrappedSequenceElement(Tokens $tokens, int $startIndex, int $endIndex): bool
504
    {
505
        $startIsComma = $tokens[$startIndex]->equals(',');
119✔
506
        $endIsComma = $tokens[$endIndex]->equals(',');
119✔
507

508
        if ($startIsComma && $endIsComma) {
119✔
509
            return true; // `,(X),`
8✔
510
        }
511

512
        $blockTypeStart = $this->getBlock($tokens, $startIndex, true);
118✔
513
        $blockTypeEnd = $this->getBlock($tokens, $endIndex, false);
118✔
514

515
        return
118✔
516
            ($startIsComma && null !== $blockTypeEnd) // `,(X)]`
118✔
517
            || ($endIsComma && null !== $blockTypeStart) // `[(X),`
118✔
518
            || (null !== $blockTypeEnd && null !== $blockTypeStart); // any type of `{(X)}`, `[(X)]` and `((X))`
118✔
519
    }
520

521
    // any of `for( (X); ;(X)) ;` note that the middle element is covered as 'single statement' as it is `; (X) ;`
522
    private function isWrappedForElement(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
523
    {
524
        $forCandidateIndex = null;
119✔
525

526
        if ($tokens[$beforeOpenIndex]->equals('(') && $tokens[$afterCloseIndex]->equals(';')) {
119✔
527
            $forCandidateIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
5✔
528
        } elseif ($tokens[$afterCloseIndex]->equals(')') && $tokens[$beforeOpenIndex]->equals(';')) {
119✔
529
            $forCandidateIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $afterCloseIndex);
1✔
530
            $forCandidateIndex = $tokens->getPrevMeaningfulToken($forCandidateIndex);
1✔
531
        }
532

533
        return null !== $forCandidateIndex && $tokens[$forCandidateIndex]->isGivenKind(\T_FOR);
119✔
534
    }
535

536
    // `fn() => (X);`
537
    private function isWrappedFnBody(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
538
    {
539
        if (!$tokens[$beforeOpenIndex]->isGivenKind(\T_DOUBLE_ARROW)) {
125✔
540
            return false;
118✔
541
        }
542

543
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
12✔
544

545
        if ($tokens[$beforeOpenIndex]->isGivenKind(\T_STRING)) {
12✔
546
            while (true) {
4✔
547
                $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
548

549
                if (!$tokens[$beforeOpenIndex]->isGivenKind([\T_STRING, CT::T_TYPE_INTERSECTION, CT::T_TYPE_ALTERNATION])) {
4✔
550
                    break;
4✔
551
                }
552
            }
553

554
            if (!$tokens[$beforeOpenIndex]->isGivenKind(CT::T_TYPE_COLON)) {
4✔
UNCOV
555
                return false;
×
556
            }
557

558
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
559
        }
560

561
        if (!$tokens[$beforeOpenIndex]->equals(')')) {
12✔
UNCOV
562
            return false;
×
563
        }
564

565
        $beforeOpenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $beforeOpenIndex);
12✔
566
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
12✔
567

568
        if ($tokens[$beforeOpenIndex]->isGivenKind(CT::T_RETURN_REF)) {
12✔
569
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
2✔
570
        }
571

572
        if (!$tokens[$beforeOpenIndex]->isGivenKind(\T_FN)) {
12✔
UNCOV
573
            return false;
×
574
        }
575

576
        return $tokens[$afterCloseIndex]->equalsAny([';', ',', [\T_CLOSE_TAG]]);
12✔
577
    }
578

579
    private function isPreUnaryOperation(Tokens $tokens, int $index): bool
580
    {
581
        return $this->tokensAnalyzer->isUnaryPredecessorOperator($index) || $tokens[$index]->isCast();
166✔
582
    }
583

584
    private function getBeforePreUnaryOperation(Tokens $tokens, int $index): int
585
    {
586
        do {
587
            $index = $tokens->getPrevMeaningfulToken($index);
15✔
588
        } while ($this->isPreUnaryOperation($tokens, $index));
15✔
589

590
        return $index;
15✔
591
    }
592

593
    // array access `(X)[` or `(X){` or object access `(X)->` or `(X)?->`
594
    private function isAccess(Tokens $tokens, int $index): bool
595
    {
596
        $token = $tokens[$index];
166✔
597

598
        return $token->isObjectOperator() || $token->equals('[') || $token->isGivenKind(CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN);
166✔
599
    }
600

601
    private function getAfterAccess(Tokens $tokens, int $index): int
602
    {
603
        while (true) {
10✔
604
            $block = $this->getBlock($tokens, $index, true);
10✔
605

606
            if (null !== $block) {
10✔
607
                $index = $tokens->findBlockEnd($block['type'], $index);
7✔
608
                $index = $tokens->getNextMeaningfulToken($index);
7✔
609

610
                continue;
7✔
611
            }
612

613
            if (
614
                $tokens[$index]->isObjectOperator()
10✔
615
                || $tokens[$index]->equalsAny(['$', [\T_PAAMAYIM_NEKUDOTAYIM], [\T_STRING], [\T_VARIABLE]])
10✔
616
            ) {
617
                $index = $tokens->getNextMeaningfulToken($index);
6✔
618

619
                continue;
6✔
620
            }
621

622
            break;
10✔
623
        }
624

625
        return $index;
10✔
626
    }
627

628
    /**
629
     * @return null|array{type: Tokens::BLOCK_TYPE_*, isStart: bool}
630
     */
631
    private function getBlock(Tokens $tokens, int $index, bool $isStart): ?array
632
    {
633
        $block = Tokens::detectBlockType($tokens[$index]);
174✔
634

635
        return null !== $block && $isStart === $block['isStart'] && \in_array($block['type'], self::BLOCK_TYPES, true) ? $block : null;
174✔
636
    }
637

638
    private function containsOperation(Tokens $tokens, int $startIndex, int $endIndex): bool
639
    {
640
        while (true) {
101✔
641
            $startIndex = $tokens->getNextMeaningfulToken($startIndex);
101✔
642

643
            if ($startIndex === $endIndex) {
101✔
644
                break;
86✔
645
            }
646

647
            $block = Tokens::detectBlockType($tokens[$startIndex]);
101✔
648

649
            if (null !== $block && $block['isStart']) {
101✔
650
                $startIndex = $tokens->findBlockEnd($block['type'], $startIndex);
20✔
651

652
                continue;
20✔
653
            }
654

655
            if (!$tokens[$startIndex]->equalsAny($this->noopTypes)) {
101✔
656
                return true;
24✔
657
            }
658
        }
659

660
        return false;
86✔
661
    }
662

663
    private function getConfigType(Tokens $tokens, int $beforeOpenIndex): ?string
664
    {
665
        if ($tokens[$beforeOpenIndex]->isGivenKind(self::TOKEN_TYPE_NO_CONFIG)) {
162✔
UNCOV
666
            return null;
×
667
        }
668

669
        foreach (self::TOKEN_TYPE_CONFIG_MAP as $type => $configItem) {
162✔
670
            if ($tokens[$beforeOpenIndex]->isGivenKind($type)) {
162✔
671
                return $configItem;
65✔
672
            }
673
        }
674

675
        return 'others';
102✔
676
    }
677

678
    private function removeUselessParenthesisPair(
679
        Tokens $tokens,
680
        int $beforeOpenIndex,
681
        int $afterCloseIndex,
682
        int $openIndex,
683
        int $closeIndex,
684
        ?string $configType
685
    ): void {
686
        $statements = $this->configuration['statements'];
173✔
687

688
        if (null === $configType || !\in_array($configType, $statements, true)) {
173✔
689
            return;
11✔
690
        }
691

692
        $needsSpaceAfter = !$this->isAccess($tokens, $afterCloseIndex)
163✔
693
            && !$tokens[$afterCloseIndex]->equalsAny([';', ',', [\T_CLOSE_TAG]])
163✔
694
            && null === $this->getBlock($tokens, $afterCloseIndex, false)
163✔
695
            && !($tokens[$afterCloseIndex]->equalsAny([':', ';']) && $tokens[$beforeOpenIndex]->isGivenKind(\T_CASE));
163✔
696

697
        $needsSpaceBefore = !$this->isPreUnaryOperation($tokens, $beforeOpenIndex)
163✔
698
            && !$tokens[$beforeOpenIndex]->equalsAny(['}', [\T_EXIT], [\T_OPEN_TAG]])
163✔
699
            && null === $this->getBlock($tokens, $beforeOpenIndex, true);
163✔
700

701
        $this->removeBrace($tokens, $closeIndex, $needsSpaceAfter);
163✔
702
        $this->removeBrace($tokens, $openIndex, $needsSpaceBefore);
163✔
703
    }
704

705
    private function removeBrace(Tokens $tokens, int $index, bool $needsSpace): void
706
    {
707
        if ($needsSpace) {
163✔
708
            foreach ([-1, 1] as $direction) {
136✔
709
                $siblingIndex = $tokens->getNonEmptySibling($index, $direction);
136✔
710

711
                if ($tokens[$siblingIndex]->isWhitespace() || $tokens[$siblingIndex]->isComment()) {
136✔
712
                    $needsSpace = false;
109✔
713

714
                    break;
109✔
715
                }
716
            }
717
        }
718

719
        if ($needsSpace) {
163✔
720
            $tokens[$index] = new Token([\T_WHITESPACE, ' ']);
37✔
721
        } else {
722
            $tokens->clearTokenAndMergeSurroundingWhitespace($index);
162✔
723
        }
724
    }
725

726
    private function closeCurlyBelongsToDynamicElement(Tokens $tokens, int $beforeOpenIndex): bool
727
    {
728
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $beforeOpenIndex);
10✔
729
        $index = $tokens->getPrevMeaningfulToken($index);
10✔
730

731
        if ($tokens[$index]->isGivenKind(\T_DOUBLE_COLON)) {
10✔
732
            return true;
3✔
733
        }
734

735
        if ($tokens[$index]->equals(':')) {
7✔
736
            $index = $tokens->getPrevTokenOfKind($index, [[\T_CASE], '?']);
1✔
737

738
            return !$tokens[$index]->isGivenKind(\T_CASE);
1✔
739
        }
740

741
        return false;
7✔
742
    }
743
}
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