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

keradus / PHP-CS-Fixer / 25961188992

15 May 2026 03:27PM UTC coverage: 93.797% (+0.7%) from 93.053%
25961188992

push

github

web-flow
fix: `MultilinePromotedPropertiesFixer` - fix for `new` in initializers (#9619)

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

49 existing lines in 7 files now uncovered.

29908 of 31886 relevant lines covered (93.8%)

51.7 hits per line

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

96.58
/src/Fixer/ReturnNotation/ReturnAssignmentFixer.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\ReturnNotation;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\DocBlock\DocBlock;
19
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
20
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
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
 *  skip_named_var_tags?: bool,
36
 * }
37
 * @phpstan-type _AutogeneratedComputedConfiguration array{
38
 *  skip_named_var_tags: bool,
39
 * }
40
 *
41
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
42
 *
43
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
44
 */
45
final class ReturnAssignmentFixer extends AbstractFixer implements ConfigurableFixerInterface
46
{
47
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
48
    use ConfigurableFixerTrait;
49

50
    private TokensAnalyzer $tokensAnalyzer;
51

52
    public function getDefinition(): FixerDefinitionInterface
53
    {
54
        return new FixerDefinition(
3✔
55
            'Local, dynamic and directly referenced variables should not be assigned and directly returned by a function or method.',
3✔
56
            [
3✔
57
                new CodeSample(
3✔
58
                    <<<'PHP'
3✔
59
                        <?php
60

61
                        function a() {
62
                            $a = 1;
63
                            return $a;
64
                        }
65

66
                        function foo() {
67
                            /** @var int[] */
68
                            $a = doSomething();
69

70
                            return $a;
71
                        }
72

73
                        function bar() {
74
                            /** @var int[] $b */
75
                            $b = doSomething();
76

77
                            return $b;
78
                        }
79

80
                        PHP,
3✔
81
                ),
3✔
82
                new CodeSample(
3✔
83
                    <<<'PHP'
3✔
84
                        <?php
85

86
                        function a() {
87
                            $a = 1;
88
                            return $a;
89
                        }
90

91
                        function foo() {
92
                            /** @var int[] */
93
                            $a = doSomething();
94

95
                            return $a;
96
                        }
97

98
                        function bar() {
99
                            /** @var int[] $b */
100
                            $b = doSomething();
101

102
                            return $b;
103
                        }
104

105
                        PHP,
3✔
106
                    [
3✔
107
                        'skip_named_var_tags' => true,
3✔
108
                    ],
3✔
109
                ),
3✔
110
            ],
3✔
111
        );
3✔
112
    }
113

114
    /**
115
     * {@inheritdoc}
116
     *
117
     * Must run before BlankLineBeforeStatementFixer.
118
     * Must run after NoEmptyStatementFixer, NoUnneededBracesFixer, NoUnneededCurlyBracesFixer.
119
     */
120
    public function getPriority(): int
121
    {
122
        return -15;
1✔
123
    }
124

125
    public function isCandidate(Tokens $tokens): bool
126
    {
127
        return $tokens->isAllTokenKindsFound([\T_FUNCTION, \T_RETURN, \T_VARIABLE]);
81✔
128
    }
129

130
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
131
    {
132
        $tokenCount = \count($tokens);
80✔
133
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
80✔
134

135
        for ($index = 1; $index < $tokenCount; ++$index) {
80✔
136
            if (!$tokens[$index]->isGivenKind(\T_FUNCTION)) {
80✔
137
                continue;
78✔
138
            }
139

140
            $next = $tokens->getNextMeaningfulToken($index);
80✔
141
            if ($tokens[$next]->isGivenKind(CT::T_RETURN_REF)) {
80✔
142
                continue;
3✔
143
            }
144

145
            $functionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
77✔
146
            if ($tokens[$functionOpenIndex]->equals(';')) { // abstract function
77✔
147
                $index = $functionOpenIndex - 1;
1✔
148

149
                continue;
1✔
150
            }
151

152
            $functionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $functionOpenIndex);
77✔
153
            $totalTokensAdded = 0;
77✔
154

155
            do {
156
                $tokensAdded = $this->fixFunction(
77✔
157
                    $tokens,
77✔
158
                    $index,
77✔
159
                    $functionOpenIndex,
77✔
160
                    $functionCloseIndex,
77✔
161
                );
77✔
162

163
                $functionCloseIndex += $tokensAdded;
77✔
164
                $totalTokensAdded += $tokensAdded;
77✔
165
            } while ($tokensAdded > 0);
77✔
166

167
            $index = $functionCloseIndex;
77✔
168
            $tokenCount += $totalTokensAdded;
77✔
169
        }
170
    }
