• 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

96.15
/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\FixerDefinition\CodeSample;
19
use PhpCsFixer\FixerDefinition\FixerDefinition;
20
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
21
use PhpCsFixer\Tokenizer\CT;
22
use PhpCsFixer\Tokenizer\FCT;
23
use PhpCsFixer\Tokenizer\Token;
24
use PhpCsFixer\Tokenizer\Tokens;
25
use PhpCsFixer\Tokenizer\TokensAnalyzer;
26

27
final class ReturnAssignmentFixer extends AbstractFixer
28
{
29
    private TokensAnalyzer $tokensAnalyzer;
30

31
    public function getDefinition(): FixerDefinitionInterface
32
    {
33
        return new FixerDefinition(
3✔
34
            'Local, dynamic and directly referenced variables should not be assigned and directly returned by a function or method.',
3✔
35
            [new CodeSample("<?php\nfunction a() {\n    \$a = 1;\n    return \$a;\n}\n")]
3✔
36
        );
3✔
37
    }
38

39
    /**
40
     * {@inheritdoc}
41
     *
42
     * Must run before BlankLineBeforeStatementFixer.
43
     * Must run after NoEmptyStatementFixer, NoUnneededBracesFixer, NoUnneededCurlyBracesFixer.
44
     */
45
    public function getPriority(): int
46
    {
47
        return -15;
1✔
48
    }
49

50
    public function isCandidate(Tokens $tokens): bool
51
    {
52
        return $tokens->isAllTokenKindsFound([T_FUNCTION, T_RETURN, T_VARIABLE]);
67✔
53
    }
54

55
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
56
    {
57
        $tokenCount = \count($tokens);
66✔
58
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
66✔
59

60
        for ($index = 1; $index < $tokenCount; ++$index) {
66✔
61
            if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
66✔
62
                continue;
64✔
63
            }
64

65
            $next = $tokens->getNextMeaningfulToken($index);
66✔
66
            if ($tokens[$next]->isGivenKind(CT::T_RETURN_REF)) {
66✔
67
                continue;
3✔
68
            }
69

70
            $functionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
63✔
71
            if ($tokens[$functionOpenIndex]->equals(';')) { // abstract function
63✔
72
                $index = $functionOpenIndex - 1;
1✔
73

74
                continue;
1✔
75
            }
76

77
            $functionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $functionOpenIndex);
63✔
78
            $totalTokensAdded = 0;
63✔
79

80
            do {
81
                $tokensAdded = $this->fixFunction(
63✔
82
                    $tokens,
63✔
83
                    $index,
63✔
84
                    $functionOpenIndex,
63✔
85
                    $functionCloseIndex
63✔
86
                );
63✔
87

88
                $functionCloseIndex += $tokensAdded;
63✔
89
                $totalTokensAdded += $tokensAdded;
63✔
90
            } while ($tokensAdded > 0);
63✔
91

92
            $index = $functionCloseIndex;
63✔
93
            $tokenCount += $totalTokensAdded;
63✔
94
        }
95
    }
96

97
    /**
98
     * @param int $functionIndex      token index of T_FUNCTION
99
     * @param int $functionOpenIndex  token index of the opening brace token of the function
100
     * @param int $functionCloseIndex token index of the closing brace token of the function
101
     *
102
     * @return int >= 0 number of tokens inserted into the Tokens collection
103
     */
104
    private function fixFunction(Tokens $tokens, int $functionIndex, int $functionOpenIndex, int $functionCloseIndex): int
