ReturnAssignmentFixer::fixFunction()   F
last analyzed

Complexity

Conditions 24
Paths 780

Size

Total Lines 164
Code Lines 87

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 87
c 1
b 0
f 0
dl 0
loc 164
rs 0.3055
cc 24
nc 780
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <[email protected]>
9
 *     Dariusz Rumiński <[email protected]>
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\Token;
23
use PhpCsFixer\Tokenizer\Tokens;
24
use PhpCsFixer\Tokenizer\TokensAnalyzer;
25
26
/**
27
 * @author SpacePossum
28
 */
29
final class ReturnAssignmentFixer extends AbstractFixer
30
{
31
    /**
32
     * @var TokensAnalyzer
33
     */
34
    private $tokensAnalyzer;
35
36
    /**
37
     * {@inheritdoc}
38
     */
39
    public function getDefinition(): FixerDefinitionInterface
40
    {
41
        return new FixerDefinition(
42
            'Local, dynamic and directly referenced variables should not be assigned and directly returned by a function or method.',
43
            [new CodeSample("<?php\nfunction a() {\n    \$a = 1;\n    return \$a;\n}\n")]
44
        );
45
    }
46
47
    /**
48
     * {@inheritdoc}
49
     *
50
     * Must run before BlankLineBeforeStatementFixer.
51
     * Must run after NoEmptyStatementFixer, NoUnneededCurlyBracesFixer.
52
     */
53
    public function getPriority(): int
54
    {
55
        return -15;
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function isCandidate(Tokens $tokens): bool
62
    {
63
        return $tokens->isAllTokenKindsFound([T_FUNCTION, T_RETURN, T_VARIABLE]);
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
70
    {
71
        $tokenCount = \count($tokens);
72
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
73
74
        for ($index = 1; $index < $tokenCount; ++$index) {
75
            if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
76
                continue;
77
            }
78
79
            $functionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
80
            if ($tokens[$functionOpenIndex]->equals(';')) { // abstract function
81
                $index = $functionOpenIndex - 1;
82
83
                continue;
84
            }
85
86
            $functionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $functionOpenIndex);
0 ignored issues
show
Bug introduced by
It seems like $functionOpenIndex can also be of type null; however, parameter $searchIndex of PhpCsFixer\Tokenizer\Tokens::findBlockEnd() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

86
            $functionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, /** @scrutinizer ignore-type */ $functionOpenIndex);
Loading history...
87
            $totalTokensAdded = 0;
88
89
            do {
90
                $tokensAdded = $this->fixFunction(
91
                    $tokens,
92
                    $index,
93
                    $functionOpenIndex,
0 ignored issues
show
Bug introduced by
It seems like $functionOpenIndex can also be of type null; however, parameter $functionOpenIndex of PhpCsFixer\Fixer\ReturnN...entFixer::fixFunction() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
                    /** @scrutinizer ignore-type */ $functionOpenIndex,
Loading history...
94
                    $functionCloseIndex
95
                );
96
97
                $totalTokensAdded += $tokensAdded;
98
            } while ($tokensAdded > 0);
99
100
            $index = $functionCloseIndex + $totalTokensAdded;
101
            $tokenCount += $totalTokensAdded;
102
        }
103
    }
104
105
    /**
106
     * @param int $functionIndex      token index of T_FUNCTION
107
     * @param int $functionOpenIndex  token index of the opening brace token of the function
108
     * @param int $functionCloseIndex token index of the closing brace token of the function
109
     *
110
     * @return int >= 0 number of tokens inserted into the Tokens collection
111
     */
112
    private function fixFunction(Tokens $tokens, int $functionIndex, int $functionOpenIndex, int $functionCloseIndex): int