171

172
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
173
    {
174
        return new FixerConfigurationResolver([
92✔
175
            (new FixerOptionBuilder('skip_named_var_tags', 'Whether to skip cases where named `@var` tags are used.'))
92✔
176
                ->setDefault(false)
92✔
177
                ->setAllowedTypes(['bool'])
92✔
178
                ->getOption(),
92✔
179
        ]);
92✔
180
    }
181

182
    /**
183
     * @param int $functionIndex      token index of T_FUNCTION
184
     * @param int $functionOpenIndex  token index of the opening brace token of the function
185
     * @param int $functionCloseIndex token index of the closing brace token of the function
186
     *
187
     * @return int >= 0 number of tokens inserted into the Tokens collection
188
     */
189
    private function fixFunction(Tokens $tokens, int $functionIndex, int $functionOpenIndex, int $functionCloseIndex): int
190
    {
191
        $inserted = 0;
77✔
192
        $candidates = [];
77✔
193
        $isRisky = false;
77✔
194

195
        if ($tokens[$tokens->getNextMeaningfulToken($functionIndex)]->isGivenKind(CT::T_RETURN_REF)) {
77✔
196
            $isRisky = true;
1✔
197
        }
198

199
        // go through the function declaration and check if references are passed
200
        // - check if it will be risky to fix return statements of this function
201
        for ($index = $functionIndex + 1; $index < $functionOpenIndex; ++$index) {
77✔
202
            if ($tokens[$index]->equals('&')) {
77✔
203
                $isRisky = true;
4✔
204

205
                break;
4✔
206
            }
207
        }
208

209
        // go through all the tokens of the body of the function:
210
        // - check if it will be risky to fix return statements of this function
211
        // - check nested functions; fix when found and update the upper limit + number of inserted token
212
        // - check for return statements that might be fixed (based on if fixing will be risky, which is only know after analyzing the whole function)
213

214
        for ($index = $functionOpenIndex + 1; $index < $functionCloseIndex; ++$index) {
77✔
215
            if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
77✔
216
                $nestedFunctionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
5✔
217
                if ($tokens[$nestedFunctionOpenIndex]->equals(';')) { // abstract function
5✔
UNCOV
218
                    $index = $nestedFunctionOpenIndex - 1;
×
219

UNCOV
220
                    continue;
×
221
                }
222

223
                $nestedFunctionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nestedFunctionOpenIndex);
5✔
224

225
                $tokensAdded = $this->fixFunction(
5✔
226
                    $tokens,
5✔
227
                    $index,
5✔
228
                    $nestedFunctionOpenIndex,
5✔
229
                    $nestedFunctionCloseIndex,
5✔
230
                );
5✔
231

232
                $index = $nestedFunctionCloseIndex + $tokensAdded;
5✔
233
                $functionCloseIndex += $tokensAdded;
5✔
234
                $inserted += $tokensAdded;
5✔
235
            }
236

237
            if ($isRisky) {
77✔
238
                continue; // don't bother to look into anything else than nested functions as the current is risky already
16✔
239
            }
240

241
            if ($tokens[$index]->equals('&')) {
75✔
242
                $isRisky = true;
1✔
243

244
                continue;
1✔
245
            }
246

247
            if ($tokens[$index]->isGivenKind(\T_RETURN)) {
75✔
248
                $candidates[] = $index;
63✔
249

250
                continue;
63✔
251
            }
252

253
            // test if there is anything in the function body that might
254
            // change global state or indirect changes (like through references, eval, etc.)
255