105
    {
106
        static $riskyKinds = [
63✔
107
            CT::T_DYNAMIC_VAR_BRACE_OPEN, // "$h = ${$g};" case
63✔
108
            T_EVAL,                       // "$c = eval('return $this;');" case
63✔
109
            T_GLOBAL,
63✔
110
            T_INCLUDE,                    // loading additional symbols we cannot analyze here
63✔
111
            T_INCLUDE_ONCE,               // "
63✔
112
            T_REQUIRE,                    // "
63✔
113
            T_REQUIRE_ONCE,               // "
63✔
114
        ];
63✔
115

116
        $inserted = 0;
63✔
117
        $candidates = [];
63✔
118
        $isRisky = false;
63✔
119

120
        if ($tokens[$tokens->getNextMeaningfulToken($functionIndex)]->isGivenKind(CT::T_RETURN_REF)) {
63✔
121
            $isRisky = true;
1✔
122
        }
123

124
        // go through the function declaration and check if references are passed
125
        // - check if it will be risky to fix return statements of this function
126
        for ($index = $functionIndex + 1; $index < $functionOpenIndex; ++$index) {
63✔
127
            if ($tokens[$index]->equals('&')) {
63✔
128
                $isRisky = true;
4✔
129

130
                break;
4✔
131
            }
132
        }
133

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

139
        for ($index = $functionOpenIndex + 1; $index < $functionCloseIndex; ++$index) {
63✔
140
            if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
63✔
141
                $nestedFunctionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
5✔
142
                if ($tokens[$nestedFunctionOpenIndex]->equals(';')) { // abstract function
5✔
143
                    $index = $nestedFunctionOpenIndex - 1;
×
144

145
                    continue;
×
146
                }
147

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

150
                $tokensAdded = $this->fixFunction(
5✔
151
                    $tokens,
5✔
152
                    $index,
5✔
153
                    $nestedFunctionOpenIndex,
5✔
154
                    $nestedFunctionCloseIndex
5✔
155
                );
5✔
156

157
                $index = $nestedFunctionCloseIndex + $tokensAdded;
5✔
158
                $functionCloseIndex += $tokensAdded;
5✔
159
                $inserted += $tokensAdded;
5✔
160
            }
161

162
            if ($isRisky) {
63✔
163
                continue; // don't bother to look into anything else than nested functions as the current is risky already
16✔
164
            }
165

166
            if ($tokens[$index]->equals('&')) {
61✔
167
                $isRisky = true;
1✔
168

169
                continue;
1✔
170
            }
171

172
            if ($tokens[$index]->isGivenKind(T_RETURN)) {
61✔
173
                $candidates[] = $index;
49✔
174

175
                continue;
49✔
176
            }
177

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

181
            if ($tokens[$index]->isGivenKind($riskyKinds)) {
61✔
182
                $isRisky = true;
8✔
183

184
                continue;
8✔
185
            }
186

187
            if ($tokens[$index]->isGivenKind(T_STATIC)) {
61✔
188
                $nextIndex = $tokens->getNextMeaningfulToken($index);
2✔
189

190
                if (!$tokens[$nextIndex]->isGivenKind(T_FUNCTION)) {
2✔
191
                    $isRisky = true; // "static $a" case
1✔
192

193
                    continue;
1✔
194
                }
195
            }
196

197
            if ($tokens[$index]->equals('$')) {
61✔
198
                $nextIndex = $tokens->getNextMeaningfulToken($index);
2✔
199
                if ($tokens[$nextIndex]->isGivenKind(T_VARIABLE)) {
2✔
200
                    $isRisky = true; // "$$a" case
1✔
201

202
                    continue;
1✔
203
                }
204
            }
205

206
            if ($this->tokensAnalyzer->isSuperGlobal($index)) {
61✔
207
                $isRisky = true;
2✔
208

209
                continue;
2✔
210
            }
211
        }
212

213
        if ($isRisky) {
63✔
214
            return $inserted;
16✔
215
        }
216

217
        // fix the candidates in reverse order when applicable
218
        for ($i = \count($candidates) - 1; $i >= 0; --$i) {
49✔
219
            $index = $candidates[$i];
49✔
220

221
            // Check if returning only a variable (i.e. not the result of an expression, function call etc.)
222
            $returnVarIndex = $tokens->getNextMeaningfulToken($index);
49✔
223
            if (!$tokens[$returnVarIndex]->isGivenKind(T_VARIABLE)) {
49✔
224
                continue; // example: "return 1;"
29✔
225
            }
226

227
            $endReturnVarIndex = $tokens->getNextMeaningfulToken($returnVarIndex);
49✔
228
            if (!$tokens[$endReturnVarIndex]->equalsAny([';', [T_CLOSE_TAG]])) {
49✔
229
                continue; // example: "return $a + 1;"
4✔
230
            }
231

232
            // Check that the variable is assigned just before it is returned
233
            $assignVarEndIndex = $tokens->getPrevMeaningfulToken($index);
48✔
234
            if (!$tokens[$assignVarEndIndex]->equals(';')) {
48✔
235
                continue; // example: "? return $a;"
4✔
236
            }
237

238
            // Note: here we are @ "; return $a;" (or "; return $a ? >")
239
            while (true) {
45✔
240
                $prevMeaningFul = $tokens->getPrevMeaningfulToken($assignVarEndIndex);
45✔
241

242
                if (!$tokens[$prevMeaningFul]->equals(')')) {
45✔
243
                    break;
45✔
244
                }
245

246
                $assignVarEndIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $prevMeaningFul);
15✔
247
            }
