1<?php 2/** 3 * PSR2_Sniffs_ControlStructures_SwitchDeclarationSniff. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer 9 * @author Greg Sherwood <gsherwood@squiz.net> 10 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 11 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 12 * @link http://pear.php.net/package/PHP_CodeSniffer 13 */ 14 15/** 16 * PSR2_Sniffs_ControlStructures_SwitchDeclarationSniff. 17 * 18 * Ensures all switch statements are defined correctly. 19 * 20 * @category PHP 21 * @package PHP_CodeSniffer 22 * @author Greg Sherwood <gsherwood@squiz.net> 23 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 24 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 25 * @version Release: @package_version@ 26 * @link http://pear.php.net/package/PHP_CodeSniffer 27 */ 28class 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