| @@ 31-239 (lines=209) @@ | ||
| 28 | * @version Release: @package_version@ |
|
| 29 | * @link http://pear.php.net/package/PHP_CodeSniffer |
|
| 30 | */ |
|
| 31 | class SwitchDeclarationSniff implements PHP_CodeSniffer_Sniff { |
|
| 32 | ||
| 33 | /** |
|
| 34 | * The number of spaces code should be indented. |
|
| 35 | * |
|
| 36 | * @var int |
|
| 37 | */ |
|
| 38 | public $indent = 4; |
|
| 39 | ||
| 40 | ||
| 41 | /** |
|
| 42 | * @inheritDoc |
|
| 43 | */ |
|
| 44 | public function register() { |
|
| 45 | return [T_SWITCH]; |
|
| 46 | ||
| 47 | } |
|
| 48 | ||
| 49 | ||
| 50 | /** |
|
| 51 | * @inheritDoc |
|
| 52 | */ |
|
| 53 | public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { |
|
| 54 | $tokens = $phpcsFile->getTokens(); |
|
| 55 | ||
| 56 | // TODO: Auto-detect spaces vs tabs, maybe using a trait method indent($index, $this->indent) |
|
| 57 | $this->indent = 1; |
|
| 58 | ||
| 59 | // We can't process SWITCH statements unless we know where they start and end. |
|
| 60 | if (isset($tokens[$stackPtr]['scope_opener']) === false |
|
| 61 | || isset($tokens[$stackPtr]['scope_closer']) === false |
|
| 62 | ) { |
|
| 63 | return; |
|
| 64 | } |
|
| 65 | ||
| 66 | $switch = $tokens[$stackPtr]; |
|
| 67 | $nextCase = $stackPtr; |
|
| 68 | $caseAlignment = ($switch['column'] + $this->indent); |
|
| 69 | $caseCount = 0; |
|
| 70 | $foundDefault = false; |
|
| 71 | ||
| 72 | while (($nextCase = $this->findNextCase($phpcsFile, ($nextCase + 1), $switch['scope_closer'])) !== false) { |
|
| 73 | if ($tokens[$nextCase]['code'] === T_DEFAULT) { |
|
| 74 | $type = 'default'; |
|
| 75 | $foundDefault = true; |
|
| 76 | } else { |
|
| 77 | $type = 'case'; |
|
| 78 | $caseCount++; |
|
| 79 | } |
|
| 80 | ||
| 81 | if ($tokens[$nextCase]['content'] !== strtolower($tokens[$nextCase]['content'])) { |
|
| 82 | $expected = strtolower($tokens[$nextCase]['content']); |
|
| 83 | $error = strtoupper($type) . ' keyword must be lowercase; expected "%s" but found "%s"'; |
|
| 84 | $data = [ |
|
| 85 | $expected, |
|
| 86 | $tokens[$nextCase]['content'], |
|
| 87 | ]; |
|
| 88 | ||
| 89 | $fix = $phpcsFile->addFixableError($error, $nextCase, $type . 'NotLower', $data); |
|
| 90 | if ($fix === true) { |
|
| 91 | $phpcsFile->fixer->replaceToken($nextCase, $expected); |
|
| 92 | } |
|
| 93 | } |
|
| 94 | ||
| 95 | if ($type === 'case' |
|
| 96 | && ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE |
|
| 97 | || $tokens[($nextCase + 1)]['content'] !== ' ') |
|
| 98 | ) { |
|
| 99 | $error = 'CASE keyword must be followed by a single space'; |
|
| 100 | $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpacingAfterCase'); |
|
| 101 | if ($fix === true) { |
|
| 102 | if ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE) { |
|
| 103 | $phpcsFile->fixer->addContent($nextCase, ' '); |
|
| 104 | } else { |
|
| 105 | $phpcsFile->fixer->replaceToken(($nextCase + 1), ' '); |
|
| 106 | } |
|
| 107 | } |
|
| 108 | } |
|
| 109 | ||
| 110 | $opener = $tokens[$nextCase]['scope_opener']; |
|
| 111 | if ($tokens[$opener]['code'] === T_COLON) { |
|
| 112 | if ($tokens[($opener - 1)]['code'] === T_WHITESPACE) { |
|
| 113 | $error = 'There must be no space before the colon in a ' . strtoupper($type) . ' statement'; |
|
| 114 | $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon' . strtoupper($type)); |
|
| 115 | if ($fix === true) { |
|
| 116 | $phpcsFile->fixer->replaceToken(($opener - 1), ''); |
|
| 117 | } |
|
| 118 | } |
|
| 119 | ||
| 120 | $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true); |
|
| 121 | if ($tokens[$next]['line'] === $tokens[$opener]['line'] |
|
| 122 | && $tokens[$next]['code'] === T_COMMENT |
|
| 123 | ) { |
|
| 124 | // Skip comments on the same line. |
|
| 125 | $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true); |
|
| 126 | } |
|
| 127 | ||
| 128 | if ($tokens[$next]['line'] !== ($tokens[$opener]['line'] + 1)) { |
|
| 129 | $error = 'The ' . strtoupper($type) . ' body must start on the line following the statement'; |
|
| 130 | $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon' . strtoupper($type)); |
|
| 131 | if ($fix === true) { |
|
| 132 | if ($tokens[$next]['line'] === $tokens[$opener]['line']) { |
|
| 133 | $padding = str_repeat(' ', ($caseAlignment + $this->indent - 1)); |
|
| 134 | $phpcsFile->fixer->addContentBefore($next, $phpcsFile->eolChar . $padding); |
|
| 135 | } else { |
|
| 136 | $phpcsFile->fixer->beginChangeset(); |
|
| 137 | for ($i = ($opener + 1); $i < $next; $i++) { |
|
| 138 | if ($tokens[$i]['line'] === $tokens[$next]['line']) { |
|
| 139 | break; |
|
| 140 | } |
|
| 141 | ||
| 142 | $phpcsFile->fixer->replaceToken($i, ''); |
|
| 143 | } |
|
| 144 | ||
| 145 | $phpcsFile->fixer->addNewLineBefore($i); |
|
| 146 | $phpcsFile->fixer->endChangeset(); |
|
| 147 | } |
|
| 148 | } |
|
| 149 | } |
|
| 150 | } else { |
|
| 151 | $error = strtoupper($type) . ' statements must be defined using a colon'; |
|
| 152 | $phpcsFile->addError($error, $nextCase, 'WrongOpener' . $type); |
|
| 153 | } |
|
| 154 | ||
| 155 | $nextCloser = $tokens[$nextCase]['scope_closer']; |
|
| 156 | if ($tokens[$nextCloser]['scope_condition'] === $nextCase) { |
|
| 157 | // Only need to check some things once, even if the |
|
| 158 | // closer is shared between multiple case statements, or even |
|
| 159 | // the default case. |
|
| 160 | $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCloser - 1), $nextCase, true); |
|
| 161 | if ($tokens[$prev]['line'] === $tokens[$nextCloser]['line']) { |
|
| 162 | $error = 'Terminating statement must be on a line by itself'; |
|
| 163 | $fix = $phpcsFile->addFixableError($error, $nextCloser, 'BreakNotNewLine'); |
|
| 164 | if ($fix === true) { |
|
| 165 | $phpcsFile->fixer->addNewLine($prev); |
|
| 166 | $phpcsFile->fixer->replaceToken($nextCloser, trim($tokens[$nextCloser]['content'])); |
|
| 167 | } |
|
| 168 | } else { |
|
| 169 | $diff = ($caseAlignment + $this->indent - $tokens[$nextCloser]['column']); |
|
| 170 | if ($diff !== 0) { |
|
| 171 | $error = 'Terminating statement must be indented to the same level as the CASE body'; |
|
| 172 | $fix = $phpcsFile->addFixableError($error, $nextCloser, 'BreakIndent'); |
|
| 173 | if ($fix === true) { |
|
| 174 | if ($diff > 0) { |
|
| 175 | $phpcsFile->fixer->addContentBefore($nextCloser, str_repeat(' ', $diff)); |
|
| 176 | } else { |
|
| 177 | $phpcsFile->fixer->substrToken(($nextCloser - 1), 0, $diff); |
|
| 178 | } |
|
| 179 | } |
|
| 180 | } |
|
| 181 | } |
|
| 182 | } |
|
| 183 | ||
| 184 | // We only want cases from here on in. |
|
| 185 | if ($type !== 'case') { |
|
| 186 | continue; |
|
| 187 | } |
|
| 188 | ||
| 189 | $nextCode = $phpcsFile->findNext(T_WHITESPACE, |
|
| 190 | ($tokens[$nextCase]['scope_opener'] + 1), |
|
| 191 | $nextCloser, |
|
| 192 | true); |
|
| 193 | ||
| 194 | if ($tokens[$nextCode]['code'] !== T_CASE && $tokens[$nextCode]['code'] !== T_DEFAULT) { |
|
| 195 | // This case statement has content. If the next case or default comes |
|
| 196 | // before the closer, it means we dont have a terminating statement |
|
| 197 | // and instead need a comment. |
|
| 198 | $nextCode = $this->findNextCase($phpcsFile, ($tokens[$nextCase]['scope_opener'] + 1), $nextCloser); |
|
| 199 | if ($nextCode !== false) { |
|
| 200 | $prevCode = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCode - 1), $nextCase, true); |
|
| 201 | if ($tokens[$prevCode]['code'] !== T_COMMENT) { |
|
| 202 | $error = 'There must be a comment when fall-through is intentional in a non-empty case body'; |
|
| 203 | $phpcsFile->addError($error, $nextCase, 'TerminatingComment'); |
|
| 204 | } |
|
| 205 | } |
|
| 206 | } |
|
| 207 | } |
|
| 208 | ||
| 209 | } |
|
| 210 | ||
| 211 | ||
| 212 | /** |
|
| 213 | * Find the next CASE or DEFAULT statement from a point in the file. |
|
| 214 | * |
|
| 215 | * Note that nested switches are ignored. |
|
| 216 | * |
|
| 217 | * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned. |
|
| 218 | * @param int $stackPtr The position to start looking at. |
|
| 219 | * @param int $end The position to stop looking at. |
|
| 220 | * |
|
| 221 | * @return int | bool |
|
| 222 | */ |
|
| 223 | protected function findNextCase(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $end) { |
|
| 224 | $tokens = $phpcsFile->getTokens(); |
|
| 225 | while (($stackPtr = $phpcsFile->findNext([T_CASE, T_DEFAULT, T_SWITCH], $stackPtr, $end)) !== false) { |
|
| 226 | // Skip nested SWITCH statements; they are handled on their own. |
|
| 227 | if ($tokens[$stackPtr]['code'] === T_SWITCH) { |
|
| 228 | $stackPtr = $tokens[$stackPtr]['scope_closer']; |
|
| 229 | continue; |
|
| 230 | } |
|
| 231 | ||
| 232 | break; |
|
| 233 | } |
|
| 234 | ||
| 235 | return $stackPtr; |
|
| 236 | ||
| 237 | } |
|
| 238 | ||
| 239 | } |
|
| 240 | ||
| @@ 28-248 (lines=221) @@ | ||
| 25 | * @version Release: @package_version@ |
|
| 26 | * @link http://pear.php.net/package/PHP_CodeSniffer |
|
| 27 | */ |
|
| 28 | class PSR2_Sniffs_ControlStructures_SwitchDeclarationSniff implements PHP_CodeSniffer_Sniff |
|
| 29 | { |
|
| 30 | ||
| 31 | /** |
|
| 32 | * The number of spaces code should be indented. |
|
| 33 | * |
|
| 34 | * @var int |
|
| 35 | */ |
|
| 36 | public $indent = 4; |
|
| 37 | ||
| 38 | ||
| 39 | /** |
|
| 40 | * Returns an array of tokens this test wants to listen for. |
|
| 41 | * |
|
| 42 | * @return array |
|
| 43 | */ |
|
| 44 | public function register() |
|
| 45 | { |
|
| 46 | return array(T_SWITCH); |
|
| 47 | ||
| 48 | }//end register() |
|
| 49 | ||
| 50 | ||
| 51 | /** |
|
| 52 | * Processes this test, when one of its tokens is encountered. |
|
| 53 | * |
|
| 54 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. |
|
| 55 | * @param int $stackPtr The position of the current token in the |
|
| 56 | * stack passed in $tokens. |
|
| 57 | * |
|
| 58 | * @return void |
|
| 59 | */ |
|
| 60 | public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) |
|
| 61 | { |
|
| 62 | $tokens = $phpcsFile->getTokens(); |
|
| 63 | ||
| 64 | // We can't process SWITCH statements unless we know where they start and end. |
|
| 65 | if (isset($tokens[$stackPtr]['scope_opener']) === false |
|
| 66 | || isset($tokens[$stackPtr]['scope_closer']) === false |
|
| 67 | ) { |
|
| 68 | return; |
|
| 69 | } |
|
| 70 | ||
| 71 | $switch = $tokens[$stackPtr]; |
|
| 72 | $nextCase = $stackPtr; |
|
| 73 | $caseAlignment = ($switch['column'] + $this->indent); |
|
| 74 | $caseCount = 0; |
|
| 75 | $foundDefault = false; |
|
| 76 | ||
| 77 | while (($nextCase = $this->_findNextCase($phpcsFile, ($nextCase + 1), $switch['scope_closer'])) !== false) { |
|
| 78 | if ($tokens[$nextCase]['code'] === T_DEFAULT) { |
|
| 79 | $type = 'default'; |
|
| 80 | $foundDefault = true; |
|
| 81 | } else { |
|
| 82 | $type = 'case'; |
|
| 83 | $caseCount++; |
|
| 84 | } |
|
| 85 | ||
| 86 | if ($tokens[$nextCase]['content'] !== strtolower($tokens[$nextCase]['content'])) { |
|
| 87 | $expected = strtolower($tokens[$nextCase]['content']); |
|
| 88 | $error = strtoupper($type).' keyword must be lowercase; expected "%s" but found "%s"'; |
|
| 89 | $data = array( |
|
| 90 | $expected, |
|
| 91 | $tokens[$nextCase]['content'], |
|
| 92 | ); |
|
| 93 | ||
| 94 | $fix = $phpcsFile->addFixableError($error, $nextCase, $type.'NotLower', $data); |
|
| 95 | if ($fix === true) { |
|
| 96 | $phpcsFile->fixer->replaceToken($nextCase, $expected); |
|
| 97 | } |
|
| 98 | } |
|
| 99 | ||
| 100 | if ($type === 'case' |
|
| 101 | && ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE |
|
| 102 | || $tokens[($nextCase + 1)]['content'] !== ' ') |
|
| 103 | ) { |
|
| 104 | $error = 'CASE keyword must be followed by a single space'; |
|
| 105 | $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpacingAfterCase'); |
|
| 106 | if ($fix === true) { |
|
| 107 | if ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE) { |
|
| 108 | $phpcsFile->fixer->addContent($nextCase, ' '); |
|
| 109 | } else { |
|
| 110 | $phpcsFile->fixer->replaceToken(($nextCase + 1), ' '); |
|
| 111 | } |
|
| 112 | } |
|
| 113 | } |
|
| 114 | ||
| 115 | $opener = $tokens[$nextCase]['scope_opener']; |
|
| 116 | $nextCloser = $tokens[$nextCase]['scope_closer']; |
|
| 117 | if ($tokens[$opener]['code'] === T_COLON) { |
|
| 118 | if ($tokens[($opener - 1)]['code'] === T_WHITESPACE) { |
|
| 119 | $error = 'There must be no space before the colon in a '.strtoupper($type).' statement'; |
|
| 120 | $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon'.strtoupper($type)); |
|
| 121 | if ($fix === true) { |
|
| 122 | $phpcsFile->fixer->replaceToken(($opener - 1), ''); |
|
| 123 | } |
|
| 124 | } |
|
| 125 | ||
| 126 | $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true); |
|
| 127 | if ($tokens[$next]['line'] === $tokens[$opener]['line'] |
|
| 128 | && $tokens[$next]['code'] === T_COMMENT |
|
| 129 | ) { |
|
| 130 | // Skip comments on the same line. |
|
| 131 | $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true); |
|
| 132 | } |
|
| 133 | ||
| 134 | if ($tokens[$next]['line'] !== ($tokens[$opener]['line'] + 1)) { |
|
| 135 | $error = 'The '.strtoupper($type).' body must start on the line following the statement'; |
|
| 136 | $fix = $phpcsFile->addFixableError($error, $nextCase, 'BodyOnNextLine'.strtoupper($type)); |
|
| 137 | if ($fix === true) { |
|
| 138 | if ($tokens[$next]['line'] === $tokens[$opener]['line']) { |
|
| 139 | $padding = str_repeat(' ', ($caseAlignment + $this->indent - 1)); |
|
| 140 | $phpcsFile->fixer->addContentBefore($next, $phpcsFile->eolChar.$padding); |
|
| 141 | } else { |
|
| 142 | $phpcsFile->fixer->beginChangeset(); |
|
| 143 | for ($i = ($opener + 1); $i < $next; $i++) { |
|
| 144 | if ($tokens[$i]['line'] === $tokens[$next]['line']) { |
|
| 145 | break; |
|
| 146 | } |
|
| 147 | ||
| 148 | $phpcsFile->fixer->replaceToken($i, ''); |
|
| 149 | } |
|
| 150 | ||
| 151 | $phpcsFile->fixer->addNewLineBefore($i); |
|
| 152 | $phpcsFile->fixer->endChangeset(); |
|
| 153 | } |
|
| 154 | } |
|
| 155 | }//end if |
|
| 156 | ||
| 157 | if ($tokens[$nextCloser]['scope_condition'] === $nextCase) { |
|
| 158 | // Only need to check some things once, even if the |
|
| 159 | // closer is shared between multiple case statements, or even |
|
| 160 | // the default case. |
|
| 161 | $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCloser - 1), $nextCase, true); |
|
| 162 | if ($tokens[$prev]['line'] === $tokens[$nextCloser]['line']) { |
|
| 163 | $error = 'Terminating statement must be on a line by itself'; |
|
| 164 | $fix = $phpcsFile->addFixableError($error, $nextCloser, 'BreakNotNewLine'); |
|
| 165 | if ($fix === true) { |
|
| 166 | $phpcsFile->fixer->addNewLine($prev); |
|
| 167 | $phpcsFile->fixer->replaceToken($nextCloser, trim($tokens[$nextCloser]['content'])); |
|
| 168 | } |
|
| 169 | } else { |
|
| 170 | $diff = ($caseAlignment + $this->indent - $tokens[$nextCloser]['column']); |
|
| 171 | if ($diff !== 0) { |
|
| 172 | $error = 'Terminating statement must be indented to the same level as the CASE body'; |
|
| 173 | $fix = $phpcsFile->addFixableError($error, $nextCloser, 'BreakIndent'); |
|
| 174 | if ($fix === true) { |
|
| 175 | if ($diff > 0) { |
|
| 176 | $phpcsFile->fixer->addContentBefore($nextCloser, str_repeat(' ', $diff)); |
|
| 177 | } else { |
|
| 178 | $phpcsFile->fixer->substrToken(($nextCloser - 1), 0, $diff); |
|
| 179 | } |
|
| 180 | } |
|
| 181 | } |
|
| 182 | }//end if |
|
| 183 | }//end if |
|
| 184 | } else { |
|
| 185 | $error = strtoupper($type).' statements must be defined using a colon'; |
|
| 186 | $phpcsFile->addError($error, $nextCase, 'WrongOpener'.$type); |
|
| 187 | }//end if |
|
| 188 | ||
| 189 | // We only want cases from here on in. |
|
| 190 | if ($type !== 'case') { |
|
| 191 | continue; |
|
| 192 | } |
|
| 193 | ||
| 194 | $nextCode = $phpcsFile->findNext( |
|
| 195 | T_WHITESPACE, |
|
| 196 | ($tokens[$nextCase]['scope_opener'] + 1), |
|
| 197 | $nextCloser, |
|
| 198 | true |
|
| 199 | ); |
|
| 200 | ||
| 201 | if ($tokens[$nextCode]['code'] !== T_CASE && $tokens[$nextCode]['code'] !== T_DEFAULT) { |
|
| 202 | // This case statement has content. If the next case or default comes |
|
| 203 | // before the closer, it means we dont have a terminating statement |
|
| 204 | // and instead need a comment. |
|
| 205 | $nextCode = $this->_findNextCase($phpcsFile, ($tokens[$nextCase]['scope_opener'] + 1), $nextCloser); |
|
| 206 | if ($nextCode !== false) { |
|
| 207 | $prevCode = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCode - 1), $nextCase, true); |
|
| 208 | if ($tokens[$prevCode]['code'] !== T_COMMENT) { |
|
| 209 | $error = 'There must be a comment when fall-through is intentional in a non-empty case body'; |
|
| 210 | $phpcsFile->addError($error, $nextCase, 'TerminatingComment'); |
|
| 211 | } |
|
| 212 | } |
|
| 213 | } |
|
| 214 | }//end while |
|
| 215 | ||
| 216 | }//end process() |
|
| 217 | ||
| 218 | ||
| 219 | /** |
|
| 220 | * Find the next CASE or DEFAULT statement from a point in the file. |
|
| 221 | * |
|
| 222 | * Note that nested switches are ignored. |
|
| 223 | * |
|
| 224 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. |
|
| 225 | * @param int $stackPtr The position to start looking at. |
|
| 226 | * @param int $end The position to stop looking at. |
|
| 227 | * |
|
| 228 | * @return int | bool |
|
| 229 | */ |
|
| 230 | private function _findNextCase(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $end) |
|
| 231 | { |
|
| 232 | $tokens = $phpcsFile->getTokens(); |
|
| 233 | while (($stackPtr = $phpcsFile->findNext(array(T_CASE, T_DEFAULT, T_SWITCH), $stackPtr, $end)) !== false) { |
|
| 234 | // Skip nested SWITCH statements; they are handled on their own. |
|
| 235 | if ($tokens[$stackPtr]['code'] === T_SWITCH) { |
|
| 236 | $stackPtr = $tokens[$stackPtr]['scope_closer']; |
|
| 237 | continue; |
|
| 238 | } |
|
| 239 | ||
| 240 | break; |
|
| 241 | } |
|
| 242 | ||
| 243 | return $stackPtr; |
|
| 244 | ||
| 245 | }//end _findNextCase() |
|
| 246 | ||
| 247 | ||
| 248 | }//end class |
|
| 249 | ||