248

249
            $assignVarOperatorIndex = $tokens->getPrevTokenOfKind(
45✔
250
                $assignVarEndIndex,
45✔
251
                ['=', ';', '{', '}', [T_OPEN_TAG], [T_OPEN_TAG_WITH_ECHO]]
45✔
252
            );
45✔
253

254
            if ($tokens[$assignVarOperatorIndex]->equals('}')) {
45✔
255
                $startIndex = $this->isCloseBracePartOfDefinition($tokens, $assignVarOperatorIndex); // test for `anonymous class`, `lambda` and `match`
7✔
256

257
                if (null === $startIndex) {
7✔
258
                    continue;
1✔
259
                }
260

261
                $assignVarOperatorIndex = $tokens->getPrevMeaningfulToken($startIndex);
6✔
262
            }
263

264
            if (!$tokens[$assignVarOperatorIndex]->equals('=')) {
44✔
265
                continue;
5✔
266
            }
267

268
            // Note: here we are @ "= [^;{<? ? >] ; return $a;"
269
            $assignVarIndex = $tokens->getPrevMeaningfulToken($assignVarOperatorIndex);
39✔
270
            if (!$tokens[$assignVarIndex]->equals($tokens[$returnVarIndex], false)) {
39✔
271
                continue;
×
272
            }
273

274
            // Note: here we are @ "$a = [^;{<? ? >] ; return $a;"
275
            $beforeAssignVarIndex = $tokens->getPrevMeaningfulToken($assignVarIndex);
39✔
276
            if (!$tokens[$beforeAssignVarIndex]->equalsAny([';', '{', '}'])) {
39✔
277
                continue;
4✔
278
            }
279

280
            // Check if there is a `catch` or `finally` block between the assignment and the return
281
            if ($this->isUsedInCatchOrFinally($tokens, $returnVarIndex, $functionOpenIndex, $functionCloseIndex)) {
35✔
282
                continue;
8✔
283
            }
284

285
            // Note: here we are @ "[;{}] $a = [^;{<? ? >] ; return $a;"
286
            $inserted += $this->simplifyReturnStatement(
31✔
287
                $tokens,
31✔
288
                $assignVarIndex,
31✔
289
                $assignVarOperatorIndex,
31✔
290
                $index,
31✔
291
                $endReturnVarIndex
31✔
292
            );
31✔
293
        }
294

295
        return $inserted;
49✔
296
    }
297

298
    /**
299
     * @return int >= 0 number of tokens inserted into the Tokens collection
300
     */