256
            if ($tokens[$index]->isGivenKind([
75✔
257
                CT::T_DYNAMIC_VAR_BRACE_OPEN, // "$h = ${$g};" case
75✔
258
                \T_EVAL,                       // "$c = eval('return $this;');" case
75✔
259
                \T_GLOBAL,
75✔
260
                \T_INCLUDE,                    // loading additional symbols we cannot analyse here
75✔
261
                \T_INCLUDE_ONCE,               // "
75✔
262
                \T_REQUIRE,                    // "
75✔
263
                \T_REQUIRE_ONCE,               // "
75✔
264
            ])) {
75✔
265
                $isRisky = true;
8✔
266

267
                continue;
8✔
268
            }
269

270
            if ($tokens[$index]->isGivenKind(\T_STATIC)) {
75✔
271
                $nextIndex = $tokens->getNextMeaningfulToken($index);
2✔
272

273
                if (!$tokens[$nextIndex]->isGivenKind(\T_FUNCTION)) {
2✔
274
                    $isRisky = true; // "static $a" case
1✔
275

276
                    continue;
1✔
277
                }
278
            }
279

280
            if ($tokens[$index]->equals('$')) {
75✔
281
                $nextIndex = $tokens->getNextMeaningfulToken($index);
2✔
282
                if ($tokens[$nextIndex]->isGivenKind(\T_VARIABLE)) {
2✔
283
                    $isRisky = true; // "$$a" case
1✔
284

285
                    continue;
1✔
286
                }
287
            }
288

289
            if ($this->tokensAnalyzer->isSuperGlobal($index)) {
75✔
290
                $isRisky = true;
2✔
291

292
                continue;
2✔
293
            }
294
        }
295

296
        if ($isRisky) {
77✔
297
            return $inserted;
16✔
298
        }
299

300
        // fix the candidates in reverse order when applicable
301
        for ($i = \count($candidates) - 1; $i >= 0; --$i) {
63✔
302
            $index = $candidates[$i];
63✔
303

304
            // Check if returning only a variable (i.e. not the result of an expression, function call etc.)
305
            $returnVarIndex = $tokens->getNextMeaningfulToken($index);
63✔
306
            if (!$tokens[$returnVarIndex]->isGivenKind(\T_VARIABLE)) {
63✔
307
                continue; // example: "return 1;"
40✔
308
            }
309

310
            $endReturnVarIndex = $tokens->getNextMeaningfulToken($returnVarIndex);
63✔
311
            if (!$tokens[$endReturnVarIndex]->equalsAny([';', [\T_CLOSE_TAG]])) {
63✔
312
                continue; // example: "return $a + 1;"
4✔
313
            }
314

315
            // Check that the variable is assigned just before it is returned
316
            $assignVarEndIndex = $tokens->getPrevMeaningfulToken($index);
62✔
317
            if (!$tokens[$assignVarEndIndex]->equals(';')) {
62✔
318
                continue; // example: "? return $a;"
4✔
319
            }
320

321
            // Note: here we are @ "; return $a;" (or "; return $a ? >")
322
            while (true) {
59✔
323
                $prevMeaningful = $tokens->getPrevMeaningfulToken($assignVarEndIndex);
59✔
324

325
                if (!$tokens[$prevMeaningful]->equals(')')) {
59✔
326
                    break;
59✔
327
                }
328

329
                $assignVarEndIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $prevMeaningful);
30✔
330
            }
331

332
            $assignVarOperatorIndex = $tokens->getPrevTokenOfKind(
59✔
333
                $assignVarEndIndex,
59✔
334
                ['=', ';', '{', '}', [\T_OPEN_TAG], [\T_OPEN_TAG_WITH_ECHO]],
59✔
335
            );
59✔
336

337
            if ($tokens[$assignVarOperatorIndex]->equals('}')) {
59✔
338
                $startIndex = $this->isCloseBracePartOfDefinition($tokens, $assignVarOperatorIndex); // test for `anonymous class`, `lambda` and `match`
7✔
339

340
                if (null === $startIndex) {
7✔
341
                    continue;
1✔
342
                }
343

344
                $assignVarOperatorIndex = $tokens->getPrevMeaningfulToken($startIndex);
6✔
345
            }
346

347
            if (!$tokens[$assignVarOperatorIndex]->equals('=')) {
58✔
348
                continue;
5✔
349
            }
350

351
            // Note: here we are @ "= [^;{<? ? >] ; return $a;"
352
            $assignVarIndex = $tokens->getPrevMeaningfulToken($assignVarOperatorIndex);
