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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 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
while ($x) { while ($y) { break (2); } }
201
clone($a);
202
while ($y) { continue (2); }
203
echo("foo");
204
print("foo");
205
return (1 + 2);
206
switch ($a) { case($x); }
207
yield(2);
208
'
3✔
209
                ),
3✔
210
                new CodeSample(
3✔
211
                    '<?php
3✔
212
while ($x) { while ($y) { break (2); } }
213

214
clone($a);
215

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

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

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

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

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

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

256
            // do a cheap check for negative case: `X()`
257

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

263
                continue;
65✔
264
            }
265

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

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

272
            // check for the simple useless wrapped cases
273

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

277
                continue;
100✔
278
            }
279

280
            // handle `clone` statements
281

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

287
                continue;
10✔
288
            }
289

290
            // handle `instance of` statements
291

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

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

306
                continue;
6✔
307
            }
308

309
            // last checks deal with operators, do not swap around
310

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

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

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

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

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

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

360
        $newCandidateIndex = $tokens->getNextMeaningfulToken($openIndex);
9✔
361

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

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

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

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

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

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

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

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

403
        $boundariesMoved = false;
74✔
404

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

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

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

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

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

429
        // check if part of some operation sequence
430

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

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

438
        $beforeToken = $tokens[$beforeOpenIndex];
48✔
439
        $afterToken = $tokens[$afterCloseIndex];
48✔
440

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

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

448
            return true;
5✔
449
        }
450

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

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

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

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

471
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
10✔
472

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

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

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

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

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

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

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

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

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

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

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

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

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

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

539
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
12✔
540

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

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

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

554
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
555
        }
556

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

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

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

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

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

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

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

586
        return $index;
15✔
587
    }
588

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

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

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

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

606
                continue;
7✔
607
            }
608

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

615
                continue;
6✔
616
            }
617

618
            break;
10✔
619
        }
620

621
        return $index;
10✔
622
    }
623

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

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

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

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

643
            $block = Tokens::detectBlockType($tokens[$startIndex]);
101✔
644

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

648
                continue;
20✔
649
            }
650

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

656
        return false;
86✔
657
    }
658

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

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

671
        return 'others';
102✔
672
    }
673

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

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

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

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

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

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

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

710
                    break;
109✔
711
                }
712
            }
713
        }
714

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

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

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

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

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

737
        return false;
7✔
738
    }
739
}
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