1<?php 2/** 3 * Squiz_Sniffs_Formatting_OperationBracketSniff. 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_Formatting_OperationBracketSniff. 18 * 19 * Tests that all arithmetic operations are bracketed. 20 * 21 * @category PHP 22 * @package PHP_CodeSniffer 23 * @author Greg Sherwood <gsherwood@squiz.net> 24 * @author Marc McIntyre <mmcintyre@squiz.net> 25 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 26 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 27 * @version Release: @package_version@ 28 * @link http://pear.php.net/package/PHP_CodeSniffer 29 */ 30class Squiz_Sniffs_Formatting_OperatorBracketSniff implements PHP_CodeSniffer_Sniff 31{ 32 33 /** 34 * A list of tokenizers this sniff supports. 35 * 36 * @var array 37 */ 38 public $supportedTokenizers = array( 39 'PHP', 40 'JS', 41 ); 42 43 44 /** 45 * Returns an array of tokens this test wants to listen for. 46 * 47 * @return array 48 */ 49 public function register() 50 { 51 return PHP_CodeSniffer_Tokens::$operators; 52 53 }//end register() 54 55 56 /** 57 * Processes this test, when one of its tokens is encountered. 58 * 59 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 60 * @param int $stackPtr The position of the current token in the 61 * stack passed in $tokens. 62 * 63 * @return void 64 */ 65 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 66 { 67 $tokens = $phpcsFile->getTokens(); 68 69 if ($phpcsFile->tokenizerType === 'JS' && $tokens[$stackPtr]['code'] === T_PLUS) { 70 // JavaScript uses the plus operator for string concatenation as well 71 // so we cannot accurately determine if it is a string concat or addition. 72 // So just ignore it. 73 return; 74 } 75 76 // If the & is a reference, then we don't want to check for brackets. 77 if ($tokens[$stackPtr]['code'] === T_BITWISE_AND && $phpcsFile->isReference($stackPtr) === true) { 78 return; 79 } 80 81 // There is one instance where brackets aren't needed, which involves 82 // the minus sign being used to assign a negative number to a variable. 83 if ($tokens[$stackPtr]['code'] === T_MINUS) { 84 // Check to see if we are trying to return -n. 85 $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true); 86 if ($tokens[$prev]['code'] === T_RETURN) { 87 return; 88 } 89 90 $number = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); 91 if ($tokens[$number]['code'] === T_LNUMBER || $tokens[$number]['code'] === T_DNUMBER) { 92 $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); 93 if ($previous !== false) { 94 $isAssignment = in_array($tokens[$previous]['code'], PHP_CodeSniffer_Tokens::$assignmentTokens); 95 $isEquality = in_array($tokens[$previous]['code'], PHP_CodeSniffer_Tokens::$equalityTokens); 96 $isComparison = in_array($tokens[$previous]['code'], PHP_CodeSniffer_Tokens::$comparisonTokens); 97 if ($isAssignment === true || $isEquality === true || $isComparison === true) { 98 // This is a negative assignment or comparison. 99 // We need to check that the minus and the number are 100 // adjacent. 101 if (($number - $stackPtr) !== 1) { 102 $error = 'No space allowed between minus sign and number'; 103 $phpcsFile->addError($error, $stackPtr, 'SpacingAfterMinus'); 104 } 105 106 return; 107 } 108 } 109 } 110 }//end if 111 112 $previousToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true, null, true); 113 if ($previousToken !== false) { 114 // A list of tokens that indicate that the token is not 115 // part of an arithmetic operation. 116 $invalidTokens = array( 117 T_COMMA, 118 T_COLON, 119 T_OPEN_PARENTHESIS, 120 T_OPEN_SQUARE_BRACKET, 121 T_OPEN_SHORT_ARRAY, 122 T_CASE, 123 ); 124 125 if (in_array($tokens[$previousToken]['code'], $invalidTokens) === true) { 126 return; 127 } 128 } 129 130 // Tokens that are allowed inside a bracketed operation. 131 $allowed = array( 132 T_VARIABLE, 133 T_LNUMBER, 134 T_DNUMBER, 135 T_STRING, 136 T_WHITESPACE, 137 T_NS_SEPARATOR, 138 T_THIS, 139 T_SELF, 140 T_OBJECT_OPERATOR, 141 T_DOUBLE_COLON, 142 T_OPEN_SQUARE_BRACKET, 143 T_CLOSE_SQUARE_BRACKET, 144 T_MODULUS, 145 T_NONE, 146 ); 147 148 $allowed += PHP_CodeSniffer_Tokens::$operators; 149 150 $lastBracket = false; 151 if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { 152 $parenthesis = array_reverse($tokens[$stackPtr]['nested_parenthesis'], true); 153 foreach ($parenthesis as $bracket => $endBracket) { 154 $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($bracket - 1), null, true); 155 $prevCode = $tokens[$prevToken]['code']; 156 157 if ($prevCode === T_ISSET) { 158 // This operation is inside an isset() call, but has 159 // no bracket of it's own. 160 break; 161 } 162 163 if ($prevCode === T_STRING || $prevCode === T_SWITCH) { 164 // We allow simple operations to not be bracketed. 165 // For example, ceil($one / $two). 166 for ($prev = ($stackPtr - 1); $prev > $bracket; $prev--) { 167 if (in_array($tokens[$prev]['code'], $allowed) === true) { 168 continue; 169 } 170 171 if ($tokens[$prev]['code'] === T_CLOSE_PARENTHESIS) { 172 $prev = $tokens[$prev]['parenthesis_opener']; 173 } else { 174 break; 175 } 176 } 177 178 if ($prev !== $bracket) { 179 break; 180 } 181 182 for ($next = ($stackPtr + 1); $next < $endBracket; $next++) { 183 if (in_array($tokens[$next]['code'], $allowed) === true) { 184 continue; 185 } 186 187 if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS) { 188 $next = $tokens[$next]['parenthesis_closer']; 189 } else { 190 break; 191 } 192 } 193 194 if ($next !== $endBracket) { 195 break; 196 } 197 }//end if 198 199 if (in_array($prevCode, PHP_CodeSniffer_Tokens::$scopeOpeners) === true) { 200 // This operation is inside a control structure like FOREACH 201 // or IF, but has no bracket of it's own. 202 // The only control structure allowed to do this is SWITCH. 203 if ($prevCode !== T_SWITCH) { 204 break; 205 } 206 } 207 208 if ($prevCode === T_OPEN_PARENTHESIS) { 209 // These are two open parenthesis in a row. If the current 210 // one doesn't enclose the operator, go to the previous one. 211 if ($endBracket < $stackPtr) { 212 continue; 213 } 214 } 215 216 $lastBracket = $bracket; 217 break; 218 }//end foreach 219 }//end if 220 221 if ($lastBracket === false) { 222 // It is not in a bracketed statement at all. 223 $this->addMissingBracketsError($phpcsFile, $stackPtr); 224 return; 225 } else if ($tokens[$lastBracket]['parenthesis_closer'] < $stackPtr) { 226 // There are a set of brackets in front of it that don't include it. 227 $this->addMissingBracketsError($phpcsFile, $stackPtr); 228 return; 229 } else { 230 // We are enclosed in a set of bracket, so the last thing to 231 // check is that we are not also enclosed in square brackets 232 // like this: ($array[$index + 1]), which is invalid. 233 $brackets = array( 234 T_OPEN_SQUARE_BRACKET, 235 T_CLOSE_SQUARE_BRACKET, 236 ); 237 238 $squareBracket = $phpcsFile->findPrevious($brackets, ($stackPtr - 1), $lastBracket); 239 if ($squareBracket !== false && $tokens[$squareBracket]['code'] === T_OPEN_SQUARE_BRACKET) { 240 $closeSquareBracket = $phpcsFile->findNext($brackets, ($stackPtr + 1)); 241 if ($closeSquareBracket !== false && $tokens[$closeSquareBracket]['code'] === T_CLOSE_SQUARE_BRACKET) { 242 $this->addMissingBracketsError($phpcsFile, $stackPtr); 243 } 244 } 245 246 return; 247 }//end if 248 249 $lastAssignment = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$assignmentTokens, $stackPtr, null, false, null, true); 250 if ($lastAssignment !== false && $lastAssignment > $lastBracket) { 251 $this->addMissingBracketsError($phpcsFile, $stackPtr); 252 } 253 254 }//end process() 255 256 257 /** 258 * Add and fix the missing brackets error. 259 * 260 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 261 * @param int $stackPtr The position of the current token in the 262 * stack passed in $tokens. 263 * 264 * @return void 265 */ 266 public function addMissingBracketsError(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 267 { 268 $error = 'Arithmetic operation must be bracketed'; 269 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingBrackets'); 270 271 if ($fix === false) { 272 return; 273 } 274 275 $tokens = $phpcsFile->getTokens(); 276 277 $allowed = array( 278 T_VARIABLE => true, 279 T_LNUMBER => true, 280 T_DNUMBER => true, 281 T_STRING => true, 282 T_WHITESPACE => true, 283 T_THIS => true, 284 T_SELF => true, 285 T_OBJECT_OPERATOR => true, 286 T_DOUBLE_COLON => true, 287 T_MODULUS => true, 288 T_ISSET => true, 289 T_ARRAY => true, 290 T_NONE => true, 291 ); 292 293 // Find the first token in the expression. 294 for ($before = ($stackPtr - 1); $before > 0; $before--) { 295 // Special case for plus operators because we can't tell if they are used 296 // for addition or string contact. So assume string concat to be safe. 297 if ($phpcsFile->tokenizerType === 'JS' && $tokens[$before]['code'] === T_PLUS) { 298 break; 299 } 300 301 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$before]['code']]) === true 302 || isset(PHP_CodeSniffer_Tokens::$operators[$tokens[$before]['code']]) === true 303 || isset(PHP_CodeSniffer_Tokens::$castTokens[$tokens[$before]['code']]) === true 304 || isset($allowed[$tokens[$before]['code']]) === true 305 ) { 306 continue; 307 } 308 309 if ($tokens[$before]['code'] === T_CLOSE_PARENTHESIS) { 310 $before = $tokens[$before]['parenthesis_opener']; 311 continue; 312 } 313 314 if ($tokens[$before]['code'] === T_CLOSE_SQUARE_BRACKET) { 315 $before = $tokens[$before]['bracket_opener']; 316 continue; 317 } 318 319 break; 320 }//end for 321 322 $before = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($before + 1), null, true); 323 324 // Find the last token in the expression. 325 for ($after = ($stackPtr + 1); $after < $phpcsFile->numTokens; $after++) { 326 // Special case for plus operators because we can't tell if they are used 327 // for addition or string contact. So assume string concat to be safe. 328 if ($phpcsFile->tokenizerType === 'JS' && $tokens[$after]['code'] === T_PLUS) { 329 break; 330 } 331 332 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$after]['code']]) === true 333 || isset(PHP_CodeSniffer_Tokens::$operators[$tokens[$after]['code']]) === true 334 || isset(PHP_CodeSniffer_Tokens::$castTokens[$tokens[$after]['code']]) === true 335 || isset($allowed[$tokens[$after]['code']]) === true 336 ) { 337 continue; 338 } 339 340 if ($tokens[$after]['code'] === T_OPEN_PARENTHESIS) { 341 $after = $tokens[$after]['parenthesis_closer']; 342 continue; 343 } 344 345 if ($tokens[$after]['code'] === T_OPEN_SQUARE_BRACKET) { 346 $after = $tokens[$after]['bracket_closer']; 347 continue; 348 } 349 350 break; 351 }//end for 352 353 $after = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($after - 1), null, true); 354 355 // Can only fix this error if both tokens are available for fixing. 356 // Adding one bracket without the other will create parse errors. 357 $phpcsFile->fixer->beginChangeset(); 358 $phpcsFile->fixer->replaceToken($before, '('.$tokens[$before]['content']); 359 $phpcsFile->fixer->replaceToken($after, $tokens[$after]['content'].')'); 360 $phpcsFile->fixer->endChangeset(); 361 362 }//end addMissingBracketsError() 363 364 365}//end class 366