1<?php 2/** 3 * Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer 9 * @author Greg Sherwood <gsherwood@squiz.net> 10 * @author Marc McIntyre <mmcintyre@squiz.net> 11 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 12 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 13 * @link http://pear.php.net/package/PHP_CodeSniffer 14 */ 15 16/** 17 * Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff. 18 * 19 * Ensures all the breaks and cases are aligned correctly according to their 20 * parent switch's alignment and enforces other switch formatting. 21 * 22 * @category PHP 23 * @package PHP_CodeSniffer 24 * @author Greg Sherwood <gsherwood@squiz.net> 25 * @author Marc McIntyre <mmcintyre@squiz.net> 26 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 27 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 28 * @version Release: @package_version@ 29 * @link http://pear.php.net/package/PHP_CodeSniffer 30 */ 31class Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff implements PHP_CodeSniffer_Sniff 32{ 33 34 /** 35 * A list of tokenizers this sniff supports. 36 * 37 * @var array 38 */ 39 public $supportedTokenizers = array( 40 'PHP', 41 'JS', 42 ); 43 44 /** 45 * The number of spaces code should be indented. 46 * 47 * @var int 48 */ 49 public $indent = 4; 50 51 52 /** 53 * Returns an array of tokens this test wants to listen for. 54 * 55 * @return array 56 */ 57 public function register() 58 { 59 return array(T_SWITCH); 60 61 }//end register() 62 63 64 /** 65 * Processes this test, when one of its tokens is encountered. 66 * 67 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 68 * @param int $stackPtr The position of the current token in the 69 * stack passed in $tokens. 70 * 71 * @return void 72 */ 73 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 74 { 75 $tokens = $phpcsFile->getTokens(); 76 77 // We can't process SWITCH statements unless we know where they start and end. 78 if (isset($tokens[$stackPtr]['scope_opener']) === false 79 || isset($tokens[$stackPtr]['scope_closer']) === false 80 ) { 81 return; 82 } 83 84 $switch = $tokens[$stackPtr]; 85 $nextCase = $stackPtr; 86 $caseAlignment = ($switch['column'] + $this->indent); 87 $caseCount = 0; 88 $foundDefault = false; 89 90 while (($nextCase = $phpcsFile->findNext(array(T_CASE, T_DEFAULT, T_SWITCH), ($nextCase + 1), $switch['scope_closer'])) !== false) { 91 // Skip nested SWITCH statements; they are handled on their own. 92 if ($tokens[$nextCase]['code'] === T_SWITCH) { 93 $nextCase = $tokens[$nextCase]['scope_closer']; 94 continue; 95 } 96 97 if ($tokens[$nextCase]['code'] === T_DEFAULT) { 98 $type = 'Default'; 99 $foundDefault = true; 100 } else { 101 $type = 'Case'; 102 $caseCount++; 103 } 104 105 if ($tokens[$nextCase]['content'] !== strtolower($tokens[$nextCase]['content'])) { 106 $expected = strtolower($tokens[$nextCase]['content']); 107 $error = strtoupper($type).' keyword must be lowercase; expected "%s" but found "%s"'; 108 $data = array( 109 $expected, 110 $tokens[$nextCase]['content'], 111 ); 112 113 $fix = $phpcsFile->addFixableError($error, $nextCase, $type.'NotLower', $data); 114 if ($fix === true) { 115 $phpcsFile->fixer->replaceToken($nextCase, $expected); 116 } 117 } 118 119 if ($tokens[$nextCase]['column'] !== $caseAlignment) { 120 $error = strtoupper($type).' keyword must be indented '.$this->indent.' spaces from SWITCH keyword'; 121 $fix = $phpcsFile->addFixableError($error, $nextCase, $type.'Indent'); 122 123 if ($fix === true) { 124 $padding = str_repeat(' ', ($caseAlignment - 1)); 125 if ($tokens[$nextCase]['column'] === 1 126 || $tokens[($nextCase - 1)]['code'] !== T_WHITESPACE 127 ) { 128 $phpcsFile->fixer->addContentBefore($nextCase, $padding); 129 } else { 130 $phpcsFile->fixer->replaceToken(($nextCase - 1), $padding); 131 } 132 } 133 } 134 135 if ($type === 'Case' 136 && ($tokens[($nextCase + 1)]['type'] !== 'T_WHITESPACE' 137 || $tokens[($nextCase + 1)]['content'] !== ' ') 138 ) { 139 $error = 'CASE keyword must be followed by a single space'; 140 $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpacingAfterCase'); 141 if ($fix === true) { 142 if ($tokens[($nextCase + 1)]['type'] !== 'T_WHITESPACE') { 143 $phpcsFile->fixer->addContent($nextCase, ' '); 144 } else { 145 $phpcsFile->fixer->replaceToken(($nextCase + 1), ' '); 146 } 147 } 148 } 149 150 if (isset($tokens[$nextCase]['scope_opener']) === false) { 151 $error = 'Possible parse error: CASE missing opening colon'; 152 $phpcsFile->addWarning($error, $nextCase, 'MissingColon'); 153 continue; 154 } 155 156 $opener = $tokens[$nextCase]['scope_opener']; 157 if ($tokens[($opener - 1)]['type'] === 'T_WHITESPACE') { 158 $error = 'There must be no space before the colon in a '.strtoupper($type).' statement'; 159 $fix = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon'.$type); 160 if ($fix === true) { 161 $phpcsFile->fixer->replaceToken(($opener - 1), ''); 162 } 163 } 164 165 $nextBreak = $tokens[$nextCase]['scope_closer']; 166 if ($tokens[$nextBreak]['code'] === T_BREAK 167 || $tokens[$nextBreak]['code'] === T_RETURN 168 || $tokens[$nextBreak]['code'] === T_CONTINUE 169 || $tokens[$nextBreak]['code'] === T_THROW 170 || $tokens[$nextBreak]['code'] === T_EXIT 171 ) { 172 if ($tokens[$nextBreak]['scope_condition'] === $nextCase) { 173 // Only need to check a couple of things once, even if the 174 // break is shared between multiple case statements, or even 175 // the default case. 176 if ($tokens[$nextBreak]['column'] !== $caseAlignment) { 177 $error = 'Case breaking statement must be indented '.$this->indent.' spaces from SWITCH keyword'; 178 $fix = $phpcsFile->addFixableError($error, $nextBreak, 'BreakIndent'); 179 180 if ($fix === true) { 181 $padding = str_repeat(' ', ($caseAlignment - 1)); 182 if ($tokens[$nextBreak]['column'] === 1 183 || $tokens[($nextBreak - 1)]['code'] !== T_WHITESPACE 184 ) { 185 $phpcsFile->fixer->addContentBefore($nextBreak, $padding); 186 } else { 187 $phpcsFile->fixer->replaceToken(($nextBreak - 1), $padding); 188 } 189 } 190 } 191 192 $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($nextBreak - 1), $stackPtr, true); 193 if ($tokens[$prev]['line'] !== ($tokens[$nextBreak]['line'] - 1)) { 194 $error = 'Blank lines are not allowed before case breaking statements'; 195 $phpcsFile->addError($error, $nextBreak, 'SpacingBeforeBreak'); 196 } 197 198 $nextLine = $tokens[$tokens[$stackPtr]['scope_closer']]['line']; 199 $semicolon = $phpcsFile->findEndOfStatement($nextBreak); 200 for ($i = ($semicolon + 1); $i < $tokens[$stackPtr]['scope_closer']; $i++) { 201 if ($tokens[$i]['type'] !== 'T_WHITESPACE') { 202 $nextLine = $tokens[$i]['line']; 203 break; 204 } 205 } 206 207 if ($type === 'Case') { 208 // Ensure the BREAK statement is followed by 209 // a single blank line, or the end switch brace. 210 if ($nextLine !== ($tokens[$semicolon]['line'] + 2) && $i !== $tokens[$stackPtr]['scope_closer']) { 211 $error = 'Case breaking statements must be followed by a single blank line'; 212 $fix = $phpcsFile->addFixableError($error, $nextBreak, 'SpacingAfterBreak'); 213 if ($fix === true) { 214 $phpcsFile->fixer->beginChangeset(); 215 for ($i = ($semicolon + 1); $i <= $tokens[$stackPtr]['scope_closer']; $i++) { 216 if ($tokens[$i]['line'] === $nextLine) { 217 $phpcsFile->fixer->addNewlineBefore($i); 218 break; 219 } 220 221 if ($tokens[$i]['line'] === $tokens[$semicolon]['line']) { 222 continue; 223 } 224 225 $phpcsFile->fixer->replaceToken($i, ''); 226 } 227 228 $phpcsFile->fixer->endChangeset(); 229 } 230 }//end if 231 } else { 232 // Ensure the BREAK statement is not followed by a blank line. 233 if ($nextLine !== ($tokens[$semicolon]['line'] + 1)) { 234 $error = 'Blank lines are not allowed after the DEFAULT case\'s breaking statement'; 235 $phpcsFile->addError($error, $nextBreak, 'SpacingAfterDefaultBreak'); 236 } 237 }//end if 238 239 $caseLine = $tokens[$nextCase]['line']; 240 $nextLine = $tokens[$nextBreak]['line']; 241 for ($i = ($opener + 1); $i < $nextBreak; $i++) { 242 if ($tokens[$i]['type'] !== 'T_WHITESPACE') { 243 $nextLine = $tokens[$i]['line']; 244 break; 245 } 246 } 247 248 if ($nextLine !== ($caseLine + 1)) { 249 $error = 'Blank lines are not allowed after '.strtoupper($type).' statements'; 250 $phpcsFile->addError($error, $nextCase, 'SpacingAfter'.$type); 251 } 252 }//end if 253 254 if ($tokens[$nextBreak]['code'] === T_BREAK) { 255 if ($type === 'Case') { 256 // Ensure empty CASE statements are not allowed. 257 // They must have some code content in them. A comment is not enough. 258 // But count RETURN statements as valid content if they also 259 // happen to close the CASE statement. 260 $foundContent = false; 261 for ($i = ($tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) { 262 if ($tokens[$i]['code'] === T_CASE) { 263 $i = $tokens[$i]['scope_opener']; 264 continue; 265 } 266 267 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$i]['code']]) === false) { 268 $foundContent = true; 269 break; 270 } 271 } 272 273 if ($foundContent === false) { 274 $error = 'Empty CASE statements are not allowed'; 275 $phpcsFile->addError($error, $nextCase, 'EmptyCase'); 276 } 277 } else { 278 // Ensure empty DEFAULT statements are not allowed. 279 // They must (at least) have a comment describing why 280 // the default case is being ignored. 281 $foundContent = false; 282 for ($i = ($tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) { 283 if ($tokens[$i]['type'] !== 'T_WHITESPACE') { 284 $foundContent = true; 285 break; 286 } 287 } 288 289 if ($foundContent === false) { 290 $error = 'Comment required for empty DEFAULT case'; 291 $phpcsFile->addError($error, $nextCase, 'EmptyDefault'); 292 } 293 }//end if 294 }//end if 295 } else if ($type === 'Default') { 296 $error = 'DEFAULT case must have a breaking statement'; 297 $phpcsFile->addError($error, $nextCase, 'DefaultNoBreak'); 298 }//end if 299 }//end while 300 301 if ($foundDefault === false) { 302 $error = 'All SWITCH statements must contain a DEFAULT case'; 303 $phpcsFile->addError($error, $stackPtr, 'MissingDefault'); 304 } 305 306 if ($tokens[$switch['scope_closer']]['column'] !== $switch['column']) { 307 $error = 'Closing brace of SWITCH statement must be aligned with SWITCH keyword'; 308 $phpcsFile->addError($error, $switch['scope_closer'], 'CloseBraceAlign'); 309 } 310 311 if ($caseCount === 0) { 312 $error = 'SWITCH statements must contain at least one CASE statement'; 313 $phpcsFile->addError($error, $stackPtr, 'MissingCase'); 314 } 315 316 }//end process() 317 318 319}//end class 320