DiscouragedSwitchContinueSniff.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. /**
  3. * PHPCompatibility, an external standard for PHP_CodeSniffer.
  4. *
  5. * @package PHPCompatibility
  6. * @copyright 2012-2019 PHPCompatibility Contributors
  7. * @license https://opensource.org/licenses/LGPL-3.0 LGPL3
  8. * @link https://github.com/PHPCompatibility/PHPCompatibility
  9. */
  10. namespace PHPCompatibility\Sniffs\ControlStructures;
  11. use PHPCompatibility\Sniff;
  12. use PHP_CodeSniffer_File as File;
  13. use PHP_CodeSniffer_Tokens as Tokens;
  14. /**
  15. * Detect use of `continue` in `switch` control structures.
  16. *
  17. * As of PHP 7.3, PHP will throw a warning when `continue` is used to target a `switch`
  18. * control structure.
  19. * The sniff takes numeric arguments used with `continue` into account.
  20. *
  21. * PHP version 7.3
  22. *
  23. * @link https://www.php.net/manual/en/migration73.incompatible.php#migration73.incompatible.core.continue-targeting-switch
  24. * @link https://wiki.php.net/rfc/continue_on_switch_deprecation
  25. * @link https://github.com/php/php-src/commit/04e3523b7d095341f65ed5e71a3cac82fca690e4
  26. * (actual implementation which is different from the RFC).
  27. * @link https://www.php.net/manual/en/control-structures.switch.php
  28. *
  29. * @since 8.2.0
  30. */
  31. class DiscouragedSwitchContinueSniff extends Sniff
  32. {
  33. /**
  34. * Token codes of control structures which can be targeted using continue.
  35. *
  36. * @since 8.2.0
  37. *
  38. * @var array
  39. */
  40. protected $loopStructures = array(
  41. \T_FOR => \T_FOR,
  42. \T_FOREACH => \T_FOREACH,
  43. \T_WHILE => \T_WHILE,
  44. \T_DO => \T_DO,
  45. \T_SWITCH => \T_SWITCH,
  46. );
  47. /**
  48. * Tokens which start a new case within a switch.
  49. *
  50. * @since 8.2.0
  51. *
  52. * @var array
  53. */
  54. protected $caseTokens = array(
  55. \T_CASE => \T_CASE,
  56. \T_DEFAULT => \T_DEFAULT,
  57. );
  58. /**
  59. * Token codes which are accepted to determine the level for the continue.
  60. *
  61. * This array is enriched with the arithmetic operators in the register() method.
  62. *
  63. * @since 8.2.0
  64. *
  65. * @var array
  66. */
  67. protected $acceptedLevelTokens = array(
  68. \T_LNUMBER => \T_LNUMBER,
  69. \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
  70. \T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS,
  71. );
  72. /**
  73. * Returns an array of tokens this test wants to listen for.
  74. *
  75. * @since 8.2.0
  76. *
  77. * @return array
  78. */
  79. public function register()
  80. {
  81. $this->acceptedLevelTokens += Tokens::$arithmeticTokens;
  82. $this->acceptedLevelTokens += Tokens::$emptyTokens;
  83. return array(\T_SWITCH);
  84. }
  85. /**
  86. * Processes this test, when one of its tokens is encountered.
  87. *
  88. * @since 8.2.0
  89. *
  90. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  91. * @param int $stackPtr The position of the current token in the
  92. * stack passed in $tokens.
  93. *
  94. * @return void
  95. */
  96. public function process(File $phpcsFile, $stackPtr)
  97. {
  98. if ($this->supportsAbove('7.3') === false) {
  99. return;
  100. }
  101. $tokens = $phpcsFile->getTokens();
  102. if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
  103. return;
  104. }
  105. $switchOpener = $tokens[$stackPtr]['scope_opener'];
  106. $switchCloser = $tokens[$stackPtr]['scope_closer'];
  107. // Quick check whether we need to bother with the more complex logic.
  108. $hasContinue = $phpcsFile->findNext(\T_CONTINUE, ($switchOpener + 1), $switchCloser);
  109. if ($hasContinue === false) {
  110. return;
  111. }
  112. $caseDefault = $switchOpener;
  113. do {
  114. $caseDefault = $phpcsFile->findNext($this->caseTokens, ($caseDefault + 1), $switchCloser);
  115. if ($caseDefault === false) {
  116. break;
  117. }
  118. if (isset($tokens[$caseDefault]['scope_opener']) === false) {
  119. // Unknown start of the case, skip.
  120. continue;
  121. }
  122. $caseOpener = $tokens[$caseDefault]['scope_opener'];
  123. $nextCaseDefault = $phpcsFile->findNext($this->caseTokens, ($caseDefault + 1), $switchCloser);
  124. if ($nextCaseDefault === false) {
  125. $caseCloser = $switchCloser;
  126. } else {
  127. $caseCloser = $nextCaseDefault;
  128. }
  129. // Check for unscoped control structures within the case.
  130. $controlStructure = $caseOpener;
  131. $doCount = 0;
  132. while (($controlStructure = $phpcsFile->findNext($this->loopStructures, ($controlStructure + 1), $caseCloser)) !== false) {
  133. if ($tokens[$controlStructure]['code'] === \T_DO) {
  134. $doCount++;
  135. }
  136. if (isset($tokens[$controlStructure]['scope_opener'], $tokens[$controlStructure]['scope_closer']) === false) {
  137. if ($tokens[$controlStructure]['code'] === \T_WHILE && $doCount > 0) {
  138. // While in a do-while construct.
  139. $doCount--;
  140. continue;
  141. }
  142. // Control structure without braces found within the case, ignore this case.
  143. continue 2;
  144. }
  145. }
  146. // Examine the contents of the case.
  147. $continue = $caseOpener;
  148. do {
  149. $continue = $phpcsFile->findNext(\T_CONTINUE, ($continue + 1), $caseCloser);
  150. if ($continue === false) {
  151. break;
  152. }
  153. $nextSemicolon = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($continue + 1), $caseCloser);
  154. $codeString = '';
  155. for ($i = ($continue + 1); $i < $nextSemicolon; $i++) {
  156. if (isset($this->acceptedLevelTokens[$tokens[$i]['code']]) === false) {
  157. // Function call/variable or other token which make numeric level impossible to determine.
  158. continue 2;
  159. }
  160. if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
  161. continue;
  162. }
  163. $codeString .= $tokens[$i]['content'];
  164. }
  165. $level = null;
  166. if ($codeString !== '') {
  167. if (is_numeric($codeString)) {
  168. $level = (int) $codeString;
  169. } else {
  170. // With the above logic, the string can only contain digits and operators, eval!
  171. $level = eval("return ( $codeString );");
  172. }
  173. }
  174. if (isset($level) === false || $level === 0) {
  175. $level = 1;
  176. }
  177. // Examine which control structure is being targeted by the continue statement.
  178. if (isset($tokens[$continue]['conditions']) === false) {
  179. continue;
  180. }
  181. $conditions = array_reverse($tokens[$continue]['conditions'], true);
  182. // PHPCS adds more structures to the conditions array than we want to take into
  183. // consideration, so clean up the array.
  184. foreach ($conditions as $tokenPtr => $tokenCode) {
  185. if (isset($this->loopStructures[$tokenCode]) === false) {
  186. unset($conditions[$tokenPtr]);
  187. }
  188. }
  189. $targetCondition = \array_slice($conditions, ($level - 1), 1, true);
  190. if (empty($targetCondition)) {
  191. continue;
  192. }
  193. $conditionToken = key($targetCondition);
  194. if ($conditionToken === $stackPtr) {
  195. $phpcsFile->addWarning(
  196. "Targeting a 'switch' control structure with a 'continue' statement is strongly discouraged and will throw a warning as of PHP 7.3.",
  197. $continue,
  198. 'Found'
  199. );
  200. }
  201. } while ($continue < $caseCloser);
  202. } while ($caseDefault < $switchCloser);
  203. }
  204. }