113
    {
114
        static $riskyKinds = [
115
            CT::T_DYNAMIC_VAR_BRACE_OPEN, // "$h = ${$g};" case
116
            T_EVAL,                       // "$c = eval('return $this;');" case
117
            T_GLOBAL,
118
            T_INCLUDE,                    // loading additional symbols we cannot analyze here
119
            T_INCLUDE_ONCE,               // "
120
            T_REQUIRE,                    // "
121
            T_REQUIRE_ONCE,               // "
122
            T_STATIC,
123
        ];
124
125
        $inserted = 0;
126
        $candidates = [];
127
        $isRisky = false;
128
129
        // go through the function declaration and check if references are passed
130
        // - check if it will be risky to fix return statements of this function
131
        for ($index = $functionIndex + 1; $index < $functionOpenIndex; ++$index) {
132
            if ($tokens[$index]->equals('&')) {
133
                $isRisky = true;
134
135
                break;
136
            }
137
        }
138
139
        // go through all the tokens of the body of the function:
140
        // - check if it will be risky to fix return statements of this function
141
        // - check nested functions; fix when found and update the upper limit + number of inserted token
142
        // - check for return statements that might be fixed (based on if fixing will be risky, which is only know after analyzing the whole function)
143
144
        for ($index = $functionOpenIndex + 1; $index < $functionCloseIndex; ++$index) {
145
            if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
146
                $nestedFunctionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
147
                if ($tokens[$nestedFunctionOpenIndex]->equals(';')) { // abstract function
148
                    $index = $nestedFunctionOpenIndex - 1;
149
150
                    continue;
151
                }
152
153
                $nestedFunctionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nestedFunctionOpenIndex);
0 ignored issues
show
Bug introduced by
It seems like $nestedFunctionOpenIndex can also be of type null; however, parameter $searchIndex of PhpCsFixer\Tokenizer\Tokens::findBlockEnd() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

153
                $nestedFunctionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, /** @scrutinizer ignore-type */ $nestedFunctionOpenIndex);
Loading history...
154
155
                $tokensAdded = $this->fixFunction(
156
                    $tokens,
157
                    $index,
158
                    $nestedFunctionOpenIndex,
0 ignored issues
show
Bug introduced by
It seems like $nestedFunctionOpenIndex can also be of type null; however, parameter $functionOpenIndex of PhpCsFixer\Fixer\ReturnN...entFixer::fixFunction() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
                    /** @scrutinizer ignore-type */ $nestedFunctionOpenIndex,
Loading history...
159
                    $nestedFunctionCloseIndex
160
                );
161
162
                $index = $nestedFunctionCloseIndex + $tokensAdded;
163
                $functionCloseIndex += $tokensAdded;
164
                $inserted += $tokensAdded;
165
            }
166
167
            if ($isRisky) {
168
                continue; // don't bother to look into anything else than nested functions as the current is risky already
169
            }
170
171
            if ($tokens[$index]->equals('&')) {
172
                $isRisky = true;
173
174
                continue;
175
            }
176
177
            if ($tokens[$index]->isGivenKind(T_RETURN)) {
178
                $candidates[] = $index;
179
180
                continue;
181
            }
182
183
            // test if there this is anything in the function body that might
184
            // change global state or indirect changes (like through references, eval, etc.)
185
186
            if ($tokens[$index]->isGivenKind($riskyKinds)) {
187
                $isRisky = true;
188
189
                continue;
190
            }
191
192
            if ($tokens[$index]->equals('$')) {
193
                $nextIndex = $tokens->getNextMeaningfulToken($index);
194
                if ($tokens[$nextIndex]->isGivenKind(T_VARIABLE)) {
195
                    $isRisky = true; // "$$a" case
196
197
                    continue;
198
                }
199
            }
200
201
            if ($this->tokensAnalyzer->isSuperGlobal($index)) {
202
                $isRisky = true;
203
204
                continue;
205
            }
206
        }
207
208
        if ($isRisky) {
209
            return $inserted;
210
        }
211
212
        // fix the candidates in reverse order when applicable
