AbstractNoUselessElseFixer.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer;
  13. use PhpCsFixer\Tokenizer\Tokens;
  14. abstract class AbstractNoUselessElseFixer extends AbstractFixer
  15. {
  16. public function getPriority(): int
  17. {
  18. // should be run before NoWhitespaceInBlankLineFixer, NoExtraBlankLinesFixer, BracesFixer and after NoEmptyStatementFixer.
  19. return 39;
  20. }
  21. protected function isSuperfluousElse(Tokens $tokens, int $index): bool
  22. {
  23. $previousBlockStart = $index;
  24. do {
  25. // Check if all 'if', 'else if ' and 'elseif' blocks above this 'else' always end,
  26. // if so this 'else' is overcomplete.
  27. [$previousBlockStart, $previousBlockEnd] = $this->getPreviousBlock($tokens, $previousBlockStart);
  28. // short 'if' detection
  29. $previous = $previousBlockEnd;
  30. if ($tokens[$previous]->equals('}')) {
  31. $previous = $tokens->getPrevMeaningfulToken($previous);
  32. }
  33. if (
  34. !$tokens[$previous]->equals(';') // 'if' block doesn't end with semicolon, keep 'else'
  35. || $tokens[$tokens->getPrevMeaningfulToken($previous)]->equals('{') // empty 'if' block, keep 'else'
  36. ) {
  37. return false;
  38. }
  39. $candidateIndex = $tokens->getPrevTokenOfKind(
  40. $previous,
  41. [
  42. ';',
  43. [T_BREAK],
  44. [T_CLOSE_TAG],
  45. [T_CONTINUE],
  46. [T_EXIT],
  47. [T_GOTO],
  48. [T_IF],
  49. [T_RETURN],
  50. [T_THROW],
  51. ]
  52. );
  53. if (null === $candidateIndex || $tokens[$candidateIndex]->equalsAny([';', [T_CLOSE_TAG], [T_IF]])) {
  54. return false;
  55. }
  56. if ($tokens[$candidateIndex]->isGivenKind(T_THROW)) {
  57. $previousIndex = $tokens->getPrevMeaningfulToken($candidateIndex);
  58. if (!$tokens[$previousIndex]->equalsAny([';', '{'])) {
  59. return false;
  60. }
  61. }
  62. if ($this->isInConditional($tokens, $candidateIndex, $previousBlockStart)
  63. || $this->isInConditionWithoutBraces($tokens, $candidateIndex, $previousBlockStart)
  64. ) {
  65. return false;
  66. }
  67. // implicit continue, i.e. delete candidate
  68. } while (!$tokens[$previousBlockStart]->isGivenKind(T_IF));
  69. return true;
  70. }
  71. /**
  72. * Return the first and last token index of the previous block.
  73. *
  74. * [0] First is either T_IF, T_ELSE or T_ELSEIF
  75. * [1] Last is either '}' or ';' / T_CLOSE_TAG for short notation blocks
  76. *
  77. * @param int $index T_IF, T_ELSE, T_ELSEIF
  78. *
  79. * @return array{int, int}
  80. */
  81. private function getPreviousBlock(Tokens $tokens, int $index): array
  82. {
  83. $close = $previous = $tokens->getPrevMeaningfulToken($index);
  84. // short 'if' detection
  85. if ($tokens[$close]->equals('}')) {
  86. $previous = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $close);
  87. }
  88. $open = $tokens->getPrevTokenOfKind($previous, [[T_IF], [T_ELSE], [T_ELSEIF]]);
  89. if ($tokens[$open]->isGivenKind(T_IF)) {
  90. $elseCandidate = $tokens->getPrevMeaningfulToken($open);
  91. if ($tokens[$elseCandidate]->isGivenKind(T_ELSE)) {
  92. $open = $elseCandidate;
  93. }
  94. }
  95. return [$open, $close];
  96. }
  97. /**
  98. * @param int $index Index of the token to check
  99. * @param int $lowerLimitIndex Lower limit index. Since the token to check will always be in a conditional we must stop checking at this index
  100. */
  101. private function isInConditional(Tokens $tokens, int $index, int $lowerLimitIndex): bool
  102. {
  103. $candidateIndex = $tokens->getPrevTokenOfKind($index, [')', ';', ':']);
  104. if ($tokens[$candidateIndex]->equals(':')) {
  105. return true;
  106. }
  107. if (!$tokens[$candidateIndex]->equals(')')) {
  108. return false; // token is ';' or close tag
  109. }
  110. // token is always ')' here.
  111. // If it is part of the condition the token is always in, return false.
  112. // If it is not it is a nested condition so return true
  113. $open = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $candidateIndex);
  114. return $tokens->getPrevMeaningfulToken($open) > $lowerLimitIndex;
  115. }
  116. /**
  117. * For internal use only, as it is not perfect.
  118. *
  119. * Returns if the token at given index is part of an if/elseif/else statement
  120. * without {}. Assumes not passing the last `;`/close tag of the statement, not
  121. * out of range index, etc.
  122. *
  123. * @param int $index Index of the token to check
  124. */
  125. private function isInConditionWithoutBraces(Tokens $tokens, int $index, int $lowerLimitIndex): bool
  126. {
  127. do {
  128. if ($tokens[$index]->isComment() || $tokens[$index]->isWhitespace()) {
  129. $index = $tokens->getPrevMeaningfulToken($index);
  130. }
  131. $token = $tokens[$index];
  132. if ($token->isGivenKind([T_IF, T_ELSEIF, T_ELSE])) {
  133. return true;
  134. }
  135. if ($token->equals(';')) {
  136. return false;
  137. }
  138. if ($token->equals('{')) {
  139. $index = $tokens->getPrevMeaningfulToken($index);
  140. // OK if belongs to: for, do, while, foreach
  141. // Not OK if belongs to: if, else, elseif
  142. if ($tokens[$index]->isGivenKind(T_DO)) {
  143. --$index;
  144. continue;
  145. }
  146. if (!$tokens[$index]->equals(')')) {
  147. return false; // like `else {`
  148. }
  149. $index = $tokens->findBlockStart(
  150. Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
  151. $index
  152. );
  153. $index = $tokens->getPrevMeaningfulToken($index);
  154. if ($tokens[$index]->isGivenKind([T_IF, T_ELSEIF])) {
  155. return false;
  156. }
  157. } elseif ($token->equals(')')) {
  158. $type = Tokens::detectBlockType($token);
  159. $index = $tokens->findBlockStart(
  160. $type['type'],
  161. $index
  162. );
  163. $index = $tokens->getPrevMeaningfulToken($index);
  164. } else {
  165. --$index;
  166. }
  167. } while ($index > $lowerLimitIndex);
  168. return false;
  169. }
  170. }