53✔
353
            if (!$tokens[$assignVarIndex]->equals($tokens[$returnVarIndex], false)) {
53✔
UNCOV
354
                continue;
×
355
            }
356

357
            // Note: here we are @ "$a = [^;{<? ? >] ; return $a;"
358
            $beforeAssignVarIndex = $tokens->getPrevMeaningfulToken($assignVarIndex);
53✔
359
            if (!$tokens[$beforeAssignVarIndex]->equalsAny([';', '{', '}'])) {
53✔
360
                continue;
4✔
361
            }
362

363
            if (
364
                $this->configuration['skip_named_var_tags']
49✔
365
                && $this->hasNamedVarTag($tokens, $assignVarIndex, $functionOpenIndex)
49✔
366
            ) {
367
                continue;
4✔
368
            }
369

370
            // Check if there is a `catch` or `finally` block between the assignment and the return
371
            if ($this->isUsedInCatchOrFinally($tokens, $returnVarIndex, $functionOpenIndex, $functionCloseIndex)) {
46✔
372
                continue;
8✔
373
            }
374

375
            // Note: here we are @ "[;{}] $a = [^;{<? ? >] ; return $a;"
376
            $inserted += $this->simplifyReturnStatement(
42✔
377
                $tokens,
42✔
378
                $assignVarIndex,
42✔
379
                $assignVarOperatorIndex,
42✔
380
                $index,
42✔
381
                $endReturnVarIndex,
42✔
382
            );
42✔
383
        }
384

385
        return $inserted;
63✔
386
    }
387

388
    /**
389
     * @return int >= 0 number of tokens inserted into the Tokens collection
390
     */
391
    private function simplifyReturnStatement(
392
        Tokens $tokens,
393
        int $assignVarIndex,
394
        int $assignVarOperatorIndex,
395
        int $returnIndex,
396
        int $returnVarEndIndex
397
    ): int {
398
        $inserted = 0;
42✔
399
        $originalIndent = $tokens[$assignVarIndex - 1]->isWhitespace()
42✔
400
            ? $tokens[$assignVarIndex - 1]->getContent()
39✔
401
            : null;
3✔
402

403
        // remove the return statement
404
        if ($tokens[$returnVarEndIndex]->equals(';')) { // do not remove PHP close tags
42✔
405
            $tokens->clearTokenAndMergeSurroundingWhitespace($returnVarEndIndex);
41✔
406
        }
407

408
        for ($i = $returnIndex; $i <= $returnVarEndIndex - 1; ++$i) {
42✔
409
            $this->clearIfSave($tokens, $i);
42✔
410
        }
411

412
        // remove no longer needed indentation of the old/remove return statement
413
        if ($tokens[$returnIndex - 1]->isWhitespace()) {
42✔
414
            $content = $tokens[$returnIndex - 1]->getContent();
39✔
415
            $fistLinebreakPos = strrpos($content, "\n");
39✔
416
            $content = false === $fistLinebreakPos
39✔
417
                ? ' '
2✔
418
                : substr($content, $fistLinebreakPos);
37✔
419

420
            $tokens[$returnIndex - 1] = new Token([\T_WHITESPACE, $content]);
39✔
421
        }
422

423
        // remove the variable and the assignment
424
        for ($i = $assignVarIndex; $i <= $assignVarOperatorIndex; ++$i) {
42✔
425
            $this->clearIfSave($tokens, $i);
42✔
426
        }
427

428
        // insert new return statement
429
        $tokens->insertAt($assignVarIndex, new Token([\T_RETURN, 'return']));
42✔
430
        ++$inserted;
42✔
431

432
        // use the original indent of the var assignment for the new return statement
433
        if (
434
            null !== $originalIndent
42✔
435
            && $tokens[$assignVarIndex - 1]->isWhitespace()
42✔
436
            && $originalIndent !== $tokens[$assignVarIndex - 1]->getContent()
42✔
437
        ) {
438
            $tokens[$assignVarIndex - 1] = new Token([\T_WHITESPACE, $originalIndent]);
38✔
439
        }
440

441
        // remove trailing space after the new return statement which might be added during the cleanup process
442
        $nextIndex = $tokens->getNonEmptySibling($assignVarIndex, 1);
42✔
443
        if (!$tokens[$nextIndex]->isWhitespace()) {
42✔
444
            $tokens->insertAt($nextIndex, new Token([\T_WHITESPACE, ' ']));
41✔
445
            ++$inserted;
41✔
446
        }
447

448
        return $inserted;
42✔
449
    }
