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

keradus / PHP-CS-Fixer / 17193291398

24 Aug 2025 08:15PM UTC coverage: 94.745% (+0.005%) from 94.74%
17193291398

push

github

web-flow
chore: deprecate `Annotation::getTagsWithTypes` in favor of `TAGS_WITH_TYPES` constant (#8977)

4 of 4 new or added lines in 3 files covered. (100.0%)

108 existing lines in 8 files now uncovered.

28322 of 29893 relevant lines covered (94.74%)

45.84 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
final class NoUnneededControlParenthesesFixer extends AbstractFixer implements ConfigurableFixerInterface
50
{
51
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
52
    use ConfigurableFixerTrait;
53

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

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

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

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

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

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

159
    private TokensAnalyzer $tokensAnalyzer;
160

161
    public function __construct()
162
    {
163
        parent::__construct();
195✔
164

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

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

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

212
clone($a);
213

214
while ($y) { continue (2); }
215
',
3✔
216
                    ['statements' => ['break', 'continue']]
3✔
217
                ),
3✔
218
            ]
3✔
219
        );
3✔
220
    }
221

222
    /**
223
     * {@inheritdoc}
224
     *
225
     * Must run before ConcatSpaceFixer, NewExpressionParenthesesFixer, NoTrailingWhitespaceFixer.
226
     * Must run after ModernizeTypesCastingFixer, NoAlternativeSyntaxFixer.
227
     */
228
    public function getPriority(): int
229
    {
230
        return 30;
1✔
231
    }
232

233
    public function isCandidate(Tokens $tokens): bool
234
    {
235
        return $tokens->isAnyTokenKindsFound(['(', CT::T_BRACE_CLASS_INSTANTIATION_OPEN]);
186✔
236
    }
237

238
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
239
    {
240
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
186✔
241

242
        foreach ($tokens as $openIndex => $token) {
186✔
243
            if ($token->equals('(')) {
186✔
244
                $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
186✔
245
            } elseif ($token->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_OPEN)) {
186✔
246
                $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_BRACE_CLASS_INSTANTIATION, $openIndex);
9✔
247
            } else {
248
                continue;
186✔
249
            }
250

251
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($openIndex);
186✔
252
            $afterCloseIndex = $tokens->getNextMeaningfulToken($closeIndex);
186✔
253

254
            // do a cheap check for negative case: `X()`
255

256
            if ($tokens->getNextMeaningfulToken($openIndex) === $closeIndex) {
186✔
257
                if ($tokens[$beforeOpenIndex]->isGivenKind(\T_EXIT)) {
65✔
258
                    $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'others');
1✔
259
                }
260

261
                continue;
65✔
262
            }
263

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

266
            if ($tokens[$beforeOpenIndex]->equalsAny(self::KNOWN_NEGATIVE_PRE_TYPES)) {
184✔
267
                continue;
58✔
268
            }
269

270
            // check for the simple useless wrapped cases
271

272
            if ($this->isUselessWrapped($tokens, $beforeOpenIndex, $afterCloseIndex)) {
183✔
273
                $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, $this->getConfigType($tokens, $beforeOpenIndex));
100✔
274

275
                continue;
100✔
276
            }
277

278
            // handle `clone` statements
279

280
            if ($tokens[$beforeOpenIndex]->isGivenKind(\T_CLONE)) {
101✔
281
                if ($this->isWrappedCloneArgument($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
10✔
282
                    $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'clone');
7✔
283
                }
284

285
                continue;
10✔
286
            }
287

288
            // handle `instance of` statements
289

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

292
            if (null !== $instanceOfIndex) {
92✔
293
                if ($this->isWrappedInstanceOf($tokens, $instanceOfIndex, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
6✔
294
                    $this->removeUselessParenthesisPair(
5✔
295
                        $tokens,
5✔
296
                        $beforeOpenIndex,
5✔
297
                        $afterCloseIndex,
5✔
298
                        $openIndex,
5✔
299
                        $closeIndex,
5✔
300
                        $tokens[$beforeOpenIndex]->equals('!') ? 'negative_instanceof' : 'others'
5✔
301
                    );
5✔
302
                }
303

304
                continue;
6✔
305
            }
306

307
            // last checks deal with operators, do not swap around
308

309
            if ($this->isWrappedPartOfOperation($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
87✔
310
                $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, $this->getConfigType($tokens, $beforeOpenIndex));
71✔
311
            }
312
        }
313
    }
314

