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

PHP-CS-Fixer / PHP-CS-Fixer / 3825623956

pending completion
3825623956

Pull #6734

github

GitHub
Merge f402171a0 into f5726f543
Pull Request #6734: bug: Fix type error when using paths intersection mode

1 of 1 new or added line in 1 file covered. (100.0%)

22556 of 24273 relevant lines covered (92.93%)

39.1 hits per line

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

86.25
/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\FixerConfiguration\AllowedValueSubset;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
22
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23
use PhpCsFixer\FixerDefinition\CodeSample;
24
use PhpCsFixer\FixerDefinition\FixerDefinition;
25
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
26
use PhpCsFixer\Tokenizer\CT;
27
use PhpCsFixer\Tokenizer\Token;
28
use PhpCsFixer\Tokenizer\Tokens;
29
use PhpCsFixer\Tokenizer\TokensAnalyzer;
30

31
/**
32
 * @author Sullivan Senechal <soullivaneuh@gmail.com>
33
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
34
 * @author Gregor Harlan <gharlan@web.de>
35
 */
36
final class NoUnneededControlParenthesesFixer extends AbstractFixer implements ConfigurableFixerInterface
37
{
38
    /**
39
     * @var int[]
40
     */
41
    private const BLOCK_TYPES = [
42
        Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE,
43
        Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE,
44
        Tokens::BLOCK_TYPE_CURLY_BRACE,
45
        Tokens::BLOCK_TYPE_DESTRUCTURING_SQUARE_BRACE,
46
        Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE,
47
        Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE,
48
        Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE,
49
        Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
50
    ];
51

52
    private const BEFORE_TYPES = [
53
        ';',
54
        '{',
55
        [T_OPEN_TAG],
56
        [T_OPEN_TAG_WITH_ECHO],
57
        [T_ECHO],
58
        [T_PRINT],
59
        [T_RETURN],
60
        [T_THROW],
61
        [T_YIELD],
62
        [T_YIELD_FROM],
63
        [T_BREAK],
64
        [T_CONTINUE],
65
        // won't be fixed, but true in concept, helpful for fast check
66
        [T_REQUIRE],
67
        [T_REQUIRE_ONCE],
68
        [T_INCLUDE],
69
        [T_INCLUDE_ONCE],
70
    ];
71

72
    private const NOOP_TYPES = [
73
        '$',
74
        [T_CONSTANT_ENCAPSED_STRING],
75
        [T_DNUMBER],
76
        [T_DOUBLE_COLON],
77
        [T_LNUMBER],
78
        [T_NS_SEPARATOR],
79
        [T_OBJECT_OPERATOR],
80
        [T_STRING],
81
        [T_VARIABLE],
82
        [T_STATIC],
83
        // magic constants
84
        [T_CLASS_C],
85
        [T_DIR],
86
        [T_FILE],
87
        [T_FUNC_C],
88
        [T_LINE],
89
        [T_METHOD_C],
90
        [T_NS_C],
91
        [T_TRAIT_C],
92
    ];
93

94
    private const CONFIG_OPTIONS = [
95
        'break',
96
        'clone',
97
        'continue',
98
        'echo_print',
99
        'negative_instanceof',
100
        'others',
101
        'return',
102
        'switch_case',
103
        'yield',
104
        'yield_from',
105
    ];
106

107
    private const TOKEN_TYPE_CONFIG_MAP = [
108
        T_BREAK => 'break',
109
        T_CASE => 'switch_case',
110
        T_CONTINUE => 'continue',
111
        T_ECHO => 'echo_print',
112
        T_PRINT => 'echo_print',
113
        T_RETURN => 'return',
114
        T_YIELD => 'yield',
115
        T_YIELD_FROM => 'yield_from',
116
    ];
117

118
    // handled by the `include` rule
119
    private const TOKEN_TYPE_NO_CONFIG = [
120
        T_REQUIRE,
121
        T_REQUIRE_ONCE,
122
        T_INCLUDE,
123
        T_INCLUDE_ONCE,
124
    ];
125

126
    private TokensAnalyzer $tokensAnalyzer;
127

128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function getDefinition(): FixerDefinitionInterface
132
    {
133
        return new FixerDefinition(
3✔
134
            'Removes unneeded parentheses around control statements.',
3✔
135
            [
3✔
136
                new CodeSample(
3✔
137
                    '<?php
3✔
138
while ($x) { while ($y) { break (2); } }
139
clone($a);
140
while ($y) { continue (2); }
141
echo("foo");
142
print("foo");
143
return (1 + 2);
144
switch ($a) { case($x); }
145
yield(2);
146
'
3✔
147
                ),
3✔
148
                new CodeSample(
3✔
149
                    '<?php
3✔
150
while ($x) { while ($y) { break (2); } }
151

152
clone($a);
153

154
while ($y) { continue (2); }
155
',
3✔
156
                    ['statements' => ['break', 'continue']]
3✔
157
                ),
3✔
158
            ]
3✔
159
        );
3✔
160
    }
161

162
    /**
163
     * {@inheritdoc}
164
     *
165
     * Must run before ConcatSpaceFixer, NoTrailingWhitespaceFixer.
166
     * Must run after ModernizeTypesCastingFixer, NoAlternativeSyntaxFixer.
167
     */
168
    public function getPriority(): int
169
    {
170
        return 30;
1✔
171
    }
172

173
    /**
174
     * {@inheritdoc}
175
     */
176
    public function isCandidate(Tokens $tokens): bool
177
    {
178
        return $tokens->isAnyTokenKindsFound(['(', CT::T_BRACE_CLASS_INSTANTIATION_OPEN]);
175✔
179
    }
180

181
    /**
182
     * {@inheritdoc}
183
     */
184
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
185
    {
186
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
175✔
187

188
        foreach ($tokens as $openIndex => $token) {
175✔
189
            if ($token->equals('(')) {
175✔
190
                $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
175✔
191
            } elseif ($token->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_OPEN)) {
175✔
192
                $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_BRACE_CLASS_INSTANTIATION, $openIndex);
9✔
193
            } else {
194
                continue;
175✔
195
            }
196

197
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($openIndex);
175✔
198
            $afterCloseIndex = $tokens->getNextMeaningfulToken($closeIndex);
175✔
199

200
            // do a cheap check for negative case: `X()`
201

202
            if ($tokens->getNextMeaningfulToken($openIndex) === $closeIndex) {
175✔
203
                if ($this->isExitStatement($tokens, $beforeOpenIndex)) {
61✔
204
                    $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'others');
1✔
205
                }
206

207
                continue;
61✔
208
            }