450

451
    private function clearIfSave(Tokens $tokens, int $index): void
452
    {
453
        if ($tokens[$index]->isComment()) {
42✔
454
            return;
3✔
455
        }
456

457
        if ($tokens[$index]->isWhitespace() && $tokens[$tokens->getPrevNonWhitespace($index)]->isComment()) {
42✔
458
            return;
16✔
459
        }
460

461
        $tokens->clearTokenAndMergeSurroundingWhitespace($index);
42✔
462
    }
463

464
    /**
465
     * @param int $index open brace index
466
     *
467
     * @return null|int index of the first token of a definition (lambda, anonymous class or match) or `null` if not an anonymous
468
     */
469
    private function isCloseBracePartOfDefinition(Tokens $tokens, int $index): ?int
470
    {
471
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
7✔
472
        $candidateIndex = $this->isOpenBraceOfLambda($tokens, $index);
7✔
473

474
        if (null !== $candidateIndex) {
7✔
475
            return $candidateIndex;
1✔
476
        }
477

478
        $candidateIndex = $this->isOpenBraceOfAnonymousClass($tokens, $index);
6✔
479

480
        return $candidateIndex ?? $this->isOpenBraceOfMatch($tokens, $index);
6✔
481
    }
482

483
    /**
484
     * @param int $index open brace index
485
     *
486
     * @return null|int index of T_NEW of anonymous class or `null` if not an anonymous
487
     */
488
    private function isOpenBraceOfAnonymousClass(Tokens $tokens, int $index): ?int
489
    {
490
        do {
491
            $index = $tokens->getPrevMeaningfulToken($index);
6✔
492
        } while ($tokens[$index]->equalsAny([',', [\T_STRING], [\T_IMPLEMENTS], [\T_EXTENDS], [\T_NS_SEPARATOR]]));
6✔
493

494
        if ($tokens[$index]->equals(')')) { // skip constructor braces and content within
6✔
495
            $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
3✔
496
            $index = $tokens->getPrevMeaningfulToken($index);
3✔
497
        }
498

499
        if (!$tokens[$index]->isGivenKind(\T_CLASS) || !$this->tokensAnalyzer->isAnonymousClass($index)) {
6✔
500
            return null;
2✔
501
        }
502

503
        return $tokens->getPrevTokenOfKind($index, [[\T_NEW]]);
4✔
504
    }
505

506
    /**
507
     * @param int $index open brace index
508
     *
509
     * @return null|int index of T_FUNCTION or T_STATIC of lambda or `null` if not a lambda
510
     */
511
    private function isOpenBraceOfLambda(Tokens $tokens, int $index): ?int
512
    {
513
        $index = $tokens->getPrevMeaningfulToken($index);
7✔
514

515
        if (!$tokens[$index]->equals(')')) {
7✔
516
            return null;
4✔
517
        }
518

519
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
4✔
520
        $index = $tokens->getPrevMeaningfulToken($index);
4✔
521

522
        if ($tokens[$index]->isGivenKind(CT::T_USE_LAMBDA)) {
4✔
523
            $index = $tokens->getPrevTokenOfKind($index, [')']);
1✔
524
            $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
1✔
525
            $index = $tokens->getPrevMeaningfulToken($index);
1✔
526
        }
527

528
        if ($tokens[$index]->isGivenKind(CT::T_RETURN_REF)) {
4✔
529
            $index = $tokens->getPrevMeaningfulToken($index);
1✔
530
        }
531

532
        if (!$tokens[$index]->isGivenKind(\T_FUNCTION)) {
4✔
533
            return null;
3✔
534
        }
535

536
        $staticCandidate = $tokens->getPrevMeaningfulToken($index);
1✔
537

538
        return $tokens[$staticCandidate]->isGivenKind(\T_STATIC) ? $staticCandidate : $index;
1✔
539
    }
540

541
    /**
542
     * @param int $index open brace index
543
     *
544
     * @return null|int index of T_MATCH or `null` if not a `match`
545
     */
