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

keradus / PHP-CS-Fixer / 16018263876

02 Jul 2025 06:58AM UTC coverage: 94.846% (-0.002%) from 94.848%
16018263876

push

github

keradus
debug2

28193 of 29725 relevant lines covered (94.85%)

45.34 hits per line

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

97.74
/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
 * @author Sullivan Senechal <soullivaneuh@gmail.com>
35
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
36
 * @author Gregor Harlan <gharlan@web.de>
37
 *
38
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
39
 *
40
 * @phpstan-type _AutogeneratedInputConfiguration array{
41
 *  statements?: list<'break'|'clone'|'continue'|'echo_print'|'negative_instanceof'|'others'|'return'|'switch_case'|'yield'|'yield_from'>,
42
 * }
43
 * @phpstan-type _AutogeneratedComputedConfiguration array{
44
 *  statements: list<'break'|'clone'|'continue'|'echo_print'|'negative_instanceof'|'others'|'return'|'switch_case'|'yield'|'yield_from'>,
45
 * }
46
 */
47
final class NoUnneededControlParenthesesFixer extends AbstractFixer implements ConfigurableFixerInterface
48
{
49
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
50
    use ConfigurableFixerTrait;
51

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

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

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

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

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

152
    /**
153
     * @var list<array{int}|string>
154
     */
155
    private array $noopTypes;
156

157
    private TokensAnalyzer $tokensAnalyzer;
158

159
    public function __construct()
160
    {
161
        parent::__construct();
195✔
162

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

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

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

210
clone($a);
211

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

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

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

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

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

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

252
            // do a cheap check for negative case: `X()`
253

254
            if ($tokens->getNextMeaningfulToken($openIndex) === $closeIndex) {
186✔
255
                if ($this->isExitStatement($tokens, $beforeOpenIndex)) {
65✔
256
                    $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'others');
1✔
257
                }
258

259
                continue;
65✔
260
            }
261

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

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

268
            // check for the simple useless wrapped cases
269

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

273
                continue;
100✔
274
            }
275

276
            // handle `clone` statements
277

278
            if ($this->isCloneStatement($tokens, $beforeOpenIndex)) {
101✔
279
                if ($this->isWrappedCloneArgument($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
10✔
280
                    $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'clone');
7✔
281
                }
282

283
                continue;
10✔
284
            }
285

286
            // handle `instance of` statements
287

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

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

302
                continue;
6✔
303
            }
304

305
            // last checks deal with operators, do not swap around
306

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

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

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

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

339
    private function isExitStatement(Tokens $tokens, int $beforeOpenIndex): bool
340
    {
341
        return $tokens[$beforeOpenIndex]->isGivenKind(T_EXIT);
65✔
342
    }
343

344
    private function isCloneStatement(Tokens $tokens, int $beforeOpenIndex): bool
345
    {
346
        return $tokens[$beforeOpenIndex]->isGivenKind(T_CLONE);
101✔
347
    }
348

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

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

366
        $newCandidateIndex = $tokens->getNextMeaningfulToken($openIndex);
9✔
367

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

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

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

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

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

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

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

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

409
        $boundariesMoved = false;
74✔
410

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

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

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

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

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

435
        // check if part of some operation sequence
436

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

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

444
        $beforeToken = $tokens[$beforeOpenIndex];
48✔
445
        $afterToken = $tokens[$afterCloseIndex];
48✔
446

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

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

454
            return true;
5✔
455
        }
456

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

463
        $afterIsStatementEnd = $afterToken->equalsAny([';', [T_CLOSE_TAG]]);
44✔
464

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

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

477
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
10✔
478

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

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

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

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

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

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

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

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

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

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

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

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

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

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

545
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
12✔
546

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

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

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

560
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
561
        }
562

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

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

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

574
        if (!$tokens[$beforeOpenIndex]->isGivenKind(T_FN)) {
12✔
575
            return false;
×
576
        }
577

578
        return $tokens[$afterCloseIndex]->equalsAny([';', ',', [T_CLOSE_TAG]]);
12✔
579
    }
580

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

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

592
        return $index;
15✔
593
    }
594

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

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

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

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

612
                continue;
7✔
613
            }
614

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

621
                continue;
6✔
622
            }
623

624
            break;
10✔
625
        }
626

627
        return $index;
10✔
628
    }
629

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

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

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

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

649
            $block = Tokens::detectBlockType($tokens[$startIndex]);
101✔
650

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

654
                continue;
20✔
655
            }
656

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

662
        return false;
86✔
663
    }
664

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

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

677
        return 'others';
102✔
678
    }
679

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

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

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

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

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

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

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

716
                    break;
109✔
717
                }
718
            }
719
        }
720

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

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

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

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

740
            return !$tokens[$index]->isGivenKind(T_CASE);
1✔
741
        }
742

743
        return false;
7✔
744
    }
745
}
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