1<?php 2/** 3 * Checks alignment of assignments. 4 * 5 * If there are multiple adjacent assignments, it will check that the equals signs of 6 * each assignment are aligned. It will display a warning to advise that the signs should be aligned. 7 * 8 * @author Greg Sherwood <gsherwood@squiz.net> 9 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) 10 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 11 */ 12 13namespace PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting; 14 15use PHP_CodeSniffer\Files\File; 16use PHP_CodeSniffer\Sniffs\Sniff; 17use PHP_CodeSniffer\Util\Tokens; 18 19class MultipleStatementAlignmentSniff implements Sniff 20{ 21 22 /** 23 * A list of tokenizers this sniff supports. 24 * 25 * @var array 26 */ 27 public $supportedTokenizers = [ 28 'PHP', 29 'JS', 30 ]; 31 32 /** 33 * If true, an error will be thrown; otherwise a warning. 34 * 35 * @var boolean 36 */ 37 public $error = false; 38 39 /** 40 * The maximum amount of padding before the alignment is ignored. 41 * 42 * If the amount of padding required to align this assignment with the 43 * surrounding assignments exceeds this number, the assignment will be 44 * ignored and no errors or warnings will be thrown. 45 * 46 * @var integer 47 */ 48 public $maxPadding = 1000; 49 50 /** 51 * Controls which side of the assignment token is used for alignment. 52 * 53 * @var boolean 54 */ 55 public $alignAtEnd = true; 56 57 58 /** 59 * Returns an array of tokens this test wants to listen for. 60 * 61 * @return array 62 */ 63 public function register() 64 { 65 $tokens = Tokens::$assignmentTokens; 66 unset($tokens[T_DOUBLE_ARROW]); 67 return $tokens; 68 69 }//end register() 70 71 72 /** 73 * Processes this test, when one of its tokens is encountered. 74 * 75 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 76 * @param int $stackPtr The position of the current token 77 * in the stack passed in $tokens. 78 * 79 * @return int 80 */ 81 public function process(File $phpcsFile, $stackPtr) 82 { 83 $tokens = $phpcsFile->getTokens(); 84 85 // Ignore assignments used in a condition, like an IF or FOR. 86 if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { 87 // If the parenthesis is on the same line as the assignment, 88 // then it should be ignored as it is specifically being grouped. 89 $parens = $tokens[$stackPtr]['nested_parenthesis']; 90 $lastParen = array_pop($parens); 91 if ($tokens[$lastParen]['line'] === $tokens[$stackPtr]['line']) { 92 return; 93 } 94 95 foreach ($tokens[$stackPtr]['nested_parenthesis'] as $start => $end) { 96 if (isset($tokens[$start]['parenthesis_owner']) === true) { 97 return; 98 } 99 } 100 } 101 102 $lastAssign = $this->checkAlignment($phpcsFile, $stackPtr); 103 return ($lastAssign + 1); 104 105 }//end process() 106 107 108 /** 109 * Processes this test, when one of its tokens is encountered. 110 * 111 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 112 * @param int $stackPtr The position of the current token 113 * in the stack passed in $tokens. 114 * @param int $end The token where checking should end. 115 * If NULL, the entire file will be checked. 116 * 117 * @return int 118 */ 119 public function checkAlignment($phpcsFile, $stackPtr, $end=null) 120 { 121 $tokens = $phpcsFile->getTokens(); 122 123 $assignments = []; 124 $prevAssign = null; 125 $lastLine = $tokens[$stackPtr]['line']; 126 $maxPadding = null; 127 $stopped = null; 128 $lastCode = $stackPtr; 129 $lastSemi = null; 130 $arrayEnd = null; 131 132 if ($end === null) { 133 $end = $phpcsFile->numTokens; 134 } 135 136 $find = Tokens::$assignmentTokens; 137 unset($find[T_DOUBLE_ARROW]); 138 139 $scopes = Tokens::$scopeOpeners; 140 unset($scopes[T_CLOSURE]); 141 unset($scopes[T_ANON_CLASS]); 142 unset($scopes[T_OBJECT]); 143 144 for ($assign = $stackPtr; $assign < $end; $assign++) { 145 if ($tokens[$assign]['level'] < $tokens[$stackPtr]['level']) { 146 // Statement is in a different context, so the block is over. 147 break; 148 } 149 150 if (isset($tokens[$assign]['scope_opener']) === true 151 && $tokens[$assign]['level'] === $tokens[$stackPtr]['level'] 152 ) { 153 if (isset($scopes[$tokens[$assign]['code']]) === true) { 154 // This type of scope indicates that the assignment block is over. 155 break; 156 } 157 158 // Skip over the scope block because it is seen as part of the assignment block, 159 // but also process any assignment blocks that are inside as well. 160 $nextAssign = $phpcsFile->findNext($find, ($assign + 1), ($tokens[$assign]['scope_closer'] - 1)); 161 if ($nextAssign !== false) { 162 $assign = $this->checkAlignment($phpcsFile, $nextAssign); 163 } else { 164 $assign = $tokens[$assign]['scope_closer']; 165 } 166 167 $lastCode = $assign; 168 continue; 169 } 170 171 if ($assign === $arrayEnd) { 172 $arrayEnd = null; 173 } 174 175 if (isset($find[$tokens[$assign]['code']]) === false) { 176 // A blank line indicates that the assignment block has ended. 177 if (isset(Tokens::$emptyTokens[$tokens[$assign]['code']]) === false 178 && ($tokens[$assign]['line'] - $tokens[$lastCode]['line']) > 1 179 && $tokens[$assign]['level'] === $tokens[$stackPtr]['level'] 180 && $arrayEnd === null 181 ) { 182 break; 183 } 184 185 if ($tokens[$assign]['code'] === T_CLOSE_TAG) { 186 // Breaking out of PHP ends the assignment block. 187 break; 188 } 189 190 if ($tokens[$assign]['code'] === T_OPEN_SHORT_ARRAY 191 && isset($tokens[$assign]['bracket_closer']) === true 192 ) { 193 $arrayEnd = $tokens[$assign]['bracket_closer']; 194 } 195 196 if ($tokens[$assign]['code'] === T_ARRAY 197 && isset($tokens[$assign]['parenthesis_opener']) === true 198 && isset($tokens[$tokens[$assign]['parenthesis_opener']]['parenthesis_closer']) === true 199 ) { 200 $arrayEnd = $tokens[$tokens[$assign]['parenthesis_opener']]['parenthesis_closer']; 201 } 202 203 if (isset(Tokens::$emptyTokens[$tokens[$assign]['code']]) === false) { 204 $lastCode = $assign; 205 206 if ($tokens[$assign]['code'] === T_SEMICOLON) { 207 if ($tokens[$assign]['conditions'] === $tokens[$stackPtr]['conditions']) { 208 if ($lastSemi !== null && $prevAssign !== null && $lastSemi > $prevAssign) { 209 // This statement did not have an assignment operator in it. 210 break; 211 } else { 212 $lastSemi = $assign; 213 } 214 } else if ($tokens[$assign]['level'] < $tokens[$stackPtr]['level']) { 215 // Statement is in a different context, so the block is over. 216 break; 217 } 218 } 219 }//end if 220 221 continue; 222 } else if ($assign !== $stackPtr && $tokens[$assign]['line'] === $lastLine) { 223 // Skip multiple assignments on the same line. We only need to 224 // try and align the first assignment. 225 continue; 226 }//end if 227 228 if ($assign !== $stackPtr) { 229 if ($tokens[$assign]['level'] > $tokens[$stackPtr]['level']) { 230 // Has to be nested inside the same conditions as the first assignment. 231 // We've gone one level down, so process this new block. 232 $assign = $this->checkAlignment($phpcsFile, $assign); 233 $lastCode = $assign; 234 continue; 235 } else if ($tokens[$assign]['level'] < $tokens[$stackPtr]['level']) { 236 // We've gone one level up, so the block we are processing is done. 237 break; 238 } else if ($arrayEnd !== null) { 239 // Assignments inside arrays are not part of 240 // the original block, so process this new block. 241 $assign = ($this->checkAlignment($phpcsFile, $assign, $arrayEnd) - 1); 242 $arrayEnd = null; 243 $lastCode = $assign; 244 continue; 245 } 246 247 // Make sure it is not assigned inside a condition (eg. IF, FOR). 248 if (isset($tokens[$assign]['nested_parenthesis']) === true) { 249 // If the parenthesis is on the same line as the assignment, 250 // then it should be ignored as it is specifically being grouped. 251 $parens = $tokens[$assign]['nested_parenthesis']; 252 $lastParen = array_pop($parens); 253 if ($tokens[$lastParen]['line'] === $tokens[$assign]['line']) { 254 break; 255 } 256 257 foreach ($tokens[$assign]['nested_parenthesis'] as $start => $end) { 258 if (isset($tokens[$start]['parenthesis_owner']) === true) { 259 break(2); 260 } 261 } 262 } 263 }//end if 264 265 $var = $phpcsFile->findPrevious( 266 Tokens::$emptyTokens, 267 ($assign - 1), 268 null, 269 true 270 ); 271 272 // Make sure we wouldn't break our max padding length if we 273 // aligned with this statement, or they wouldn't break the max 274 // padding length if they aligned with us. 275 $varEnd = $tokens[($var + 1)]['column']; 276 $assignLen = $tokens[$assign]['length']; 277 if ($this->alignAtEnd !== true) { 278 $assignLen = 1; 279 } 280 281 if ($assign !== $stackPtr) { 282 if ($prevAssign === null) { 283 // Processing an inner block but no assignments found. 284 break; 285 } 286 287 if (($varEnd + 1) > $assignments[$prevAssign]['assign_col']) { 288 $padding = 1; 289 $assignColumn = ($varEnd + 1); 290 } else { 291 $padding = ($assignments[$prevAssign]['assign_col'] - $varEnd + $assignments[$prevAssign]['assign_len'] - $assignLen); 292 if ($padding <= 0) { 293 $padding = 1; 294 } 295 296 if ($padding > $this->maxPadding) { 297 $stopped = $assign; 298 break; 299 } 300 301 $assignColumn = ($varEnd + $padding); 302 }//end if 303 304 if (($assignColumn + $assignLen) > ($assignments[$maxPadding]['assign_col'] + $assignments[$maxPadding]['assign_len'])) { 305 $newPadding = ($varEnd - $assignments[$maxPadding]['var_end'] + $assignLen - $assignments[$maxPadding]['assign_len'] + 1); 306 if ($newPadding > $this->maxPadding) { 307 $stopped = $assign; 308 break; 309 } else { 310 // New alignment settings for previous assignments. 311 foreach ($assignments as $i => $data) { 312 if ($i === $assign) { 313 break; 314 } 315 316 $newPadding = ($varEnd - $data['var_end'] + $assignLen - $data['assign_len'] + 1); 317 $assignments[$i]['expected'] = $newPadding; 318 $assignments[$i]['assign_col'] = ($data['var_end'] + $newPadding); 319 } 320 321 $padding = 1; 322 $assignColumn = ($varEnd + 1); 323 } 324 } else if ($padding > $assignments[$maxPadding]['expected']) { 325 $maxPadding = $assign; 326 }//end if 327 } else { 328 $padding = 1; 329 $assignColumn = ($varEnd + 1); 330 $maxPadding = $assign; 331 }//end if 332 333 $found = 0; 334 if ($tokens[($var + 1)]['code'] === T_WHITESPACE) { 335 $found = $tokens[($var + 1)]['length']; 336 if ($found === 0) { 337 // This means a newline was found. 338 $found = 1; 339 } 340 } 341 342 $assignments[$assign] = [ 343 'var_end' => $varEnd, 344 'assign_len' => $assignLen, 345 'assign_col' => $assignColumn, 346 'expected' => $padding, 347 'found' => $found, 348 ]; 349 350 $lastLine = $tokens[$assign]['line']; 351 $prevAssign = $assign; 352 }//end for 353 354 if (empty($assignments) === true) { 355 return $stackPtr; 356 } 357 358 $numAssignments = count($assignments); 359 360 $errorGenerated = false; 361 foreach ($assignments as $assignment => $data) { 362 if ($data['found'] === $data['expected']) { 363 continue; 364 } 365 366 $expectedText = $data['expected'].' space'; 367 if ($data['expected'] !== 1) { 368 $expectedText .= 's'; 369 } 370 371 if ($data['found'] === null) { 372 $foundText = 'a new line'; 373 } else { 374 $foundText = $data['found'].' space'; 375 if ($data['found'] !== 1) { 376 $foundText .= 's'; 377 } 378 } 379 380 if ($numAssignments === 1) { 381 $type = 'Incorrect'; 382 $error = 'Equals sign not aligned correctly; expected %s but found %s'; 383 } else { 384 $type = 'NotSame'; 385 $error = 'Equals sign not aligned with surrounding assignments; expected %s but found %s'; 386 } 387 388 $errorData = [ 389 $expectedText, 390 $foundText, 391 ]; 392 393 if ($this->error === true) { 394 $fix = $phpcsFile->addFixableError($error, $assignment, $type, $errorData); 395 } else { 396 $fix = $phpcsFile->addFixableWarning($error, $assignment, $type.'Warning', $errorData); 397 } 398 399 $errorGenerated = true; 400 401 if ($fix === true && $data['found'] !== null) { 402 $newContent = str_repeat(' ', $data['expected']); 403 if ($data['found'] === 0) { 404 $phpcsFile->fixer->addContentBefore($assignment, $newContent); 405 } else { 406 $phpcsFile->fixer->replaceToken(($assignment - 1), $newContent); 407 } 408 } 409 }//end foreach 410 411 if ($numAssignments > 1) { 412 if ($errorGenerated === true) { 413 $phpcsFile->recordMetric($stackPtr, 'Adjacent assignments aligned', 'no'); 414 } else { 415 $phpcsFile->recordMetric($stackPtr, 'Adjacent assignments aligned', 'yes'); 416 } 417 } 418 419 if ($stopped !== null) { 420 return $this->checkAlignment($phpcsFile, $stopped); 421 } else { 422 return $assign; 423 } 424 425 }//end checkAlignment() 426 427 428}//end class 429