301
    private function simplifyReturnStatement(
302
        Tokens $tokens,
303
        int $assignVarIndex,
304
        int $assignVarOperatorIndex,
305
        int $returnIndex,
306
        int $returnVarEndIndex
307
    ): int {
308
        $inserted = 0;
31✔
309
        $originalIndent = $tokens[$assignVarIndex - 1]->isWhitespace()
31✔
310
            ? $tokens[$assignVarIndex - 1]->getContent()
28✔
311
            : null;
3✔
312

313
        // remove the return statement
314
        if ($tokens[$returnVarEndIndex]->equals(';')) { // do not remove PHP close tags
31✔
315
            $tokens->clearTokenAndMergeSurroundingWhitespace($returnVarEndIndex);
30✔
316
        }
317

318
        for ($i = $returnIndex; $i <= $returnVarEndIndex - 1; ++$i) {
31✔
319
            $this->clearIfSave($tokens, $i);
31✔
320
        }
321

322
        // remove no longer needed indentation of the old/remove return statement
323
        if ($tokens[$returnIndex - 1]->isWhitespace()) {
31✔
324
            $content = $tokens[$returnIndex - 1]->getContent();
28✔
325
            $fistLinebreakPos = strrpos($content, "\n");
28✔
326
            $content = false === $fistLinebreakPos
28✔
327
                ? ' '
2✔
328
                : substr($content, $fistLinebreakPos);
26✔
329

330
            $tokens[$returnIndex - 1] = new Token([T_WHITESPACE, $content]);
28✔
331
        }
332

333
        // remove the variable and the assignment
334
        for ($i = $assignVarIndex; $i <= $assignVarOperatorIndex; ++$i) {
31✔
335
            $this->clearIfSave($tokens, $i);
31✔
336
        }
337

338
        // insert new return statement
339
        $tokens->insertAt($assignVarIndex, new Token([T_RETURN, 'return']));
31✔
340
        ++$inserted;
31✔
341

342
        // use the original indent of the var assignment for the new return statement
343
        if (
344
            null !== $originalIndent
31✔
345
            && $tokens[$assignVarIndex - 1]->isWhitespace()
31✔
346
            && $originalIndent !== $tokens[$assignVarIndex - 1]->getContent()
31✔
347
        ) {
348
            $tokens[$assignVarIndex - 1] = new Token([T_WHITESPACE, $originalIndent]);
27✔
349
        }
350

351
        // remove trailing space after the new return statement which might be added during the cleanup process
352
        $nextIndex = $tokens->getNonEmptySibling($assignVarIndex, 1);
31✔
353
        if (!$tokens[$nextIndex]->isWhitespace()) {
31✔
354
            $tokens->insertAt($nextIndex, new Token([T_WHITESPACE, ' ']));
30✔
355
            ++$inserted;
30✔
356
        }
357

358
        return $inserted;
31✔
359
    }
360

361
    private function clearIfSave(Tokens $tokens, int $index): void
362
    {
363
        if ($tokens[$index]->isComment()) {
31✔
364
            return;
3✔
365
        }
366

367
        if ($tokens[$index]->isWhitespace() && $tokens[$tokens->getPrevNonWhitespace($index)]->isComment()) {
31✔
368
            return;
4✔
369
        }
370

371
        $tokens->clearTokenAndMergeSurroundingWhitespace($index);
31✔
372
    }
373

374
    /**
375
     * @param int $index open brace index
376
     *
377
     * @return null|int index of the first token of a definition (lambda, anonymous class or match) or `null` if not an anonymous
378
     */
379
    private function isCloseBracePartOfDefinition(Tokens $tokens, int $index): ?int
380
    {
381
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
7✔
382
        $candidateIndex = $this->isOpenBraceOfLambda($tokens, $index);
7✔
383

384
        if (null !== $candidateIndex) {
7✔
385
            return $candidateIndex;
1✔
386
        }
387

388
        $candidateIndex = $this->isOpenBraceOfAnonymousClass($tokens, $index);
6✔
389

390
        return $candidateIndex ?? $this->isOpenBraceOfMatch($tokens, $index);
6✔
391
    }
392

393
    /**
394
     * @param int $index open brace index
395
     *
396
     * @return null|int index of T_NEW of anonymous class or `null` if not an anonymous
397
     */
398
    private function isOpenBraceOfAnonymousClass(Tokens $tokens, int $index): ?int
399
    {
400
        do {
401
            $index = $tokens->getPrevMeaningfulToken($index);
6✔
402
        } while ($tokens[$index]->equalsAny([',', [T_STRING], [T_IMPLEMENTS], [T_EXTENDS], [T_NS_SEPARATOR]]));
6✔
403

404
        if ($tokens[$index]->equals(')')) { // skip constructor braces and content within
6✔
405
            $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
3✔
406
            $index = $tokens->getPrevMeaningfulToken($index);
3✔
407
        }
408

409
        if (!$tokens[$index]->isGivenKind(T_CLASS) || !$this->tokensAnalyzer->isAnonymousClass($index)) {
6✔
410
            return null;
2✔
411
        }
412

413
        return $tokens->getPrevTokenOfKind($index, [[T_NEW]]);
4✔
414
    }
415

416
    /**
417
     * @param int $index open brace index
418
     *
419
     * @return null|int index of T_FUNCTION or T_STATIC of lambda or `null` if not a lambda
420
     */
421
    private function isOpenBraceOfLambda(Tokens $tokens, int $index): ?int