209

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

212
            if ($this->isKnownNegativePre($tokens[$beforeOpenIndex])) {
173✔
213
                continue;
54✔
214
            }
215

216
            // check for the simple useless wrapped cases
217

218
            if ($this->isUselessWrapped($tokens, $beforeOpenIndex, $afterCloseIndex)) {
172✔
219
                $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, $this->getConfigType($tokens, $beforeOpenIndex));
93✔
220

221
                continue;
93✔
222
            }
223

224
            // handle `clone` statements
225

226
            if ($this->isCloneStatement($tokens, $beforeOpenIndex)) {
98✔
227
                if ($this->isWrappedCloneArgument($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
9✔
228
                    $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'clone');
6✔
229
                }
230

231
                continue;
9✔
232
            }
233

234
            // handle `instance of` statements
235

236
            $instanceOfIndex = $this->getIndexOfInstanceOfStatement($tokens, $openIndex, $closeIndex);
90✔
237

238
            if (null !== $instanceOfIndex) {
90✔
239
                if ($this->isWrappedInstanceOf($tokens, $instanceOfIndex, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
5✔
240
                    $this->removeUselessParenthesisPair(
4✔
241
                        $tokens,
4✔
242
                        $beforeOpenIndex,
4✔
243
                        $afterCloseIndex,
4✔
244
                        $openIndex,
4✔
245
                        $closeIndex,
4✔
246
                        $tokens[$beforeOpenIndex]->equals('!') ? 'negative_instanceof' : 'others'
4✔
247
                    );
4✔
248
                }
249

250
                continue;
5✔
251
            }
252

253
            // last checks deal with operators, do not swap around
254

255
            if ($this->isWrappedPartOfOperation($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
86✔
256
                $this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, $this->getConfigType($tokens, $beforeOpenIndex));
69✔
257
            }
258
        }
259
    }
260

261
    /**
262
     * {@inheritdoc}
263
     */