213
        for ($i = \count($candidates) - 1; $i >= 0; --$i) {
214
            $index = $candidates[$i];
215
216
            // Check if returning only a variable (i.e. not the result of an expression, function call etc.)
217
            $returnVarIndex = $tokens->getNextMeaningfulToken($index);
218
            if (!$tokens[$returnVarIndex]->isGivenKind(T_VARIABLE)) {
219
                continue; // example: "return 1;"
220
            }
221
222
            $endReturnVarIndex = $tokens->getNextMeaningfulToken($returnVarIndex);
0 ignored issues
show
Bug introduced by
It seems like $returnVarIndex can also be of type null; however, parameter $index of PhpCsFixer\Tokenizer\Tok...etNextMeaningfulToken() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

222
            $endReturnVarIndex = $tokens->getNextMeaningfulToken(/** @scrutinizer ignore-type */ $returnVarIndex);
Loading history...
223
            if (!$tokens[$endReturnVarIndex]->equalsAny([';', [T_CLOSE_TAG]])) {
224
                continue; // example: "return $a + 1;"
225
            }
226
227
            // Check that the variable is assigned just before it is returned
228
            $assignVarEndIndex = $tokens->getPrevMeaningfulToken($index);
229
            if (!$tokens[$assignVarEndIndex]->equals(';')) {
230
                continue; // example: "? return $a;"
231
            }
232
233
            // Note: here we are @ "; return $a;" (or "; return $a ? >")
234
            do {
235
                $prevMeaningFul = $tokens->getPrevMeaningfulToken($assignVarEndIndex);
0 ignored issues
show
Bug introduced by
It seems like $assignVarEndIndex can also be of type null; however, parameter $index of PhpCsFixer\Tokenizer\Tok...etPrevMeaningfulToken() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

235
                $prevMeaningFul = $tokens->getPrevMeaningfulToken(/** @scrutinizer ignore-type */ $assignVarEndIndex);
Loading history...
236
237
                if (!$tokens[$prevMeaningFul]->equals(')')) {
238
                    break;
239
                }
240
241
                $assignVarEndIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $prevMeaningFul);
0 ignored issues
show
Bug introduced by
It seems like $prevMeaningFul can also be of type null; however, parameter $searchIndex of PhpCsFixer\Tokenizer\Tokens::findBlockStart() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

241
                $assignVarEndIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, /** @scrutinizer ignore-type */ $prevMeaningFul);
Loading history...
242
            } while (true);
243
244
            $assignVarOperatorIndex = $tokens->getPrevTokenOfKind(
245
                $assignVarEndIndex,
0 ignored issues
show
Bug introduced by
It seems like $assignVarEndIndex can also be of type null; however, parameter $index of PhpCsFixer\Tokenizer\Tokens::getPrevTokenOfKind() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

245
                /** @scrutinizer ignore-type */ $assignVarEndIndex,
Loading history...
246
                ['=', ';', '{', [T_OPEN_TAG], [T_OPEN_TAG_WITH_ECHO]]
247
            );
248
249
            if (null === $assignVarOperatorIndex || !$tokens[$assignVarOperatorIndex]->equals('=')) {
250
                continue;
251
            }
252
253
            // Note: here we are @ "= [^;{<? ? >] ; return $a;"
254
            $assignVarIndex = $tokens->getPrevMeaningfulToken($assignVarOperatorIndex);
255
            if (!$tokens[$assignVarIndex]->equals($tokens[$returnVarIndex], false)) {
256
                continue;
257
            }
258
259
            // Note: here we are @ "$a = [^;{<? ? >] ; return $a;"
260
            $beforeAssignVarIndex = $tokens->getPrevMeaningfulToken($assignVarIndex);
261
            if (!$tokens[$beforeAssignVarIndex]->equalsAny([';', '{', '}'])) {
262
                continue;
263
            }
264
265
            // Note: here we are @ "[;{}] $a = [^;{<? ? >] ; return $a;"
