123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- <?php
- /*
- * This file is part of PHP CS Fixer.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- * Dariusz Rumiński <dariusz.ruminski@gmail.com>
- *
- * This source file is subject to the MIT license that is bundled
- * with this source code in the file LICENSE.
- */
- namespace PhpCsFixer\Fixer\ReturnNotation;
- use PhpCsFixer\AbstractFixer;
- use PhpCsFixer\FixerDefinition\CodeSample;
- use PhpCsFixer\FixerDefinition\FixerDefinition;
- use PhpCsFixer\Tokenizer\CT;
- use PhpCsFixer\Tokenizer\Token;
- use PhpCsFixer\Tokenizer\Tokens;
- /**
- * @author SpacePossum
- */
- final class ReturnAssignmentFixer extends AbstractFixer
- {
- /**
- * {@inheritdoc}
- */
- public function getDefinition()
- {
- return new FixerDefinition(
- 'Local, dynamic and directly referenced variables should not be assigned and directly returned by a function or method.',
- [new CodeSample("<?php\nfunction a() {\n \$a = 1;\n return \$a;\n}\n")]
- );
- }
- /**
- * {@inheritdoc}
- *
- * Must run before BlankLineBeforeStatementFixer.
- * Must run after NoEmptyStatementFixer.
- */
- public function getPriority()
- {
- return -15;
- }
- /**
- * {@inheritdoc}
- */
- public function isCandidate(Tokens $tokens)
- {
- return $tokens->isAllTokenKindsFound([T_FUNCTION, T_RETURN, T_VARIABLE]);
- }
- /**
- * {@inheritdoc}
- */
- protected function applyFix(\SplFileInfo $file, Tokens $tokens)
- {
- $tokenCount = \count($tokens);
- for ($index = 1; $index < $tokenCount; ++$index) {
- if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
- continue;
- }
- $functionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
- if ($tokens[$functionOpenIndex]->equals(';')) { // abstract function
- $index = $functionOpenIndex - 1;
- continue;
- }
- $functionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $functionOpenIndex);
- $totalTokensAdded = 0;
- do {
- $tokensAdded = $this->fixFunction(
- $tokens,
- $index,
- $functionOpenIndex,
- $functionCloseIndex
- );
- $totalTokensAdded += $tokensAdded;
- } while ($tokensAdded > 0);
- $index = $functionCloseIndex + $totalTokensAdded;
- $tokenCount += $totalTokensAdded;
- }
- }
- /**
- * @param int $functionIndex token index of T_FUNCTION
- * @param int $functionOpenIndex token index of the opening brace token of the function
- * @param int $functionCloseIndex token index of the closing brace token of the function
- *
- * @return int >= 0 number of tokens inserted into the Tokens collection
- */
- private function fixFunction(Tokens $tokens, $functionIndex, $functionOpenIndex, $functionCloseIndex)
- {
- static $riskyKinds = [
- CT::T_DYNAMIC_VAR_BRACE_OPEN, // "$h = ${$g};" case
- T_EVAL, // "$c = eval('return $this;');" case
- T_GLOBAL,
- T_INCLUDE, // loading additional symbols we cannot analyze here
- T_INCLUDE_ONCE, // "
- T_REQUIRE, // "
- T_REQUIRE_ONCE, // "
- T_STATIC,
- ];
- $inserted = 0;
- $candidates = [];
- $isRisky = false;
- // go through the function declaration and check if references are passed
- // - check if it will be risky to fix return statements of this function
- for ($index = $functionIndex + 1; $index < $functionOpenIndex; ++$index) {
- if ($tokens[$index]->equals('&')) {
- $isRisky = true;
- break;
- }
- }
- // go through all the tokens of the body of the function:
- // - check if it will be risky to fix return statements of this function
- // - check nested functions; fix when found and update the upper limit + number of inserted token
- // - check for return statements that might be fixed (based on if fixing will be risky, which is only know after analyzing the whole function)
- for ($index = $functionOpenIndex + 1; $index < $functionCloseIndex; ++$index) {
- if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
- $nestedFunctionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
- if ($tokens[$nestedFunctionOpenIndex]->equals(';')) { // abstract function
- $index = $nestedFunctionOpenIndex - 1;
- continue;
- }
- $nestedFunctionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nestedFunctionOpenIndex);
- $tokensAdded = $this->fixFunction(
- $tokens,
- $index,
- $nestedFunctionOpenIndex,
- $nestedFunctionCloseIndex
- );
- $index = $nestedFunctionCloseIndex + $tokensAdded;
- $functionCloseIndex += $tokensAdded;
- $inserted += $tokensAdded;
- }
- if ($isRisky) {
- continue; // don't bother to look into anything else than nested functions as the current is risky already
- }
- if ($tokens[$index]->equals('&')) {
- $isRisky = true;
- continue;
- }
- if ($tokens[$index]->isGivenKind(T_RETURN)) {
- $candidates[] = $index;
- continue;
- }
- // test if there this is anything in the function body that might
- // change global state or indirect changes (like through references, eval, etc.)
- if ($tokens[$index]->isGivenKind($riskyKinds)) {
- $isRisky = true;
- continue;
- }
- if ($tokens[$index]->equals('$')) {
- $nextIndex = $tokens->getNextMeaningfulToken($index);
- if ($tokens[$nextIndex]->isGivenKind(T_VARIABLE)) {
- $isRisky = true; // "$$a" case
- continue;
- }
- }
- if ($this->isSuperGlobal($tokens[$index])) {
- $isRisky = true;
- continue;
- }
- }
- if ($isRisky) {
- return $inserted;
- }
- // fix the candidates in reverse order when applicable
- for ($i = \count($candidates) - 1; $i >= 0; --$i) {
- $index = $candidates[$i];
- // Check if returning only a variable (i.e. not the result of an expression, function call etc.)
- $returnVarIndex = $tokens->getNextMeaningfulToken($index);
- if (!$tokens[$returnVarIndex]->isGivenKind(T_VARIABLE)) {
- continue; // example: "return 1;"
- }
- $endReturnVarIndex = $tokens->getNextMeaningfulToken($returnVarIndex);
- if (!$tokens[$endReturnVarIndex]->equalsAny([';', [T_CLOSE_TAG]])) {
- continue; // example: "return $a + 1;"
- }
- // Check that the variable is assigned just before it is returned
- $assignVarEndIndex = $tokens->getPrevMeaningfulToken($index);
- if (!$tokens[$assignVarEndIndex]->equals(';')) {
- continue; // example: "? return $a;"
- }
- // Note: here we are @ "; return $a;" (or "; return $a ? >")
- $assignVarOperatorIndex = $tokens->getPrevTokenOfKind(
- $assignVarEndIndex,
- ['=', ';', '{', [T_OPEN_TAG], [T_OPEN_TAG_WITH_ECHO]]
- );
- if (null === $assignVarOperatorIndex || !$tokens[$assignVarOperatorIndex]->equals('=')) {
- continue;
- }
- // Note: here we are @ "= [^;{<? ? >] ; return $a;"
- $assignVarIndex = $tokens->getPrevMeaningfulToken($assignVarOperatorIndex);
- if (!$tokens[$assignVarIndex]->equals($tokens[$returnVarIndex], false)) {
- continue;
- }
- // Note: here we are @ "$a = [^;{<? ? >] ; return $a;"
- $beforeAssignVarIndex = $tokens->getPrevMeaningfulToken($assignVarIndex);
- if (!$tokens[$beforeAssignVarIndex]->equalsAny([';', '{', '}'])) {
- continue;
- }
- // Note: here we are @ "[;{}] $a = [^;{<? ? >] ; return $a;"
- $inserted += $this->simplifyReturnStatement(
- $tokens,
- $assignVarIndex,
- $assignVarOperatorIndex,
- $index,
- $endReturnVarIndex
- );
- }
- return $inserted;
- }
- /**
- * @param int $assignVarIndex
- * @param int $assignVarOperatorIndex
- * @param int $returnIndex
- * @param int $returnVarEndIndex
- *
- * @return int >= 0 number of tokens inserted into the Tokens collection
- */
- private function simplifyReturnStatement(
- Tokens $tokens,
- $assignVarIndex,
- $assignVarOperatorIndex,
- $returnIndex,
- $returnVarEndIndex
- ) {
- $inserted = 0;
- $originalIndent = $tokens[$assignVarIndex - 1]->isWhitespace()
- ? $tokens[$assignVarIndex - 1]->getContent()
- : null
- ;
- // remove the return statement
- if ($tokens[$returnVarEndIndex]->equals(';')) { // do not remove PHP close tags
- $tokens->clearTokenAndMergeSurroundingWhitespace($returnVarEndIndex);
- }
- for ($i = $returnIndex; $i <= $returnVarEndIndex - 1; ++$i) {
- $this->clearIfSave($tokens, $i);
- }
- // remove no longer needed indentation of the old/remove return statement
- if ($tokens[$returnIndex - 1]->isWhitespace()) {
- $content = $tokens[$returnIndex - 1]->getContent();
- $fistLinebreakPos = strrpos($content, "\n");
- $content = false === $fistLinebreakPos
- ? ' '
- : substr($content, $fistLinebreakPos)
- ;
- $tokens[$returnIndex - 1] = new Token([T_WHITESPACE, $content]);
- }
- // remove the variable and the assignment
- for ($i = $assignVarIndex; $i <= $assignVarOperatorIndex; ++$i) {
- $this->clearIfSave($tokens, $i);
- }
- // insert new return statement
- $tokens->insertAt($assignVarIndex, new Token([T_RETURN, 'return']));
- ++$inserted;
- // use the original indent of the var assignment for the new return statement
- if (
- null !== $originalIndent
- && $tokens[$assignVarIndex - 1]->isWhitespace()
- && $originalIndent !== $tokens[$assignVarIndex - 1]->getContent()
- ) {
- $tokens[$assignVarIndex - 1] = new Token([T_WHITESPACE, $originalIndent]);
- }
- // remove trailing space after the new return statement which might be added during the clean up process
- $nextIndex = $tokens->getNonEmptySibling($assignVarIndex, 1);
- if (!$tokens[$nextIndex]->isWhitespace()) {
- $tokens->insertAt($nextIndex, new Token([T_WHITESPACE, ' ']));
- ++$inserted;
- }
- return $inserted;
- }
- private function clearIfSave(Tokens $tokens, $index)
- {
- if ($tokens[$index]->isComment()) {
- return;
- }
- if ($tokens[$index]->isWhitespace() && $tokens[$tokens->getPrevNonWhitespace($index)]->isComment()) {
- return;
- }
- $tokens->clearTokenAndMergeSurroundingWhitespace($index);
- }
- /**
- * @return bool
- */
- private function isSuperGlobal(Token $token)
- {
- static $superNames = [
- '$_COOKIE' => true,
- '$_ENV' => true,
- '$_FILES' => true,
- '$_GET' => true,
- '$_POST' => true,
- '$_REQUEST' => true,
- '$_SERVER' => true,
- '$_SESSION' => true,
- '$GLOBALS' => true,
- ];
- if (!$token->isGivenKind(T_VARIABLE)) {
- return false;
- }
- return isset($superNames[strtoupper($token->getContent())]);
- }
- }
|