264
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
265
    {
266
        $defaults = array_filter(
180✔
267
            self::CONFIG_OPTIONS,
180✔
268
            static function (string $option): bool {
180✔
269
                return 'negative_instanceof' !== $option && 'others' !== $option && 'yield_from' !== $option;
180✔
270
            }
180✔
271
        );
180✔
272

273
        return new FixerConfigurationResolver([
180✔
274
            (new FixerOptionBuilder('statements', 'List of control statements to fix.'))
180✔
275
                ->setAllowedTypes(['array'])
180✔
276
                ->setAllowedValues([new AllowedValueSubset(self::CONFIG_OPTIONS)])
180✔
277
                ->setDefault(array_values($defaults))
180✔
278
                ->getOption(),
180✔
279
        ]);
180✔
280
    }
281

282
    private function isUselessWrapped(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
283
    {
284
        return
172✔
285
            $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
172✔
286
            || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
172✔
287
            || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
172✔
288
            || $this->isWrappedLanguageConstructArgument($tokens, $beforeOpenIndex, $afterCloseIndex)
172✔
289
            || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex)
172✔
290
        ;
172✔
291
    }
292

293
    private function isExitStatement(Tokens $tokens, int $beforeOpenIndex): bool
294
    {
295
        return $tokens[$beforeOpenIndex]->isGivenKind(T_EXIT);
61✔
296
    }
297

298
    private function isCloneStatement(Tokens $tokens, int $beforeOpenIndex): bool
299
    {
300
        return $tokens[$beforeOpenIndex]->isGivenKind(T_CLONE);
98✔
301
    }
302

303
    private function isWrappedCloneArgument(Tokens $tokens, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
304
    {
305
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
9✔
306

307
        if (
308
            !(
309
                $tokens[$beforeOpenIndex]->equals('?') // For BC reasons
9✔
310
                || $this->isSimpleAssignment($tokens, $beforeOpenIndex, $afterCloseIndex)
9✔
311
                || $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
9✔
312
                || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
9✔
313
                || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
9✔
314
                || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex)
9✔
315
            )
316
        ) {
317
            return false;
1✔
318
        }
319

320
        $newCandidateIndex = $tokens->getNextMeaningfulToken($openIndex);
8✔
321

322
        if ($tokens[$newCandidateIndex]->isGivenKind(T_NEW)) {
8✔
323
            $openIndex = $newCandidateIndex; // `clone (new X)`, `clone (new X())`, clone (new X(Y))`
2✔
324
        }
325

326
        return !$this->containsOperation($tokens, $openIndex, $closeIndex);
8✔
327
    }
328

329
    private function getIndexOfInstanceOfStatement(Tokens $tokens, int $openIndex, int $closeIndex): ?int
330
    {
331
        $instanceOfIndex = $tokens->findGivenKind(T_INSTANCEOF, $openIndex, $closeIndex);
90✔
332

333
        return 1 === \count($instanceOfIndex) ? array_key_first($instanceOfIndex) : null;
90✔
334
    }
335

336
    private function isWrappedInstanceOf(Tokens $tokens, int $instanceOfIndex, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
337
    {
338
        if (
339
            $this->containsOperation($tokens, $openIndex, $instanceOfIndex)
5✔
340
            || $this->containsOperation($tokens, $instanceOfIndex, $closeIndex)
5✔
341
        ) {
342
            return false;
×
343
        }
344

345
        if ($tokens[$beforeOpenIndex]->equals('!')) {
5✔
346
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
3✔
347
        }
348

349
        return
5✔
350
            $this->isSimpleAssignment($tokens, $beforeOpenIndex, $afterCloseIndex)
5✔
351
            || $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
5✔
352
            || $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
5✔
353
            || $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
5✔
354
            || $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex)
5✔
355
        ;
5✔
356
    }
357