266
            $inserted += $this->simplifyReturnStatement(
267
                $tokens,
268
                $assignVarIndex,
0 ignored issues
show
Bug introduced by
It seems like $assignVarIndex can also be of type null; however, parameter $assignVarIndex of PhpCsFixer\Fixer\ReturnN...mplifyReturnStatement() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

268
                /** @scrutinizer ignore-type */ $assignVarIndex,
Loading history...
269
                $assignVarOperatorIndex,
270
                $index,
271
                $endReturnVarIndex
0 ignored issues
show
Bug introduced by
It seems like $endReturnVarIndex can also be of type null; however, parameter $returnVarEndIndex of PhpCsFixer\Fixer\ReturnN...mplifyReturnStatement() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

271
                /** @scrutinizer ignore-type */ $endReturnVarIndex
Loading history...
272
            );
273
        }
274
275
        return $inserted;
276
    }
277
278
    /**
279
     * @return int >= 0 number of tokens inserted into the Tokens collection
280
     */
281
    private function simplifyReturnStatement(
282
        Tokens $tokens,
283
        int $assignVarIndex,
284
        int $assignVarOperatorIndex,
285
        int $returnIndex,
286
        int $returnVarEndIndex
287
    ): int {
288
        $inserted = 0;
289
        $originalIndent = $tokens[$assignVarIndex - 1]->isWhitespace()
290
            ? $tokens[$assignVarIndex - 1]->getContent()
291
            : null
292
        ;
293
294
        // remove the return statement
295
        if ($tokens[$returnVarEndIndex]->equals(';')) { // do not remove PHP close tags
296
            $tokens->clearTokenAndMergeSurroundingWhitespace($returnVarEndIndex);
297
        }
298
299
        for ($i = $returnIndex; $i <= $returnVarEndIndex - 1; ++$i) {
300
            $this->clearIfSave($tokens, $i);
301
        }
302
303
        // remove no longer needed indentation of the old/remove return statement
304
        if ($tokens[$returnIndex - 1]->isWhitespace()) {
305
            $content = $tokens[$returnIndex - 1]->getContent();
306
            $fistLinebreakPos = strrpos($content, "\n");
307
            $content = false === $fistLinebreakPos
308
                ? ' '
309
                : substr($content, $fistLinebreakPos)
310
            ;
311
312
            $tokens[$returnIndex - 1] = new Token([T_WHITESPACE, $content]);
313
        }
314
315
        // remove the variable and the assignment
316
        for ($i = $assignVarIndex; $i <= $assignVarOperatorIndex; ++$i) {
317
            $this->clearIfSave($tokens, $i);
318
        }
319
320
        // insert new return statement
321
        $tokens->insertAt($assignVarIndex, new Token([T_RETURN, 'return']));
322
        ++$inserted;
323
324
        // use the original indent of the var assignment for the new return statement
325
        if (
326
            null !== $originalIndent
327
            && $tokens[$assignVarIndex - 1]->isWhitespace()
328
            && $originalIndent !== $tokens[$assignVarIndex - 1]->getContent()
329
        ) {
330
            $tokens[$assignVarIndex - 1] = new Token([T_WHITESPACE, $originalIndent]);
331
        }
332
333
        // remove trailing space after the new return statement which might be added during the clean up process
334
        $nextIndex = $tokens->getNonEmptySibling($assignVarIndex, 1);
335
        if (!$tokens[$nextIndex]->isWhitespace()) {
336
            $tokens->insertAt($nextIndex, new Token([T_WHITESPACE, ' ']));
0 ignored issues
show
Bug introduced by
It seems like $nextIndex can also be of type null; however, parameter $index of PhpCsFixer\Tokenizer\Tokens::insertAt() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

336
            $tokens->insertAt(/** @scrutinizer ignore-type */ $nextIndex, new Token([T_WHITESPACE, ' ']));
Loading history...
337
            ++$inserted;
338
        }
339
340
        return $inserted;
341
    }
342
343
    private function clearIfSave(Tokens $tokens, int $index): void
344
    {
345
        if ($tokens[$index]->isComment()) {
346
            return;
347
        }
348
349
        if ($tokens[$index]->isWhitespace() && $tokens[$tokens->getPrevNonWhitespace($index)]->isComment()) {
350
            return;
351
        }
352
353
        $tokens->clearTokenAndMergeSurroundingWhitespace($index);
354
    }
355
}
356