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

keradus / PHP-CS-Fixer / 16303177127

15 Jul 2025 06:22PM UTC coverage: 94.758% (-0.05%) from 94.806%
16303177127

push

github

keradus
bumped version

28199 of 29759 relevant lines covered (94.76%)

45.91 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
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
        $inserted = 0;
63✔
107
        $candidates = [];
63✔
108
        $isRisky = false;
63✔
109

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

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

120
                break;
4✔
121
            }
122
        }
123

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

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

135
                    continue;
×
136
                }
137

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

140
                $tokensAdded = $this->fixFunction(
5✔
141
                    $tokens,
5✔
142
                    $index,
5✔
143
                    $nestedFunctionOpenIndex,
5✔
144
                    $nestedFunctionCloseIndex
5✔
145
                );
5✔
146

147
                $index = $nestedFunctionCloseIndex + $tokensAdded;
5✔
148
                $functionCloseIndex += $tokensAdded;
5✔
149
                $inserted += $tokensAdded;
5✔
150
            }
151

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

156
            if ($tokens[$index]->equals('&')) {
61✔
157
                $isRisky = true;
1✔
158

159
                continue;
1✔
160
            }
161

162
            if ($tokens[$index]->isGivenKind(\T_RETURN)) {
61✔
163
                $candidates[] = $index;
49✔
164

165
                continue;
49✔
166
            }
167

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

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

182
                continue;
8✔
183
            }
184

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

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

191
                    continue;
1✔
192
                }
193
            }
194

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

200
                    continue;
1✔
201
                }
202
            }
203

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

207
                continue;
2✔
208
            }
209
        }
210

211
        if ($isRisky) {
63✔
212
            return $inserted;
16✔
213
        }
214

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

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

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

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

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

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

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

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

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

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

259
                $assignVarOperatorIndex = $tokens->getPrevMeaningfulToken($startIndex);
6✔
260
            }
261

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

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

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

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

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

293
        return $inserted;
49✔
294
    }
295

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

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

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

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

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

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

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

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

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

356
        return $inserted;
31✔
357
    }
358

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

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

369
        $tokens->clearTokenAndMergeSurroundingWhitespace($index);
31✔
370
    }
371

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

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

386
        $candidateIndex = $this->isOpenBraceOfAnonymousClass($tokens, $index);
6✔
387

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

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

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

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

411
        return $tokens->getPrevTokenOfKind($index, [[\T_NEW]]);
4✔
412
    }
413

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

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

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

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

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

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

444
        $staticCandidate = $tokens->getPrevMeaningfulToken($index);
1✔
445

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

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

460
        $index = $tokens->getPrevMeaningfulToken($index);
1✔
461

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

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

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

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

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

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

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

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

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

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

524
        return false;
×
525
    }
526
}
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