422
    {
423
        $index = $tokens->getPrevMeaningfulToken($index);
7✔
424

425
        if (!$tokens[$index]->equals(')')) {
7✔
426
            return null;
4✔
427
        }
428

429
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
4✔
430
        $index = $tokens->getPrevMeaningfulToken($index);
4✔
431

432
        if ($tokens[$index]->isGivenKind(CT::T_USE_LAMBDA)) {
4✔
433
            $index = $tokens->getPrevTokenOfKind($index, [')']);
1✔
434
            $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
1✔
435
            $index = $tokens->getPrevMeaningfulToken($index);
1✔
436
        }
437

438
        if ($tokens[$index]->isGivenKind(CT::T_RETURN_REF)) {
4✔
439
            $index = $tokens->getPrevMeaningfulToken($index);
1✔
440
        }
441

442
        if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
4✔
443
            return null;
3✔
444
        }
445

446
        $staticCandidate = $tokens->getPrevMeaningfulToken($index);
1✔
447

448
        return $tokens[$staticCandidate]->isGivenKind(T_STATIC) ? $staticCandidate : $index;
1✔
449
    }
450

451
    /**
452
     * @param int $index open brace index
453
     *
454
     * @return null|int index of T_MATCH or `null` if not a `match`
455
     */
456
    private function isOpenBraceOfMatch(Tokens $tokens, int $index): ?int
457
    {
458
        if (!$tokens->isTokenKindFound(FCT::T_MATCH)) {
2✔
459
            return null;
1✔
460
        }
461

462
        $index = $tokens->getPrevMeaningfulToken($index);
1✔
463

464
        if (!$tokens[$index]->equals(')')) {
1✔
465
            return null;
×
466
        }
467

468
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
1✔
469
        $index = $tokens->getPrevMeaningfulToken($index);
1✔
470

471
        return $tokens[$index]->isGivenKind(T_MATCH) ? $index : null;
1✔
472
    }
473

474
    private function isUsedInCatchOrFinally(Tokens $tokens, int $returnVarIndex, int $functionOpenIndex, int $functionCloseIndex): bool
475
    {
476
        // Find try
477
        $tryIndex = $tokens->getPrevTokenOfKind($returnVarIndex, [[T_TRY]]);
35✔
478
        if (null === $tryIndex || $tryIndex <= $functionOpenIndex) {
35✔
479
            return false;
29✔
480
        }
481
        $tryOpenIndex = $tokens->getNextTokenOfKind($tryIndex, ['{']);
8✔
482
        $tryCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tryOpenIndex);
8✔
483

484
        // Find catch or finally
485
        $nextIndex = $tokens->getNextMeaningfulToken($tryCloseIndex);
8✔
486
        if (null === $nextIndex) {
8✔
487
            return false;
×
488
        }
489

490
        // Find catches
491
        while ($tokens[$nextIndex]->isGivenKind(T_CATCH)) {
8✔
492
            $catchOpenIndex = $tokens->getNextTokenOfKind($nextIndex, ['{']);
8✔
493
            $catchCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $catchOpenIndex);
8✔
494

495
            if ($catchCloseIndex >= $functionCloseIndex) {
8✔
496
                return false;
×
497
            }
498
            $varIndex = $tokens->getNextTokenOfKind($catchOpenIndex, [$tokens[$returnVarIndex]]);
8✔
499
            // Check if the variable is used in the finally block
500
            if (null !== $varIndex && $varIndex < $catchCloseIndex) {
8✔
501
                return true;
3✔
502
            }
503

504
            $nextIndex = $tokens->getNextMeaningfulToken($catchCloseIndex);
7✔
505
            if (null === $nextIndex) {
7✔
506
                return false;
×
507
            }
508
        }
509

510
        if (!$tokens[$nextIndex]->isGivenKind(T_FINALLY)) {
6✔
511
            return false;
2✔
512
        }
513

514
        $finallyIndex = $nextIndex;
5✔
515
        if ($finallyIndex >= $functionCloseIndex) {
5✔
516
            return false;
×
517
        }
518
        $finallyOpenIndex = $tokens->getNextTokenOfKind($finallyIndex, ['{']);
5✔
519
        $finallyCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $finallyOpenIndex);
5✔
520
        $varIndex = $tokens->getNextTokenOfKind($finallyOpenIndex, [$tokens[$returnVarIndex]]);
5✔
521
        // Check if the variable is used in the finally block
522
        if (null !== $varIndex && $varIndex < $finallyCloseIndex) {
5✔
523
            return true;
5✔
524
        }
525

526
        return false;
×
527
    }
528
}
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