ReturnAssignmentFixer.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer\Fixer\ReturnNotation;
  12. use PhpCsFixer\AbstractFixer;
  13. use PhpCsFixer\FixerDefinition\CodeSample;
  14. use PhpCsFixer\FixerDefinition\FixerDefinition;
  15. use PhpCsFixer\Tokenizer\CT;
  16. use PhpCsFixer\Tokenizer\Token;
  17. use PhpCsFixer\Tokenizer\Tokens;
  18. /**
  19. * @author SpacePossum
  20. */
  21. final class ReturnAssignmentFixer extends AbstractFixer
  22. {
  23. /**
  24. * {@inheritdoc}
  25. */
  26. public function getDefinition()
  27. {
  28. return new FixerDefinition(
  29. 'Local, dynamic and directly referenced variables should not be assigned and directly returned by a function or method.',
  30. [new CodeSample("<?php\nfunction a() {\n \$a = 1;\n return \$a;\n}\n")]
  31. );
  32. }
  33. /**
  34. * {@inheritdoc}
  35. *
  36. * Must run before BlankLineBeforeStatementFixer.
  37. * Must run after NoEmptyStatementFixer.
  38. */
  39. public function getPriority()
  40. {
  41. return -15;
  42. }
  43. /**
  44. * {@inheritdoc}
  45. */
  46. public function isCandidate(Tokens $tokens)
  47. {
  48. return $tokens->isAllTokenKindsFound([T_FUNCTION, T_RETURN, T_VARIABLE]);
  49. }
  50. /**
  51. * {@inheritdoc}
  52. */
  53. protected function applyFix(\SplFileInfo $file, Tokens $tokens)
  54. {
  55. $tokenCount = \count($tokens);
  56. for ($index = 1; $index < $tokenCount; ++$index) {
  57. if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
  58. continue;
  59. }
  60. $functionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
  61. if ($tokens[$functionOpenIndex]->equals(';')) { // abstract function
  62. $index = $functionOpenIndex - 1;
  63. continue;
  64. }
  65. $functionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $functionOpenIndex);
  66. $totalTokensAdded = 0;
  67. do {
  68. $tokensAdded = $this->fixFunction(
  69. $tokens,
  70. $index,
  71. $functionOpenIndex,
  72. $functionCloseIndex
  73. );
  74. $totalTokensAdded += $tokensAdded;
  75. } while ($tokensAdded > 0);
  76. $index = $functionCloseIndex + $totalTokensAdded;
  77. $tokenCount += $totalTokensAdded;
  78. }
  79. }
  80. /**
  81. * @param int $functionIndex token index of T_FUNCTION
  82. * @param int $functionOpenIndex token index of the opening brace token of the function
  83. * @param int $functionCloseIndex token index of the closing brace token of the function
  84. *
  85. * @return int >= 0 number of tokens inserted into the Tokens collection
  86. */
  87. private function fixFunction(Tokens $tokens, $functionIndex, $functionOpenIndex, $functionCloseIndex)
  88. {
  89. static $riskyKinds = [
  90. CT::T_DYNAMIC_VAR_BRACE_OPEN, // "$h = ${$g};" case
  91. T_EVAL, // "$c = eval('return $this;');" case
  92. T_GLOBAL,
  93. T_INCLUDE, // loading additional symbols we cannot analyze here
  94. T_INCLUDE_ONCE, // "
  95. T_REQUIRE, // "
  96. T_REQUIRE_ONCE, // "
  97. T_STATIC,
  98. ];
  99. $inserted = 0;
  100. $candidates = [];
  101. $isRisky = false;
  102. // go through the function declaration and check if references are passed
  103. // - check if it will be risky to fix return statements of this function
  104. for ($index = $functionIndex + 1; $index < $functionOpenIndex; ++$index) {
  105. if ($tokens[$index]->equals('&')) {
  106. $isRisky = true;
  107. break;
  108. }
  109. }
  110. // go through all the tokens of the body of the function:
  111. // - check if it will be risky to fix return statements of this function
  112. // - check nested functions; fix when found and update the upper limit + number of inserted token
  113. // - check for return statements that might be fixed (based on if fixing will be risky, which is only know after analyzing the whole function)
  114. for ($index = $functionOpenIndex + 1; $index < $functionCloseIndex; ++$index) {
  115. if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
  116. $nestedFunctionOpenIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
  117. if ($tokens[$nestedFunctionOpenIndex]->equals(';')) { // abstract function
  118. $index = $nestedFunctionOpenIndex - 1;
  119. continue;
  120. }
  121. $nestedFunctionCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nestedFunctionOpenIndex);
  122. $tokensAdded = $this->fixFunction(
  123. $tokens,
  124. $index,
  125. $nestedFunctionOpenIndex,
  126. $nestedFunctionCloseIndex
  127. );
  128. $index = $nestedFunctionCloseIndex + $tokensAdded;
  129. $functionCloseIndex += $tokensAdded;
  130. $inserted += $tokensAdded;
  131. }
  132. if ($isRisky) {
  133. continue; // don't bother to look into anything else than nested functions as the current is risky already
  134. }
  135. if ($tokens[$index]->equals('&')) {
  136. $isRisky = true;
  137. continue;
  138. }
  139. if ($tokens[$index]->isGivenKind(T_RETURN)) {
  140. $candidates[] = $index;
  141. continue;
  142. }
  143. // test if there this is anything in the function body that might
  144. // change global state or indirect changes (like through references, eval, etc.)
  145. if ($tokens[$index]->isGivenKind($riskyKinds)) {
  146. $isRisky = true;
  147. continue;
  148. }
  149. if ($tokens[$index]->equals('$')) {
  150. $nextIndex = $tokens->getNextMeaningfulToken($index);
  151. if ($tokens[$nextIndex]->isGivenKind(T_VARIABLE)) {
  152. $isRisky = true; // "$$a" case
  153. continue;
  154. }
  155. }
  156. if ($this->isSuperGlobal($tokens[$index])) {
  157. $isRisky = true;
  158. continue;
  159. }
  160. }
  161. if ($isRisky) {
  162. return $inserted;
  163. }
  164. // fix the candidates in reverse order when applicable
  165. for ($i = \count($candidates) - 1; $i >= 0; --$i) {
  166. $index = $candidates[$i];
  167. // Check if returning only a variable (i.e. not the result of an expression, function call etc.)
  168. $returnVarIndex = $tokens->getNextMeaningfulToken($index);
  169. if (!$tokens[$returnVarIndex]->isGivenKind(T_VARIABLE)) {
  170. continue; // example: "return 1;"
  171. }
  172. $endReturnVarIndex = $tokens->getNextMeaningfulToken($returnVarIndex);
  173. if (!$tokens[$endReturnVarIndex]->equalsAny([';', [T_CLOSE_TAG]])) {
  174. continue; // example: "return $a + 1;"
  175. }
  176. // Check that the variable is assigned just before it is returned
  177. $assignVarEndIndex = $tokens->getPrevMeaningfulToken($index);
  178. if (!$tokens[$assignVarEndIndex]->equals(';')) {
  179. continue; // example: "? return $a;"
  180. }
  181. // Note: here we are @ "; return $a;" (or "; return $a ? >")
  182. $assignVarOperatorIndex = $tokens->getPrevTokenOfKind(
  183. $assignVarEndIndex,
  184. ['=', ';', '{', [T_OPEN_TAG], [T_OPEN_TAG_WITH_ECHO]]
  185. );
  186. if (null === $assignVarOperatorIndex || !$tokens[$assignVarOperatorIndex]->equals('=')) {
  187. continue;
  188. }
  189. // Note: here we are @ "= [^;{<? ? >] ; return $a;"
  190. $assignVarIndex = $tokens->getPrevMeaningfulToken($assignVarOperatorIndex);
  191. if (!$tokens[$assignVarIndex]->equals($tokens[$returnVarIndex], false)) {
  192. continue;
  193. }
  194. // Note: here we are @ "$a = [^;{<? ? >] ; return $a;"
  195. $beforeAssignVarIndex = $tokens->getPrevMeaningfulToken($assignVarIndex);
  196. if (!$tokens[$beforeAssignVarIndex]->equalsAny([';', '{', '}'])) {
  197. continue;
  198. }
  199. // Note: here we are @ "[;{}] $a = [^;{<? ? >] ; return $a;"
  200. $inserted += $this->simplifyReturnStatement(
  201. $tokens,
  202. $assignVarIndex,
  203. $assignVarOperatorIndex,
  204. $index,
  205. $endReturnVarIndex
  206. );
  207. }
  208. return $inserted;
  209. }
  210. /**
  211. * @param int $assignVarIndex
  212. * @param int $assignVarOperatorIndex
  213. * @param int $returnIndex
  214. * @param int $returnVarEndIndex
  215. *
  216. * @return int >= 0 number of tokens inserted into the Tokens collection
  217. */
  218. private function simplifyReturnStatement(
  219. Tokens $tokens,
  220. $assignVarIndex,
  221. $assignVarOperatorIndex,
  222. $returnIndex,
  223. $returnVarEndIndex
  224. ) {
  225. $inserted = 0;
  226. $originalIndent = $tokens[$assignVarIndex - 1]->isWhitespace()
  227. ? $tokens[$assignVarIndex - 1]->getContent()
  228. : null
  229. ;
  230. // remove the return statement
  231. if ($tokens[$returnVarEndIndex]->equals(';')) { // do not remove PHP close tags
  232. $tokens->clearTokenAndMergeSurroundingWhitespace($returnVarEndIndex);
  233. }
  234. for ($i = $returnIndex; $i <= $returnVarEndIndex - 1; ++$i) {
  235. $this->clearIfSave($tokens, $i);
  236. }
  237. // remove no longer needed indentation of the old/remove return statement
  238. if ($tokens[$returnIndex - 1]->isWhitespace()) {
  239. $content = $tokens[$returnIndex - 1]->getContent();
  240. $fistLinebreakPos = strrpos($content, "\n");
  241. $content = false === $fistLinebreakPos
  242. ? ' '
  243. : substr($content, $fistLinebreakPos)
  244. ;
  245. $tokens[$returnIndex - 1] = new Token([T_WHITESPACE, $content]);
  246. }
  247. // remove the variable and the assignment
  248. for ($i = $assignVarIndex; $i <= $assignVarOperatorIndex; ++$i) {
  249. $this->clearIfSave($tokens, $i);
  250. }
  251. // insert new return statement
  252. $tokens->insertAt($assignVarIndex, new Token([T_RETURN, 'return']));
  253. ++$inserted;
  254. // use the original indent of the var assignment for the new return statement
  255. if (
  256. null !== $originalIndent
  257. && $tokens[$assignVarIndex - 1]->isWhitespace()
  258. && $originalIndent !== $tokens[$assignVarIndex - 1]->getContent()
  259. ) {
  260. $tokens[$assignVarIndex - 1] = new Token([T_WHITESPACE, $originalIndent]);
  261. }
  262. // remove trailing space after the new return statement which might be added during the clean up process
  263. $nextIndex = $tokens->getNonEmptySibling($assignVarIndex, 1);
  264. if (!$tokens[$nextIndex]->isWhitespace()) {
  265. $tokens->insertAt($nextIndex, new Token([T_WHITESPACE, ' ']));
  266. ++$inserted;
  267. }
  268. return $inserted;
  269. }
  270. private function clearIfSave(Tokens $tokens, $index)
  271. {
  272. if ($tokens[$index]->isComment()) {
  273. return;
  274. }
  275. if ($tokens[$index]->isWhitespace() && $tokens[$tokens->getPrevNonWhitespace($index)]->isComment()) {
  276. return;
  277. }
  278. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  279. }
  280. /**
  281. * @return bool
  282. */
  283. private function isSuperGlobal(Token $token)
  284. {
  285. static $superNames = [
  286. '$_COOKIE' => true,
  287. '$_ENV' => true,
  288. '$_FILES' => true,
  289. '$_GET' => true,
  290. '$_POST' => true,
  291. '$_REQUEST' => true,
  292. '$_SERVER' => true,
  293. '$_SESSION' => true,
  294. '$GLOBALS' => true,
  295. ];
  296. if (!$token->isGivenKind(T_VARIABLE)) {
  297. return false;
  298. }
  299. return isset($superNames[strtoupper($token->getContent())]);
  300. }
  301. }