NoEmptyStatementFixer.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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\Fixer\Semicolon;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\FixerDefinition\CodeSample;
  15. use PhpCsFixer\FixerDefinition\FixerDefinition;
  16. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  17. use PhpCsFixer\Tokenizer\Tokens;
  18. use PhpCsFixer\Tokenizer\TokensAnalyzer;
  19. /**
  20. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  21. */
  22. final class NoEmptyStatementFixer extends AbstractFixer
  23. {
  24. /**
  25. * {@inheritdoc}
  26. */
  27. public function getDefinition(): FixerDefinitionInterface
  28. {
  29. return new FixerDefinition(
  30. 'Remove useless (semicolon) statements.',
  31. [
  32. new CodeSample("<?php \$a = 1;;\n"),
  33. new CodeSample("<?php echo 1;2;\n"),
  34. new CodeSample("<?php while(foo()){\n continue 1;\n}\n"),
  35. ]
  36. );
  37. }
  38. /**
  39. * {@inheritdoc}
  40. *
  41. * Must run before BracesFixer, CombineConsecutiveUnsetsFixer, EmptyLoopBodyFixer, MultilineWhitespaceBeforeSemicolonsFixer, NoExtraBlankLinesFixer, NoMultipleStatementsPerLineFixer, NoSinglelineWhitespaceBeforeSemicolonsFixer, NoTrailingWhitespaceFixer, NoUselessElseFixer, NoUselessReturnFixer, NoWhitespaceInBlankLineFixer, ReturnAssignmentFixer, SpaceAfterSemicolonFixer, SwitchCaseSemicolonToColonFixer.
  42. * Must run after NoUselessSprintfFixer.
  43. */
  44. public function getPriority(): int
  45. {
  46. return 40;
  47. }
  48. /**
  49. * {@inheritdoc}
  50. */
  51. public function isCandidate(Tokens $tokens): bool
  52. {
  53. return $tokens->isTokenKindFound(';');
  54. }
  55. /**
  56. * {@inheritdoc}
  57. */
  58. protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  59. {
  60. for ($index = 0, $count = $tokens->count(); $index < $count; ++$index) {
  61. if ($tokens[$index]->isGivenKind([T_BREAK, T_CONTINUE])) {
  62. $index = $tokens->getNextMeaningfulToken($index);
  63. if ($tokens[$index]->equals([T_LNUMBER, '1'])) {
  64. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  65. }
  66. continue;
  67. }
  68. // skip T_FOR parenthesis to ignore double `;` like `for ($i = 1; ; ++$i) {...}`
  69. if ($tokens[$index]->isGivenKind(T_FOR)) {
  70. $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $tokens->getNextMeaningfulToken($index)) + 1;
  71. continue;
  72. }
  73. if (!$tokens[$index]->equals(';')) {
  74. continue;
  75. }
  76. $previousMeaningfulIndex = $tokens->getPrevMeaningfulToken($index);
  77. // A semicolon can always be removed if it follows a semicolon, '{' or opening tag.
  78. if ($tokens[$previousMeaningfulIndex]->equalsAny(['{', ';', [T_OPEN_TAG]])) {
  79. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  80. continue;
  81. }
  82. // A semicolon might be removed if it follows a '}' but only if the brace is part of certain structures.
  83. if ($tokens[$previousMeaningfulIndex]->equals('}')) {
  84. $this->fixSemicolonAfterCurlyBraceClose($tokens, $index, $previousMeaningfulIndex);
  85. continue;
  86. }
  87. // A semicolon might be removed together with its noop statement, for example "<?php 1;"
  88. $prePreviousMeaningfulIndex = $tokens->getPrevMeaningfulToken($previousMeaningfulIndex);
  89. if (
  90. $tokens[$prePreviousMeaningfulIndex]->equalsAny([';', '{', '}', [T_OPEN_TAG]])
  91. && $tokens[$previousMeaningfulIndex]->isGivenKind([T_CONSTANT_ENCAPSED_STRING, T_DNUMBER, T_LNUMBER, T_STRING, T_VARIABLE])
  92. ) {
  93. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  94. $tokens->clearTokenAndMergeSurroundingWhitespace($previousMeaningfulIndex);
  95. }
  96. }
  97. }
  98. /**
  99. * Fix semicolon after closing curly brace if needed.
  100. *
  101. * Test for the following cases
  102. * - just '{' '}' block (following open tag or ';')
  103. * - if, else, elseif
  104. * - interface, trait, class (but not anonymous)
  105. * - catch, finally (but not try)
  106. * - for, foreach, while (but not 'do - while')
  107. * - switch
  108. * - function (declaration, but not lambda)
  109. * - declare (with '{' '}')
  110. * - namespace (with '{' '}')
  111. *
  112. * @param int $index Semicolon index
  113. */
  114. private function fixSemicolonAfterCurlyBraceClose(Tokens $tokens, int $index, int $curlyCloseIndex): void
  115. {
  116. static $beforeCurlyOpeningKinds = null;
  117. if (null === $beforeCurlyOpeningKinds) {
  118. $beforeCurlyOpeningKinds = [T_ELSE, T_FINALLY, T_NAMESPACE, T_OPEN_TAG];
  119. }
  120. $curlyOpeningIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $curlyCloseIndex);
  121. $beforeCurlyOpeningIndex = $tokens->getPrevMeaningfulToken($curlyOpeningIndex);
  122. if ($tokens[$beforeCurlyOpeningIndex]->isGivenKind($beforeCurlyOpeningKinds) || $tokens[$beforeCurlyOpeningIndex]->equalsAny([';', '{', '}'])) {
  123. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  124. return;
  125. }
  126. // check for namespaces and class, interface and trait definitions
  127. if ($tokens[$beforeCurlyOpeningIndex]->isGivenKind(T_STRING)) {
  128. $classyTestIndex = $tokens->getPrevMeaningfulToken($beforeCurlyOpeningIndex);
  129. while ($tokens[$classyTestIndex]->equals(',') || $tokens[$classyTestIndex]->isGivenKind([T_STRING, T_NS_SEPARATOR, T_EXTENDS, T_IMPLEMENTS])) {
  130. $classyTestIndex = $tokens->getPrevMeaningfulToken($classyTestIndex);
  131. }
  132. $tokensAnalyzer = new TokensAnalyzer($tokens);
  133. if (
  134. $tokens[$classyTestIndex]->isGivenKind(T_NAMESPACE)
  135. || ($tokens[$classyTestIndex]->isClassy() && !$tokensAnalyzer->isAnonymousClass($classyTestIndex))
  136. ) {
  137. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  138. }
  139. return;
  140. }
  141. // early return check, below only control structures with conditions are fixed
  142. if (!$tokens[$beforeCurlyOpeningIndex]->equals(')')) {
  143. return;
  144. }
  145. $openingBraceIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $beforeCurlyOpeningIndex);
  146. $beforeOpeningBraceIndex = $tokens->getPrevMeaningfulToken($openingBraceIndex);
  147. if ($tokens[$beforeOpeningBraceIndex]->isGivenKind([T_IF, T_ELSEIF, T_FOR, T_FOREACH, T_WHILE, T_SWITCH, T_CATCH, T_DECLARE])) {
  148. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  149. return;
  150. }
  151. // check for function definition
  152. if ($tokens[$beforeOpeningBraceIndex]->isGivenKind(T_STRING)) {
  153. $beforeStringIndex = $tokens->getPrevMeaningfulToken($beforeOpeningBraceIndex);
  154. if ($tokens[$beforeStringIndex]->isGivenKind(T_FUNCTION)) {
  155. $tokens->clearTokenAndMergeSurroundingWhitespace($index); // implicit return
  156. }
  157. }
  158. }
  159. }