315
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
316
    {
317
        $defaults = array_filter(
195✔
318
            self::CONFIG_OPTIONS,
195✔
319
            static fn (string $option): bool => 'negative_instanceof' !== $option && 'others' !== $option && 'yield_from' !== $option
195✔
320
        );
195✔
321

322
        return new FixerConfigurationResolver([
195✔
323
            (new FixerOptionBuilder('statements', 'List of control statements to fix.'))
195✔
324
                ->setAllowedTypes(['string[]'])
195✔
325
                ->setAllowedValues([new AllowedValueSubset(self::CONFIG_OPTIONS)])
195✔
326
                ->setDefault(array_values($defaults))
195✔
327
                ->getOption(),
195✔
328
        ]);
195✔
329
    }
330

331
    private function isUselessWrapped(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
332
    {
333
        return
183✔
334
            $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
183✔
335
            || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
183✔
336
            || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
183✔
337
            || $this->isWrappedLanguageConstructArgument($tokens, $beforeOpenIndex, $afterCloseIndex)
183✔
338
            || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
183✔
339
    }
340

341
    private function isWrappedCloneArgument(Tokens $tokens, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
342
    {
343
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
10✔
344

345
        if (
346
            !(
347
                $tokens[$beforeOpenIndex]->equals('?') // For BC reasons
10✔
348
                || $this->isSimpleAssignment($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
349
                || $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
350
                || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
351
                || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
352
                || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex)
10✔
353
            )
354
        ) {
355
            return false;
1✔
356
        }
357

358
        $newCandidateIndex = $tokens->getNextMeaningfulToken($openIndex);
9✔
359

360
        if ($tokens[$newCandidateIndex]->isGivenKind(\T_NEW)) {
9✔
361
            $openIndex = $newCandidateIndex; // `clone (new X)`, `clone (new X())`, clone (new X(Y))`
2✔
362
        }
363

364
        return !$this->containsOperation($tokens, $openIndex, $closeIndex);
9✔
365
    }
366

367
    private function getIndexOfInstanceOfStatement(Tokens $tokens, int $openIndex, int $closeIndex): ?int
368
    {
369
        $instanceOfIndex = $tokens->findGivenKind(\T_INSTANCEOF, $openIndex, $closeIndex);
92✔
370

371
        return 1 === \count($instanceOfIndex) ? array_key_first($instanceOfIndex) : null;
92✔
372
    }
373

374
    private function isWrappedInstanceOf(Tokens $tokens, int $instanceOfIndex, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
375
    {
376
        if (
377
            $this->containsOperation($tokens, $openIndex, $instanceOfIndex)
6✔
378
            || $this->containsOperation($tokens, $instanceOfIndex, $closeIndex)
6✔
379
        ) {
UNCOV
380
            return false;
×
381
        }
382

383
        if ($tokens[$beforeOpenIndex]->equals('!')) {
6✔
384
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
385
        }
386

387
        return
6✔
388
            $this->isSimpleAssignment($tokens, $beforeOpenIndex, $afterCloseIndex)
6✔
389
            || $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
6✔
390
            || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
6✔
391
            || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
6✔
392
            || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
6✔
393
    }
394

395
    private function isWrappedPartOfOperation(Tokens $tokens, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
396
    {
397
        if ($this->containsOperation($tokens, $openIndex, $closeIndex)) {
87✔
398
            return false;
22✔
399
        }
400

401
        $boundariesMoved = false;
74✔
402

403
        if ($this->isPreUnaryOperation($tokens, $beforeOpenIndex)) {
74✔
404
            $beforeOpenIndex = $this->getBeforePreUnaryOperation($tokens, $beforeOpenIndex);
15✔
405
            $boundariesMoved = true;
15✔
406
        }
407

408
        if ($this->isAccess($tokens, $afterCloseIndex)) {
74✔
409
            $afterCloseIndex = $this->getAfterAccess($tokens, $afterCloseIndex);
10✔
410
            $boundariesMoved = true;
10✔
411

412
            if ($this->tokensAnalyzer->isUnarySuccessorOperator($afterCloseIndex)) { // post unary operation are only valid here
10✔
413
                $afterCloseIndex = $tokens->getNextMeaningfulToken($afterCloseIndex);
2✔
414
            }
415
        }
416

417
        if ($boundariesMoved) {
74✔
418
            if ($tokens[$beforeOpenIndex]->equalsAny(self::KNOWN_NEGATIVE_PRE_TYPES)) {
21✔
UNCOV
419
                return false;
×
420
            }
421

422
            if ($this->isUselessWrapped($tokens, $beforeOpenIndex, $afterCloseIndex)) {
21✔
423
                return true;
13✔
424
            }
425
        }
426

427
        // check if part of some operation sequence
428

429
        $beforeIsBinaryOperation = $this->tokensAnalyzer->isBinaryOperator($beforeOpenIndex);
63✔
430
        $afterIsBinaryOperation = $this->tokensAnalyzer->isBinaryOperator($afterCloseIndex);
63✔
431

432
        if ($beforeIsBinaryOperation && $afterIsBinaryOperation) {
63✔
433
            return true; // `+ (x) +`
19✔
434
        }
435

436
        $beforeToken = $tokens[$beforeOpenIndex];
48✔
437
        $afterToken = $tokens[$afterCloseIndex];
48✔
438

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

442
        if (($beforeIsBlockOpenOrComma && $afterIsBinaryOperation) || ($beforeIsBinaryOperation && $afterIsBlockEndOrComma)) {
48✔
443
            // $beforeIsBlockOpenOrComma && $afterIsBlockEndOrComma is covered by `isWrappedSequenceElement`
444
            // `[ (x) +` or `+ (X) ]` or `, (X) +` or `+ (X) ,`
445

446
            return true;
5✔
447
        }
448

449
        if ($tokens[$beforeOpenIndex]->equals('}')) {
44✔
450
            $beforeIsStatementOpen = !$this->closeCurlyBelongsToDynamicElement($tokens, $beforeOpenIndex);
4✔
451
        } else {
452
            $beforeIsStatementOpen = $beforeToken->equalsAny(self::BEFORE_TYPES) || $beforeToken->isGivenKind(\T_CASE);
42✔
453
        }
454

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

457
        return
44✔
458
            ($beforeIsStatementOpen && $afterIsBinaryOperation) // `<?php (X) +`
44✔
459
            || ($beforeIsBinaryOperation && $afterIsStatementEnd); // `+ (X);`
44✔
460
    }
461

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

469
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
10✔
470

471
        return $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
10✔
472
    }
473

474
    // any of `<?php|<?|<?=|;|throw|return|... (X) ;|T_CLOSE`
475
    private function isSingleStatement(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
476
    {
477
        if ($tokens[$beforeOpenIndex]->isGivenKind(\T_CASE)) {
183✔
478
            return $tokens[$afterCloseIndex]->equalsAny([':', ';']); // `switch case`
9✔
479
        }
480

481
        if (!$tokens[$afterCloseIndex]->equalsAny([';', [\T_CLOSE_TAG]])) {
177✔
482
            return false;
82✔
483
        }
484

485
        if ($tokens[$beforeOpenIndex]->equals('}')) {
120✔
486
            return !$this->closeCurlyBelongsToDynamicElement($tokens, $beforeOpenIndex);
6✔
487
        }
488

489
        return $tokens[$beforeOpenIndex]->equalsAny(self::BEFORE_TYPES);
117✔
490
    }
491

492
    private function isSimpleAssignment(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
493
    {
494
        return $tokens[$beforeOpenIndex]->equals('=') && $tokens[$afterCloseIndex]->equalsAny([';', [\T_CLOSE_TAG]]); // `= (X) ;`
15✔
495
    }
496

497
    private function isWrappedSequenceElement(Tokens $tokens, int $startIndex, int $endIndex): bool
498
    {
499
        $startIsComma = $tokens[$startIndex]->equals(',');
119✔
500
        $endIsComma = $tokens[$endIndex]->equals(',');
119✔
501

502
        if ($startIsComma && $endIsComma) {
119✔
503
            return true; // `,(X),`
8✔
504
        }
505

506
        $blockTypeStart = $this->getBlock($tokens, $startIndex, true);
118✔
507
        $blockTypeEnd = $this->getBlock($tokens, $endIndex, false);
118✔
508

509
        return
118✔
510
            ($startIsComma && null !== $blockTypeEnd) // `,(X)]`
118✔
511
            || ($endIsComma && null !== $blockTypeStart) // `[(X),`
118✔
512
            || (null !== $blockTypeEnd && null !== $blockTypeStart); // any type of `{(X)}`, `[(X)]` and `((X))`
118✔
513
    }
514

515
    // any of `for( (X); ;(X)) ;` note that the middle element is covered as 'single statement' as it is `; (X) ;`
516
    private function isWrappedForElement(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
517
    {
518
        $forCandidateIndex = null;
119✔
519

520
        if ($tokens[$beforeOpenIndex]->equals('(') && $tokens[$afterCloseIndex]->equals(';')) {
119✔
521
            $forCandidateIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
5✔
522
        } elseif ($tokens[$afterCloseIndex]->equals(')') && $tokens[$beforeOpenIndex]->equals(';')) {
119✔
523
            $forCandidateIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $afterCloseIndex);
1✔
524
            $forCandidateIndex = $tokens->getPrevMeaningfulToken($forCandidateIndex);
1✔
525
        }
526

527
        return null !== $forCandidateIndex && $tokens[$forCandidateIndex]->isGivenKind(\T_FOR);
119✔
528
    }
529

530
    // `fn() => (X);`
531
    private function isWrappedFnBody(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
532
    {
533
        if (!$tokens[$beforeOpenIndex]->isGivenKind(\T_DOUBLE_ARROW)) {
125✔
534
            return false;
118✔
535
        }
536

537
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
12✔
538

539
        if ($tokens[$beforeOpenIndex]->isGivenKind(\T_STRING)) {
12✔
540
            while (true) {
4✔
541
                $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
542

543
                if (!$tokens[$beforeOpenIndex]->isGivenKind([\T_STRING, CT::T_TYPE_INTERSECTION, CT::T_TYPE_ALTERNATION])) {
4✔
544
                    break;
4✔
545
                }
546
            }
547

548
            if (!$tokens[$beforeOpenIndex]->isGivenKind(CT::T_TYPE_COLON)) {
4✔
UNCOV
549
                return false;
×
550
            }
551

552
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
553
        }
554

555
        if (!$tokens[$beforeOpenIndex]->equals(')')) {
12✔
UNCOV
556
            return false;
×
557
        }
558

559
        $beforeOpenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $beforeOpenIndex);
12✔
560
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
12✔
561

562
        if ($tokens[$beforeOpenIndex]->isGivenKind(CT::T_RETURN_REF)) {
12✔
563
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
2✔
564
        }
565

566
        if (!$tokens[$beforeOpenIndex]->isGivenKind(\T_FN)) {
12✔
UNCOV
567
            return false;
×
568
        }
569

570
        return $tokens[$afterCloseIndex]->equalsAny([';', ',', [\T_CLOSE_TAG]]);
12✔
571
    }
572

573
    private function isPreUnaryOperation(Tokens $tokens, int $index): bool
574
    {
575
        return $this->tokensAnalyzer->isUnaryPredecessorOperator($index) || $tokens[$index]->isCast();
166✔
576
    }
577

578
    private function getBeforePreUnaryOperation(Tokens $tokens, int $index): int
579
    {
580
        do {
581
            $index = $tokens->getPrevMeaningfulToken($index);
15✔
582
        } while ($this->isPreUnaryOperation($tokens, $index));
15✔
583

584
        return $index;
15✔
585
    }
586

587
    // array access `(X)[` or `(X){` or object access `(X)->` or `(X)?->`
588
    private function isAccess(Tokens $tokens, int $index): bool
589
    {
590
        $token = $tokens[$index];
166✔
591

592
        return $token->isObjectOperator() || $token->equals('[') || $token->isGivenKind(CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN);
166✔
593
    }
594

595
    private function getAfterAccess(Tokens $tokens, int $index): int
596
    {
597
        while (true) {
10✔
598
            $block = $this->getBlock($tokens, $index, true);
10✔
599

600
            if (null !== $block) {
10✔
601
                $index = $tokens->findBlockEnd($block['type'], $index);
7✔
602
                $index = $tokens->getNextMeaningfulToken($index);
7✔
603

604
                continue;
7✔
605
            }
606

607
            if (
608
                $tokens[$index]->isObjectOperator()
10✔
609
                || $tokens[$index]->equalsAny(['$', [\T_PAAMAYIM_NEKUDOTAYIM], [\T_STRING], [\T_VARIABLE]])
10✔
610
            ) {
611
                $index = $tokens->getNextMeaningfulToken($index);
6✔
612

613
                continue;
6✔
614
            }
615

616
            break;
10✔
617
        }
618

619
        return $index;
10✔
620
    }
621

622
    /**
623
     * @return null|array{type: Tokens::BLOCK_TYPE_*, isStart: bool}
624
     */
625
    private function getBlock(Tokens $tokens, int $index, bool $isStart): ?array
626
    {
627
        $block = Tokens::detectBlockType($tokens[$index]);
174✔
628

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

632
    private function containsOperation(Tokens $tokens, int $startIndex, int $endIndex): bool
633
    {
634
        while (true) {
101✔
635
            $startIndex = $tokens->getNextMeaningfulToken($startIndex);
101✔
636

637
            if ($startIndex === $endIndex) {
101✔
638
                break;
86✔
639
            }
640

641
            $block = Tokens::detectBlockType($tokens[$startIndex]);
101✔
642

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

646
                continue;
20✔
647
            }
648

649
            if (!$tokens[$startIndex]->equalsAny($this->noopTypes)) {
101✔
650
                return true;
24✔
651
            }
652
        }
653

654
        return false;
86✔
655
    }
656

657
    private function getConfigType(Tokens $tokens, int $beforeOpenIndex): ?string
658
    {
659
        if ($tokens[$beforeOpenIndex]->isGivenKind(self::TOKEN_TYPE_NO_CONFIG)) {
162✔
UNCOV
660
            return null;
×
661
        }
662

663
        foreach (self::TOKEN_TYPE_CONFIG_MAP as $type => $configItem) {
162✔
664
            if ($tokens[$beforeOpenIndex]->isGivenKind($type)) {
162✔
665
                return $configItem;
65✔
666
            }
667
        }
668

669
        return 'others';
102✔
670
    }
671

672
    private function removeUselessParenthesisPair(
673
        Tokens $tokens,
674
        int $beforeOpenIndex,
675
        int $afterCloseIndex,
676
        int $openIndex,
677
        int $closeIndex,
678
        ?string $configType
679
    ): void {
680
        $statements = $this->configuration['statements'];
173✔
681

682
        if (null === $configType || !\in_array($configType, $statements, true)) {
173✔
683
            return;
11✔
684
        }
685

686
        $needsSpaceAfter = !$this->isAccess($tokens, $afterCloseIndex)
163✔
687
            && !$tokens[$afterCloseIndex]->equalsAny([';', ',', [\T_CLOSE_TAG]])
163✔
688
            && null === $this->getBlock($tokens, $afterCloseIndex, false)
163✔
689
            && !($tokens[$afterCloseIndex]->equalsAny([':', ';']) && $tokens[$beforeOpenIndex]->isGivenKind(\T_CASE));
163✔
690

691
        $needsSpaceBefore = !$this->isPreUnaryOperation($tokens, $beforeOpenIndex)
163✔
692
            && !$tokens[$beforeOpenIndex]->equalsAny(['}', [\T_EXIT], [\T_OPEN_TAG]])
163✔
693
            && null === $this->getBlock($tokens, $beforeOpenIndex, true);
163✔
694

695
        $this->removeBrace($tokens, $closeIndex, $needsSpaceAfter);
163✔
696
        $this->removeBrace($tokens, $openIndex, $needsSpaceBefore);
163✔
697
    }
698

699
    private function removeBrace(Tokens $tokens, int $index, bool $needsSpace): void
700
    {
701
        if ($needsSpace) {
163✔
702
            foreach ([-1, 1] as $direction) {
136✔
703
                $siblingIndex = $tokens->getNonEmptySibling($index, $direction);
136✔
704

705
                if ($tokens[$siblingIndex]->isWhitespace() || $tokens[$siblingIndex]->isComment()) {
136✔
706
                    $needsSpace = false;
109✔
707

708
                    break;
109✔
709
                }
710
            }
711
        }
712

713
        if ($needsSpace) {
163✔
714
            $tokens[$index] = new Token([\T_WHITESPACE, ' ']);
37✔
715
        } else {
716
            $tokens->clearTokenAndMergeSurroundingWhitespace($index);
162✔
717
        }
718
    }
719

720
    private function closeCurlyBelongsToDynamicElement(Tokens $tokens, int $beforeOpenIndex): bool
721
    {
722
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $beforeOpenIndex);
10✔
723
        $index = $tokens->getPrevMeaningfulToken($index);
10✔
724

725
        if ($tokens[$index]->isGivenKind(\T_DOUBLE_COLON)) {
10✔
726
            return true;
3✔
727
        }
728

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

732
            return !$tokens[$index]->isGivenKind(\T_CASE);
1✔
733
        }
734

735
        return false;
7✔
736
    }
737
}
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