PageRenderTime 71ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/php/PHP_CodeSniffer/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php

http://github.com/jonswar/perl-code-tidyall
PHP | 318 lines | 206 code | 43 blank | 69 comment | 59 complexity | 8230afe71de96ea1f1662c3f754effac MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-1.0, 0BSD, MIT
  1. <?php
  2. /**
  3. * Ensures all switch statements are defined correctly.
  4. *
  5. * @author Greg Sherwood <gsherwood@squiz.net>
  6. * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
  7. * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  8. */
  9. namespace PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures;
  10. use PHP_CodeSniffer\Sniffs\Sniff;
  11. use PHP_CodeSniffer\Files\File;
  12. use PHP_CodeSniffer\Util\Tokens;
  13. class SwitchDeclarationSniff implements Sniff
  14. {
  15. /**
  16. * The number of spaces code should be indented.
  17. *
  18. * @var integer
  19. */
  20. public $indent = 4;
  21. /**
  22. * Returns an array of tokens this test wants to listen for.
  23. *
  24. * @return array
  25. */
  26. public function register()
  27. {
  28. return [T_SWITCH];
  29. }//end register()
  30. /**
  31. * Processes this test, when one of its tokens is encountered.
  32. *
  33. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  34. * @param int $stackPtr The position of the current token in the
  35. * stack passed in $tokens.
  36. *
  37. * @return void
  38. */
  39. public function process(File $phpcsFile, $stackPtr)
  40. {
  41. $tokens = $phpcsFile->getTokens();
  42. // We can't process SWITCH statements unless we know where they start and end.
  43. if (isset($tokens[$stackPtr]['scope_opener']) === false
  44. || isset($tokens[$stackPtr]['scope_closer']) === false
  45. ) {
  46. return;
  47. }
  48. $switch = $tokens[$stackPtr];
  49. $nextCase = $stackPtr;
  50. $caseAlignment = ($switch['column'] + $this->indent);
  51. while (($nextCase = $this->findNextCase($phpcsFile, ($nextCase + 1), $switch['scope_closer'])) !== false) {
  52. if ($tokens[$nextCase]['code'] === T_DEFAULT) {
  53. $type = 'default';
  54. } else {
  55. $type = 'case';
  56. }
  57. if ($tokens[$nextCase]['content'] !== strtolower($tokens[$nextCase]['content'])) {
  58. $expected = strtolower($tokens[$nextCase]['content']);
  59. $error = strtoupper($type).' keyword must be lowercase; expected "%s" but found "%s"';
  60. $data = [
  61. $expected,
  62. $tokens[$nextCase]['content'],
  63. ];
  64. $fix = $phpcsFile->addFixableError($error, $nextCase, $type.'NotLower', $data);
  65. if ($fix === true) {
  66. $phpcsFile->fixer->replaceToken($nextCase, $expected);
  67. }
  68. }
  69. if ($type === 'case'
  70. && ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE
  71. || $tokens[($nextCase + 1)]['content'] !== ' ')
  72. ) {
  73. $error = 'CASE keyword must be followed by a single space';
  74. $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpacingAfterCase');
  75. if ($fix === true) {
  76. if ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE) {
  77. $phpcsFile->fixer->addContent($nextCase, ' ');
  78. } else {
  79. $phpcsFile->fixer->replaceToken(($nextCase + 1), ' ');
  80. }
  81. }
  82. }
  83. $opener = $tokens[$nextCase]['scope_opener'];
  84. $nextCloser = $tokens[$nextCase]['scope_closer'];
  85. if ($tokens[$opener]['code'] === T_COLON) {
  86. if ($tokens[($opener - 1)]['code'] === T_WHITESPACE) {
  87. $error = 'There must be no space before the colon in a '.strtoupper($type).' statement';
  88. $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon'.strtoupper($type));
  89. if ($fix === true) {
  90. $phpcsFile->fixer->replaceToken(($opener - 1), '');
  91. }
  92. }
  93. $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true);
  94. if ($tokens[$next]['line'] === $tokens[$opener]['line']
  95. && ($tokens[$next]['code'] === T_COMMENT
  96. || isset(Tokens::$phpcsCommentTokens[$tokens[$next]['code']]) === true)
  97. ) {
  98. // Skip comments on the same line.
  99. $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true);
  100. }
  101. if ($tokens[$next]['line'] !== ($tokens[$opener]['line'] + 1)) {
  102. $error = 'The '.strtoupper($type).' body must start on the line following the statement';
  103. $fix = $phpcsFile->addFixableError($error, $nextCase, 'BodyOnNextLine'.strtoupper($type));
  104. if ($fix === true) {
  105. if ($tokens[$next]['line'] === $tokens[$opener]['line']) {
  106. $padding = str_repeat(' ', ($caseAlignment + $this->indent - 1));
  107. $phpcsFile->fixer->addContentBefore($next, $phpcsFile->eolChar.$padding);
  108. } else {
  109. $phpcsFile->fixer->beginChangeset();
  110. for ($i = ($opener + 1); $i < $next; $i++) {
  111. if ($tokens[$i]['line'] === $tokens[$next]['line']) {
  112. break;
  113. }
  114. $phpcsFile->fixer->replaceToken($i, '');
  115. }
  116. $phpcsFile->fixer->addNewLineBefore($i);
  117. $phpcsFile->fixer->endChangeset();
  118. }
  119. }
  120. }//end if
  121. if ($tokens[$nextCloser]['scope_condition'] === $nextCase) {
  122. // Only need to check some things once, even if the
  123. // closer is shared between multiple case statements, or even
  124. // the default case.
  125. $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCloser - 1), $nextCase, true);
  126. if ($tokens[$prev]['line'] === $tokens[$nextCloser]['line']) {
  127. $error = 'Terminating statement must be on a line by itself';
  128. $fix = $phpcsFile->addFixableError($error, $nextCloser, 'BreakNotNewLine');
  129. if ($fix === true) {
  130. $phpcsFile->fixer->addNewLine($prev);
  131. $phpcsFile->fixer->replaceToken($nextCloser, trim($tokens[$nextCloser]['content']));
  132. }
  133. } else {
  134. $diff = ($caseAlignment + $this->indent - $tokens[$nextCloser]['column']);
  135. if ($diff !== 0) {
  136. $error = 'Terminating statement must be indented to the same level as the CASE body';
  137. $fix = $phpcsFile->addFixableError($error, $nextCloser, 'BreakIndent');
  138. if ($fix === true) {
  139. if ($diff > 0) {
  140. $phpcsFile->fixer->addContentBefore($nextCloser, str_repeat(' ', $diff));
  141. } else {
  142. $phpcsFile->fixer->substrToken(($nextCloser - 1), 0, $diff);
  143. }
  144. }
  145. }
  146. }//end if
  147. }//end if
  148. } else {
  149. $error = strtoupper($type).' statements must be defined using a colon';
  150. $phpcsFile->addError($error, $nextCase, 'WrongOpener'.$type);
  151. }//end if
  152. // We only want cases from here on in.
  153. if ($type !== 'case') {
  154. continue;
  155. }
  156. $nextCode = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), $nextCloser, true);
  157. if ($tokens[$nextCode]['code'] !== T_CASE && $tokens[$nextCode]['code'] !== T_DEFAULT) {
  158. // This case statement has content. If the next case or default comes
  159. // before the closer, it means we don't have an obvious terminating
  160. // statement and need to make some more effort to find one. If we
  161. // don't, we do need a comment.
  162. $nextCode = $this->findNextCase($phpcsFile, ($opener + 1), $nextCloser);
  163. if ($nextCode !== false) {
  164. $prevCode = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCode - 1), $nextCase, true);
  165. if ($tokens[$prevCode]['code'] !== T_COMMENT
  166. && $this->findNestedTerminator($phpcsFile, ($opener + 1), $nextCode) === false
  167. ) {
  168. $error = 'There must be a comment when fall-through is intentional in a non-empty case body';
  169. $phpcsFile->addError($error, $nextCase, 'TerminatingComment');
  170. }
  171. }
  172. }
  173. }//end while
  174. }//end process()
  175. /**
  176. * Find the next CASE or DEFAULT statement from a point in the file.
  177. *
  178. * Note that nested switches are ignored.
  179. *
  180. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  181. * @param int $stackPtr The position to start looking at.
  182. * @param int $end The position to stop looking at.
  183. *
  184. * @return int | bool
  185. */
  186. private function findNextCase($phpcsFile, $stackPtr, $end)
  187. {
  188. $tokens = $phpcsFile->getTokens();
  189. while (($stackPtr = $phpcsFile->findNext([T_CASE, T_DEFAULT, T_SWITCH], $stackPtr, $end)) !== false) {
  190. // Skip nested SWITCH statements; they are handled on their own.
  191. if ($tokens[$stackPtr]['code'] === T_SWITCH) {
  192. $stackPtr = $tokens[$stackPtr]['scope_closer'];
  193. continue;
  194. }
  195. break;
  196. }
  197. return $stackPtr;
  198. }//end findNextCase()
  199. /**
  200. * Returns true if a nested terminating statement is found.
  201. *
  202. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  203. * @param int $stackPtr The position to start looking at.
  204. * @param int $end The position to stop looking at.
  205. *
  206. * @return bool
  207. */
  208. private function findNestedTerminator($phpcsFile, $stackPtr, $end)
  209. {
  210. $tokens = $phpcsFile->getTokens();
  211. $terminators = [
  212. T_RETURN,
  213. T_BREAK,
  214. T_CONTINUE,
  215. T_THROW,
  216. T_EXIT,
  217. ];
  218. $lastToken = $phpcsFile->findPrevious(T_WHITESPACE, ($end - 1), $stackPtr, true);
  219. if ($lastToken !== false) {
  220. if ($tokens[$lastToken]['code'] === T_CLOSE_CURLY_BRACKET) {
  221. // We found a closing curly bracket and want to check if its
  222. // block belongs to an IF, ELSEIF or ELSE clause. If yes, we
  223. // continue searching for a terminating statement within that
  224. // block. Note that we have to make sure that every block of
  225. // the entire if/else statement has a terminating statement.
  226. $currentCloser = $lastToken;
  227. $hasElseBlock = false;
  228. do {
  229. $scopeOpener = $tokens[$currentCloser]['scope_opener'];
  230. $scopeCloser = $tokens[$currentCloser]['scope_closer'];
  231. $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($scopeOpener - 1), $stackPtr, true);
  232. if ($prevToken === false) {
  233. return false;
  234. }
  235. // IF and ELSEIF clauses possess a condition we have to account for.
  236. if ($tokens[$prevToken]['code'] === T_CLOSE_PARENTHESIS) {
  237. $prevToken = $tokens[$prevToken]['parenthesis_owner'];
  238. }
  239. if ($tokens[$prevToken]['code'] === T_IF) {
  240. // If we have not encountered an ELSE clause by now, we cannot
  241. // be sure that the whole statement terminates in every case.
  242. if ($hasElseBlock === false) {
  243. return false;
  244. }
  245. return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
  246. } else if ($tokens[$prevToken]['code'] === T_ELSEIF
  247. || $tokens[$prevToken]['code'] === T_ELSE
  248. ) {
  249. // If we find a terminating statement within this block,
  250. // we continue with the previous ELSEIF or IF clause.
  251. $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
  252. if ($hasTerminator === false) {
  253. return false;
  254. }
  255. $currentCloser = $phpcsFile->findPrevious(T_WHITESPACE, ($prevToken - 1), $stackPtr, true);
  256. if ($tokens[$prevToken]['code'] === T_ELSE) {
  257. $hasElseBlock = true;
  258. }
  259. } else {
  260. return false;
  261. }//end if
  262. } while ($currentCloser !== false && $tokens[$currentCloser]['code'] === T_CLOSE_CURLY_BRACKET);
  263. return true;
  264. } else if ($tokens[$lastToken]['code'] === T_SEMICOLON) {
  265. // We found the last statement of the CASE. Now we want to
  266. // check whether it is a terminating one.
  267. $terminator = $phpcsFile->findStartOfStatement(($lastToken - 1));
  268. if (in_array($tokens[$terminator]['code'], $terminators, true) === true) {
  269. return $terminator;
  270. }
  271. }//end if
  272. }//end if
  273. return false;
  274. }//end findNestedTerminator()
  275. }//end class