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

keradus / PHP-CS-Fixer / 17253322895

26 Aug 2025 11:52PM UTC coverage: 94.753% (+0.008%) from 94.745%
17253322895

push

github

keradus
add to git-blame-ignore-revs

28316 of 29884 relevant lines covered (94.75%)

45.64 hits per line

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

96.14
/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
/**
28
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
29
 */
30
final class ReturnAssignmentFixer extends AbstractFixer
31
{
32
    private TokensAnalyzer $tokensAnalyzer;
33

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

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

53
    public function isCandidate(Tokens $tokens): bool
54
    {
55
        return $tokens->isAllTokenKindsFound([\T_FUNCTION, \T_RETURN, \T_VARIABLE]);
67✔
56
    }
57

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

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

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

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

77
                continue;
1✔
78
            }
79

80
            $functionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $functionOpenIndex);
63✔
81
            $totalTokensAdded = 0;
63✔
82

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

91
                $functionCloseIndex += $tokensAdded;
63✔
92
                $totalTokensAdded += $tokensAdded;
63✔
93
            } while ($tokensAdded > 0);
63✔
94

95
            $index = $functionCloseIndex;
63✔
96
            $tokenCount += $totalTokensAdded;
63✔
97
        }
98
    }
99

100
    /**
101
     * @param int $functionIndex      token index of T_FUNCTION
102
     * @param int $functionOpenIndex  token index of the opening brace token of the function
103
     * @param int $functionCloseIndex token index of the closing brace token of the function
104
     *
105
     * @return int >= 0 number of tokens inserted into the Tokens collection
106
     */
107
    private function fixFunction(Tokens $tokens, int $functionIndex, int $functionOpenIndex, int $functionCloseIndex): int
108
    {
109
        $inserted = 0;
63✔
110
        $candidates = [];
63✔
111
        $isRisky = false;
63✔
112

113
        if ($tokens[$tokens->getNextMeaningfulToken($functionIndex)]->isGivenKind(CT::T_RETURN_REF)) {
63✔
114
            $isRisky = true;
1✔
115
        }
116

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

123
                break;
4✔
124
            }
125
        }
126

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

132
        for ($index = $functionOpenIndex + 1; $index < $functionCloseIndex; ++$index) {
63✔
133
            if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
63✔
134
                $nestedFunctionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
5✔
135
                if ($tokens[$nestedFunctionOpenIndex]->equals(';')) { // abstract function
5✔
136
                    $index = $nestedFunctionOpenIndex - 1;
×
137

138
                    continue;
×
139
                }
140

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

143
                $tokensAdded = $this->fixFunction(
5✔
144
                    $tokens,
5✔
145
                    $index,
5✔
146
                    $nestedFunctionOpenIndex,
5✔
147
                    $nestedFunctionCloseIndex
5✔
148
                );
5✔
149

150
                $index = $nestedFunctionCloseIndex + $tokensAdded;
5✔
151
                $functionCloseIndex += $tokensAdded;
5✔
152
                $inserted += $tokensAdded;
5✔
153
            }
154

155
            if ($isRisky) {
63✔
156
                continue; // don't bother to look into anything else than nested functions as the current is risky already
16✔
157
            }
158

159
            if ($tokens[$index]->equals('&')) {
61✔
160
                $isRisky = true;
1✔
161

162
                continue;
1✔
163
            }
164

165
            if ($tokens[$index]->isGivenKind(\T_RETURN)) {
61✔
166
                $candidates[] = $index;
49✔
167

168
                continue;
49✔
169
            }
170

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

174
            if ($tokens[$index]->isGivenKind([
61✔
175
                CT::T_DYNAMIC_VAR_BRACE_OPEN, // "$h = ${$g};" case
61✔
176
                \T_EVAL,                       // "$c = eval('return $this;');" case
61✔
177
                \T_GLOBAL,
61✔
178
                \T_INCLUDE,                    // loading additional symbols we cannot analyze here
61✔
179
                \T_INCLUDE_ONCE,               // "
61✔
180
                \T_REQUIRE,                    // "
61✔
181
                \T_REQUIRE_ONCE,               // "
61✔
182
            ])) {
61✔
183
                $isRisky = true;
8✔
184

185
                continue;
8✔
186
            }
187

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

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

194
                    continue;
1✔
195
                }
196
            }
197

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

203
                    continue;
1✔
204
                }
205
            }
206

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

210
                continue;
2✔
211
            }
212
        }
213

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

296
        return $inserted;
49✔
297
    }
298

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

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

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

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

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

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

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

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

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

359
        return $inserted;
31✔
360
    }
361

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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