358
    private function isWrappedPartOfOperation(Tokens $tokens, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
359
    {
360
        if ($this->containsOperation($tokens, $openIndex, $closeIndex)) {
86✔
361
            return false;
22✔
362
        }
363

364
        $boundariesMoved = false;
73✔
365

366
        if ($this->isPreUnaryOperation($tokens, $beforeOpenIndex)) {
73✔
367
            $beforeOpenIndex = $this->getBeforePreUnaryOperation($tokens, $beforeOpenIndex);
14✔
368
            $boundariesMoved = true;
14✔
369
        }
370

371
        if ($this->isAccess($tokens, $afterCloseIndex)) {
73✔
372
            $afterCloseIndex = $this->getAfterAccess($tokens, $afterCloseIndex);
9✔
373
            $boundariesMoved = true;
9✔
374

375
            if ($this->tokensAnalyzer->isUnarySuccessorOperator($afterCloseIndex)) { // post unary operation are only valid here
9✔
376
                $afterCloseIndex = $tokens->getNextMeaningfulToken($afterCloseIndex);
2✔
377
            }
378
        }
379

380
        if ($boundariesMoved) {
73✔
381
            if ($this->isKnownNegativePre($tokens[$beforeOpenIndex])) {
20✔
382
                return false;
×
383
            }
384

385
            if ($this->isUselessWrapped($tokens, $beforeOpenIndex, $afterCloseIndex)) {
20✔
386
                return true;
12✔
387
            }
388
        }
389

390
        // check if part of some operation sequence
391

392
        $beforeIsBinaryOperation = $this->tokensAnalyzer->isBinaryOperator($beforeOpenIndex);
63✔
393
        $afterIsBinaryOperation = $this->tokensAnalyzer->isBinaryOperator($afterCloseIndex);
63✔
394

395
        if ($beforeIsBinaryOperation && $afterIsBinaryOperation) {
63✔
396
            return true; // `+ (x) +`
18✔
397
        }
398

399
        $beforeToken = $tokens[$beforeOpenIndex];
49✔
400
        $afterToken = $tokens[$afterCloseIndex];
49✔
401

402
        $beforeIsBlockOpenOrComma = $beforeToken->equals(',') || null !== $this->getBlock($tokens, $beforeOpenIndex, true);
49✔
403
        $afterIsBlockEndOrComma = $afterToken->equals(',') || null !== $this->getBlock($tokens, $afterCloseIndex, false);
49✔
404

405
        if (($beforeIsBlockOpenOrComma && $afterIsBinaryOperation) || ($beforeIsBinaryOperation && $afterIsBlockEndOrComma)) {
49✔
406
            // $beforeIsBlockOpenOrComma && $afterIsBlockEndOrComma is covered by `isWrappedSequenceElement`
407
            // `[ (x) +` or `+ (X) ]` or `, (X) +` or `+ (X) ,`
408

409
            return true;
5✔
410
        }
411

412
        if ($tokens[$beforeOpenIndex]->equals('}')) {
45✔
413
            $beforeIsStatementOpen = !$this->closeCurlyBelongsToDynamicElement($tokens, $beforeOpenIndex);
4✔
414
        } else {
415
            $beforeIsStatementOpen = $beforeToken->equalsAny(self::BEFORE_TYPES) || $beforeToken->isGivenKind(T_CASE);
43✔
416
        }
417

418
        $afterIsStatementEnd = $afterToken->equalsAny([';', [T_CLOSE_TAG]]);
45✔
419

420
        return
45✔
421
            ($beforeIsStatementOpen && $afterIsBinaryOperation) // `<?php (X) +`
45✔
422
            || ($beforeIsBinaryOperation && $afterIsStatementEnd) // `+ (X);`
45✔
423
        ;
45✔
424
    }
425

426
    // bounded `print|yield|yield from|require|require_once|include|include_once (X)`
427
    private function isWrappedLanguageConstructArgument(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
428
    {
429
        if (!$tokens[$beforeOpenIndex]->isGivenKind([T_PRINT, T_YIELD, T_YIELD_FROM, T_REQUIRE, T_REQUIRE_ONCE, T_INCLUDE, T_INCLUDE_ONCE])) {
115✔
430
            return false;
110✔
431
        }
432

433
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
10✔
434

435
        return $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
10✔
436
    }
437

438
    // any of `<?php|<?|<?=|;|throw|return|... (X) ;|T_CLOSE`
439
    private function isSingleStatement(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
440
    {
441
        if ($tokens[$beforeOpenIndex]->isGivenKind(T_CASE)) {
172✔
442
            return $tokens[$afterCloseIndex]->equalsAny([':', ';']); // `switch case`
8✔
443
        }
444

445
        if (!$tokens[$afterCloseIndex]->equalsAny([';', [T_CLOSE_TAG]])) {
167✔
446
            return false;
80✔
447
        }
448

449
        if ($tokens[$beforeOpenIndex]->equals('}')) {
111✔
450
            return !$this->closeCurlyBelongsToDynamicElement($tokens, $beforeOpenIndex);
6✔
451
        }
452

453
        return $tokens[$beforeOpenIndex]->equalsAny(self::BEFORE_TYPES);
108✔
454
    }
455

456
    private function isSimpleAssignment(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
457
    {
458
        return $tokens[$beforeOpenIndex]->equals('=') && $tokens[$afterCloseIndex]->equalsAny([';', [T_CLOSE_TAG]]); // `= (X) ;`
13✔
459
    }
460

461
    private function isWrappedSequenceElement(Tokens $tokens, int $startIndex, int $endIndex): bool
462
    {
463
        $startIsComma = $tokens[$startIndex]->equals(',');
115✔
464
        $endIsComma = $tokens[$endIndex]->equals(',');
115✔
465

466
        if ($startIsComma && $endIsComma) {
115✔
467
            return true; // `,(X),`
8✔
468
        }
469

470
        $blockTypeStart = $this->getBlock($tokens, $startIndex, true);
114✔
471
        $blockTypeEnd = $this->getBlock($tokens, $endIndex, false);
114✔
472

473
        return
114✔
474
            ($startIsComma && null !== $blockTypeEnd) // `,(X)]`
114✔
475
            || ($endIsComma && null !== $blockTypeStart) // `[(X),`
114✔
476
            || (null !== $blockTypeEnd && null !== $blockTypeStart) // any type of `{(X)}`, `[(X)]` and `((X))`
114✔
477
        ;
114✔
478
    }
479

480
    // any of `for( (X); ;(X)) ;` note that the middle element is covered as 'single statement' as it is `; (X) ;`
481
    private function isWrappedForElement(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
482
    {
483
        $forCandidateIndex = null;
115✔
484

485
        if ($tokens[$beforeOpenIndex]->equals('(') && $tokens[$afterCloseIndex]->equals(';')) {
115✔
486
            $forCandidateIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
5✔
487
        } elseif ($tokens[$afterCloseIndex]->equals(')') && $tokens[$beforeOpenIndex]->equals(';')) {
115✔
488
            $forCandidateIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $afterCloseIndex);
1✔
489
            $forCandidateIndex = $tokens->getPrevMeaningfulToken($forCandidateIndex);
1✔
490
        }
491

492
        return null !== $forCandidateIndex && $tokens[$forCandidateIndex]->isGivenKind(T_FOR);
115✔
493
    }
494

495
    // `fn() => (X);`
496
    private function isWrappedFnBody(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
497
    {
498
        if (!$tokens[$beforeOpenIndex]->isGivenKind(T_DOUBLE_ARROW)) {
121✔
499
            return false;
114✔
500
        }
501

502
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
12✔
503

504
        if ($tokens[$beforeOpenIndex]->isGivenKind(T_STRING)) {
12✔
505
            while (true) {
4✔
506
                $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
507

508
                if (!$tokens[$beforeOpenIndex]->isGivenKind([T_STRING, CT::T_TYPE_INTERSECTION, CT::T_TYPE_ALTERNATION])) {
4✔
509
                    break;
4✔
510
                }
511
            }
512

513
            if (!$tokens[$beforeOpenIndex]->isGivenKind(CT::T_TYPE_COLON)) {
4✔
514
                return false;
×
515
            }
516

517
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
4✔
518
        }
519

520
        if (!$tokens[$beforeOpenIndex]->equals(')')) {
12✔
521
            return false;
×
522
        }
523

524
        $beforeOpenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $beforeOpenIndex);
12✔
525
        $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
12✔
526

527
        if ($tokens[$beforeOpenIndex]->isGivenKind(CT::T_RETURN_REF)) {
12✔
528
            $beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
2✔
529
        }
530

531
        if (!$tokens[$beforeOpenIndex]->isGivenKind(T_FN)) {
12✔
532
            return false;
×
533
        }
534

535
        return $tokens[$afterCloseIndex]->equalsAny([';', ',', [T_CLOSE_TAG]]);
12✔
536
    }
537

538
    private function isPreUnaryOperation(Tokens $tokens, int $index): bool
539
    {
540
        return $this->tokensAnalyzer->isUnaryPredecessorOperator($index) || $tokens[$index]->isCast();
164✔
541
    }
542

543
    private function getBeforePreUnaryOperation(Tokens $tokens, int $index): int
544
    {
545
        do {
546
            $index = $tokens->getPrevMeaningfulToken($index);
14✔
547
        } while ($this->isPreUnaryOperation($tokens, $index));
14✔
548

549
        return $index;
14✔
550
    }
551

552
    // array access `(X)[` or `(X){` or object access `(X)->` or `(X)?->`
553
    private function isAccess(Tokens $tokens, int $index): bool
554
    {
555
        $token = $tokens[$index];
164✔
556

557
        return $token->isObjectOperator() || $token->equals('[') || $token->isGivenKind([CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN]);
164✔
558
    }
559

560
    private function getAfterAccess(Tokens $tokens, int $index): int
561
    {
562
        while (true) {
9✔
563
            $block = $this->getBlock($tokens, $index, true);
9✔
564

565
            if (null !== $block) {
9✔
566
                $index = $tokens->findBlockEnd($block['type'], $index);
6✔
567
                $index = $tokens->getNextMeaningfulToken($index);
6✔
568

569
                continue;
6✔
570
            }
571

572
            if (
573
                $tokens[$index]->isObjectOperator()
9✔
574
                || $tokens[$index]->equalsAny(['$', [T_PAAMAYIM_NEKUDOTAYIM], [T_STRING], [T_VARIABLE]])
9✔
575
            ) {
576
                $index = $tokens->getNextMeaningfulToken($index);
6✔
577

578
                continue;
6✔
579
            }
580

581
            break;
9✔
582
        }
583

584
        return $index;
9✔
585
    }
586

587
    /**
588
     * @return null|array{type: Tokens::BLOCK_TYPE_*, isStart: bool}
589
     */
590
    private function getBlock(Tokens $tokens, int $index, bool $isStart): ?array
591
    {
592
        $block = Tokens::detectBlockType($tokens[$index]);
170✔
593

594
        return null !== $block && $isStart === $block['isStart'] && \in_array($block['type'], self::BLOCK_TYPES, true) ? $block : null;
170✔
595
    }
596

597
    // cheap check on a tokens type before `(` of which we know the `(` will never be superfluous
598
    private function isKnownNegativePre(Token $token): bool
599
    {
600
        static $knownNegativeTypes;
173✔
601

602
        if (null === $knownNegativeTypes) {
173✔
603
            $knownNegativeTypes = [
×
604
                [CT::T_CLASS_CONSTANT],
×
605
                [CT::T_DYNAMIC_VAR_BRACE_CLOSE],
×
606
                [CT::T_RETURN_REF],
×
607
                [CT::T_USE_LAMBDA],
×
608
                [T_ARRAY],
×
609
                [T_CATCH],
×
610
                [T_CLASS],
×
611
                [T_DECLARE],
×
612
                [T_ELSEIF],
×
613
                [T_EMPTY],
×
614
                [T_EXIT],
×
615
                [T_EVAL],
×
616
                [T_FN],
×
617
                [T_FOREACH],
×
618
                [T_FOR],
×
619
                [T_FUNCTION],
×
620
                [T_HALT_COMPILER],
×
621
                [T_IF],
×
622
                [T_ISSET],
×
623
                [T_LIST],
×
624
                [T_STRING],
×
625
                [T_SWITCH],
×
626
                [T_STATIC],
×
627
                [T_UNSET],
×
628
                [T_VARIABLE],
×
629
                [T_WHILE],
×
630
                // handled by the `include` rule
631
                [T_REQUIRE],
×
632
                [T_REQUIRE_ONCE],
×
633
                [T_INCLUDE],
×
634
                [T_INCLUDE_ONCE],
×
635
            ];
×
636

637
            if (\defined('T_MATCH')) { // @TODO: drop condition and add directly in `$knownNegativeTypes` above when PHP 8.0+ is required
×
638
                $knownNegativeTypes[] = T_MATCH;
×
639
            }
640
        }
641

642
        return $token->equalsAny($knownNegativeTypes);
173✔
643
    }
644

645
    private function containsOperation(Tokens $tokens, int $startIndex, int $endIndex): bool
646
    {
647
        while (true) {
98✔
648
            $startIndex = $tokens->getNextMeaningfulToken($startIndex);
98✔
649

650
            if ($startIndex === $endIndex) {
98✔
651
                break;
83✔
652
            }
653

654
            $block = Tokens::detectBlockType($tokens[$startIndex]);
98✔
655

656
            if (null !== $block && $block['isStart']) {
98✔
657
                $startIndex = $tokens->findBlockEnd($block['type'], $startIndex);
21✔
658

659
                continue;
21✔
660
            }
661

662
            if (!$tokens[$startIndex]->equalsAny(self::NOOP_TYPES)) {
98✔
663
                return true;
24✔
664
            }
665
        }
666

667
        return false;
83✔
668
    }
669

670
    private function getConfigType(Tokens $tokens, int $beforeOpenIndex): ?string
671
    {
672
        if ($tokens[$beforeOpenIndex]->isGivenKind(self::TOKEN_TYPE_NO_CONFIG)) {
153✔
673
            return null;
×
674
        }
675

676
        foreach (self::TOKEN_TYPE_CONFIG_MAP as $type => $configItem) {
153✔
677
            if ($tokens[$beforeOpenIndex]->isGivenKind($type)) {
153✔
678
                return $configItem;
58✔
679
            }
680
        }
681

682
        return 'others';
100✔
683
    }
684

685
    private function removeUselessParenthesisPair(
686
        Tokens $tokens,
687
        int $beforeOpenIndex,
688
        int $afterCloseIndex,
689
        int $openIndex,
690
        int $closeIndex,
691
        ?string $configType
692
    ): void {
693
        $statements = $this->configuration['statements'];
162✔
694

695
        if (null === $configType || !\in_array($configType, $statements, true)) {
162✔
696
            return;
11✔
697
        }
698

699
        $needsSpaceAfter =
162✔
700
            !$this->isAccess($tokens, $afterCloseIndex)
162✔
701
            && !$tokens[$afterCloseIndex]->equalsAny([';', ',', [T_CLOSE_TAG]])
162✔
702
            && null === $this->getBlock($tokens, $afterCloseIndex, false)
162✔
703
            && !($tokens[$afterCloseIndex]->equalsAny([':', ';']) && $tokens[$beforeOpenIndex]->isGivenKind(T_CASE))
162✔
704
        ;
162✔
705

706
        $needsSpaceBefore =
162✔
707
            !$this->isPreUnaryOperation($tokens, $beforeOpenIndex)
162✔
708
            && !$tokens[$beforeOpenIndex]->equalsAny(['}', [T_EXIT], [T_OPEN_TAG]])
162✔
709
            && null === $this->getBlock($tokens, $beforeOpenIndex, true)
162✔
710
        ;
162✔
711

712
        $this->removeBrace($tokens, $closeIndex, $needsSpaceAfter);
162✔
713
        $this->removeBrace($tokens, $openIndex, $needsSpaceBefore);
162✔
714
    }
715

716
    private function removeBrace(Tokens $tokens, int $index, bool $needsSpace): void
717
    {
718
        if ($needsSpace) {
162✔
719
            foreach ([-1, 1] as $direction) {
135✔
720
                $siblingIndex = $tokens->getNonEmptySibling($index, $direction);
135✔
721

722
                if ($tokens[$siblingIndex]->isWhitespace() || $tokens[$siblingIndex]->isComment()) {
135✔
723
                    $needsSpace = false;
108✔
724

725
                    break;
108✔
726
                }
727
            }
728
        }
729

730
        if ($needsSpace) {
162✔
731
            $tokens[$index] = new Token([T_WHITESPACE, ' ']);
37✔
732
        } else {
733
            $tokens->clearTokenAndMergeSurroundingWhitespace($index);
161✔
734
        }
735
    }
736

737
    private function closeCurlyBelongsToDynamicElement(Tokens $tokens, int $beforeOpenIndex): bool
738
    {
739
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $beforeOpenIndex);
10✔
740
        $index = $tokens->getPrevMeaningfulToken($index);
10✔
741

742
        if ($tokens[$index]->isGivenKind(T_DOUBLE_COLON)) {
10✔
743
            return true;
3✔
744
        }
745

746
        if ($tokens[$index]->equals(':')) {
7✔
747
            $index = $tokens->getPrevTokenOfKind($index, [[T_CASE], '?']);
1✔
748

749
            return !$tokens[$index]->isGivenKind(T_CASE);
1✔
750
        }
751

752
        return false;
7✔
753
    }
754
}
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

© 2025 Coveralls, Inc