546
    private function isOpenBraceOfMatch(Tokens $tokens, int $index): ?int
547
    {
548
        if (!$tokens->isTokenKindFound(FCT::T_MATCH)) {
2✔
549
            return null;
1✔
550
        }
551

552
        $index = $tokens->getPrevMeaningfulToken($index);
1✔
553

554
        if (!$tokens[$index]->equals(')')) {
1✔
UNCOV
555
            return null;
×
556
        }
557

558
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
1✔
559
        $index = $tokens->getPrevMeaningfulToken($index);
1✔
560

561
        return $tokens[$index]->isGivenKind(\T_MATCH) ? $index : null;
1✔
562
    }
563

564
    private function hasNamedVarTag(Tokens $tokens, int $assignVarIndex, int $functionOpenIndex): bool
565
    {
566
        $docIndex = $tokens->getPrevTokenOfKind($assignVarIndex, [[\T_DOC_COMMENT]]);
7✔
567
        if (null === $docIndex || $docIndex <= $functionOpenIndex) {
7✔
568
            return false;
1✔
569
        }
570

571
        $doc = new DocBlock($tokens[$docIndex]->getContent());
7✔
572
        $annotations = $doc->getAnnotationsOfType(['var', 'psalm-var', 'phpstan-var']);
7✔
573

574
        foreach ($annotations as $annotation) {
7✔
575
            if (null !== $annotation->getVariableName()) {
7✔
576
                return true;
4✔
577
            }
578
        }
579

580
        return false;
4✔
581
    }
582

583
    private function isUsedInCatchOrFinally(Tokens $tokens, int $returnVarIndex, int $functionOpenIndex, int $functionCloseIndex): bool
584
    {
585
        // Find try
586
        $tryIndex = $tokens->getPrevTokenOfKind($returnVarIndex, [[\T_TRY]]);
46✔
587
        if (null === $tryIndex || $tryIndex <= $functionOpenIndex) {
46✔
588
            return false;
40✔
589
        }
590
        $tryOpenIndex = $tokens->getNextTokenOfKind($tryIndex, ['{']);
8✔
591
        $tryCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tryOpenIndex);
8✔
592

593
        // Find catch or finally
594
        $nextIndex = $tokens->getNextMeaningfulToken($tryCloseIndex);
8✔
595
        if (null === $nextIndex) {
8✔
UNCOV
596
            return false;
×
597
        }
598

599
        // Find catches
600
        while ($tokens[$nextIndex]->isGivenKind(\T_CATCH)) {
8✔
601
            $catchOpenIndex = $tokens->getNextTokenOfKind($nextIndex, ['{']);
8✔
602
            $catchCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $catchOpenIndex);
8✔
603

604
            if ($catchCloseIndex >= $functionCloseIndex) {
8✔
UNCOV
605
                return false;
×
606
            }
607
            $varIndex = $tokens->getNextTokenOfKind($catchOpenIndex, [$tokens[$returnVarIndex]]);
8✔
608
            // Check if the variable is used in the finally block
609
            if (null !== $varIndex && $varIndex < $catchCloseIndex) {
8✔
610
                return true;
3✔
611
            }
612

613
            $nextIndex = $tokens->getNextMeaningfulToken($catchCloseIndex);
7✔
614
            if (null === $nextIndex) {
7✔
UNCOV
615
                return false;
×
616
            }
617
        }
618

619
        if (!$tokens[$nextIndex]->isGivenKind(\T_FINALLY)) {
6✔
620
            return false;
2✔
621
        }
622

623
        $finallyIndex = $nextIndex;
5✔
624
        if ($finallyIndex >= $functionCloseIndex) {
5✔
UNCOV
625
            return false;
×
626
        }
627
        $finallyOpenIndex = $tokens->getNextTokenOfKind($finallyIndex, ['{']);
5✔
628
        $finallyCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $finallyOpenIndex);
5✔
629
        $varIndex = $tokens->getNextTokenOfKind($finallyOpenIndex, [$tokens[$returnVarIndex]]);
5✔
630
        // Check if the variable is used in the finally block
631
        if (null !== $varIndex && $varIndex < $finallyCloseIndex) {
5✔
632
            return true;
5✔
633
        }
634

UNCOV
635
        return false;
×
636
    }
637
}
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