1<?php 2/** 3 * Generic_Sniffs_Formatting_MultipleStatementAlignmentSniff. 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 * Generic_Sniffs_Formatting_MultipleStatementAlignmentSniff. 17 * 18 * Checks alignment of assignments. If there are multiple adjacent assignments, 19 * it will check that the equals signs of each assignment are aligned. It will 20 * display a warning to advise that the signs should be aligned. 21 * 22 * @category PHP 23 * @package PHP_CodeSniffer 24 * @author Greg Sherwood <gsherwood@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 Generic_Sniffs_Formatting_MultipleStatementAlignmentSniff 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 * If true, an error will be thrown; otherwise a warning. 45 * 46 * @var bool 47 */ 48 public $error = false; 49 50 /** 51 * The maximum amount of padding before the alignment is ignored. 52 * 53 * If the amount of padding required to align this assignment with the 54 * surrounding assignments exceeds this number, the assignment will be 55 * ignored and no errors or warnings will be thrown. 56 * 57 * @var int 58 */ 59 public $maxPadding = 1000; 60 61 62 /** 63 * Returns an array of tokens this test wants to listen for. 64 * 65 * @return array 66 */ 67 public function register() 68 { 69 $tokens = PHP_CodeSniffer_Tokens::$assignmentTokens; 70 unset($tokens[T_DOUBLE_ARROW]); 71 return $tokens; 72 73 }//end register() 74 75 76 /** 77 * Processes this test, when one of its tokens is encountered. 78 * 79 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 80 * @param int $stackPtr The position of the current token 81 * in the stack passed in $tokens. 82 * 83 * @return int 84 */ 85 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 86 { 87 $tokens = $phpcsFile->getTokens(); 88 89 // Ignore assignments used in a condition, like an IF or FOR. 90 if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { 91 foreach ($tokens[$stackPtr]['nested_parenthesis'] as $start => $end) { 92 if (isset($tokens[$start]['parenthesis_owner']) === true) { 93 return; 94 } 95 } 96 } 97 98 $lastAssign = $this->checkAlignment($phpcsFile, $stackPtr); 99 return ($lastAssign + 1); 100 101 }//end process() 102 103 104 /** 105 * Processes this test, when one of its tokens is encountered. 106 * 107 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 108 * @param int $stackPtr The position of the current token 109 * in the stack passed in $tokens. 110 * 111 * @return int 112 */ 113 public function checkAlignment(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 114 { 115 $tokens = $phpcsFile->getTokens(); 116 117 $assignments = array(); 118 $prevAssign = null; 119 $lastLine = $tokens[$stackPtr]['line']; 120 $maxPadding = null; 121 $stopped = null; 122 $lastCode = $stackPtr; 123 $lastSemi = null; 124 125 $find = PHP_CodeSniffer_Tokens::$assignmentTokens; 126 unset($find[T_DOUBLE_ARROW]); 127 128 for ($assign = $stackPtr; $assign < $phpcsFile->numTokens; $assign++) { 129 if (isset($find[$tokens[$assign]['code']]) === false) { 130 // A blank line indicates that the assignment block has ended. 131 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$assign]['code']]) === false) { 132 if (($tokens[$assign]['line'] - $tokens[$lastCode]['line']) > 1) { 133 break; 134 } 135 136 $lastCode = $assign; 137 138 if ($tokens[$assign]['code'] === T_SEMICOLON) { 139 if ($tokens[$assign]['conditions'] === $tokens[$stackPtr]['conditions']) { 140 if ($lastSemi !== null && $prevAssign !== null && $lastSemi > $prevAssign) { 141 // This statement did not have an assignment operator in it. 142 break; 143 } else { 144 $lastSemi = $assign; 145 } 146 } else { 147 // Statement is in a different context, so the block is over. 148 break; 149 } 150 } 151 }//end if 152 153 continue; 154 } else if ($assign !== $stackPtr && $tokens[$assign]['line'] === $lastLine) { 155 // Skip multiple assignments on the same line. We only need to 156 // try and align the first assignment. 157 continue; 158 }//end if 159 160 if ($assign !== $stackPtr) { 161 // Has to be nested inside the same conditions as the first assignment. 162 if ($tokens[$assign]['conditions'] !== $tokens[$stackPtr]['conditions']) { 163 break; 164 } 165 166 // Make sure it is not assigned inside a condition (eg. IF, FOR). 167 if (isset($tokens[$assign]['nested_parenthesis']) === true) { 168 foreach ($tokens[$assign]['nested_parenthesis'] as $start => $end) { 169 if (isset($tokens[$start]['parenthesis_owner']) === true) { 170 break(2); 171 } 172 } 173 } 174 }//end if 175 176 $var = $phpcsFile->findPrevious( 177 PHP_CodeSniffer_Tokens::$emptyTokens, 178 ($assign - 1), 179 null, 180 true 181 ); 182 183 // Make sure we wouldn't break our max padding length if we 184 // aligned with this statement, or they wouldn't break the max 185 // padding length if they aligned with us. 186 $varEnd = $tokens[($var + 1)]['column']; 187 $assignLen = $tokens[$assign]['length']; 188 if ($assign !== $stackPtr) { 189 if (($varEnd + 1) > $assignments[$prevAssign]['assign_col']) { 190 $padding = 1; 191 $assignColumn = ($varEnd + 1); 192 } else { 193 $padding = ($assignments[$prevAssign]['assign_col'] - $varEnd + $assignments[$prevAssign]['assign_len'] - $assignLen); 194 if ($padding === 0) { 195 $padding = 1; 196 } 197 198 if ($padding > $this->maxPadding) { 199 $stopped = $assign; 200 break; 201 } 202 203 $assignColumn = ($varEnd + $padding); 204 }//end if 205 206 if (($assignColumn + $assignLen) > ($assignments[$maxPadding]['assign_col'] + $assignments[$maxPadding]['assign_len'])) { 207 $newPadding = ($varEnd - $assignments[$maxPadding]['var_end'] + $assignLen - $assignments[$maxPadding]['assign_len'] + 1); 208 if ($newPadding > $this->maxPadding) { 209 $stopped = $assign; 210 break; 211 } else { 212 // New alignment settings for previous assignments. 213 foreach ($assignments as $i => $data) { 214 if ($i === $assign) { 215 break; 216 } 217 218 $newPadding = ($varEnd - $data['var_end'] + $assignLen - $data['assign_len'] + 1); 219 $assignments[$i]['expected'] = $newPadding; 220 $assignments[$i]['assign_col'] = ($data['var_end'] + $newPadding); 221 } 222 223 $padding = 1; 224 $assignColumn = ($varEnd + 1); 225 } 226 } else if ($padding > $assignments[$maxPadding]['expected']) { 227 $maxPadding = $assign; 228 }//end if 229 } else { 230 $padding = 1; 231 $assignColumn = ($varEnd + 1); 232 $maxPadding = $assign; 233 }//end if 234 235 $found = 0; 236 if ($tokens[($var + 1)]['code'] === T_WHITESPACE) { 237 $found = $tokens[($var + 1)]['length']; 238 if ($found === 0) { 239 // This means a newline was found. 240 $found = 1; 241 } 242 } 243 244 $assignments[$assign] = array( 245 'var_end' => $varEnd, 246 'assign_len' => $assignLen, 247 'assign_col' => $assignColumn, 248 'expected' => $padding, 249 'found' => $found, 250 ); 251 252 $lastLine = $tokens[$assign]['line']; 253 $prevAssign = $assign; 254 }//end for 255 256 if (empty($assignments) === true) { 257 return $stackPtr; 258 } 259 260 $numAssignments = count($assignments); 261 262 $errorGenerated = false; 263 foreach ($assignments as $assignment => $data) { 264 if ($data['found'] === $data['expected']) { 265 continue; 266 } 267 268 $expectedText = $data['expected'].' space'; 269 if ($data['expected'] !== 1) { 270 $expectedText .= 's'; 271 } 272 273 if ($data['found'] === null) { 274 $foundText = 'a new line'; 275 } else { 276 $foundText = $data['found'].' space'; 277 if ($data['found'] !== 1) { 278 $foundText .= 's'; 279 } 280 } 281 282 if ($numAssignments === 1) { 283 $type = 'Incorrect'; 284 $error = 'Equals sign not aligned correctly; expected %s but found %s'; 285 } else { 286 $type = 'NotSame'; 287 $error = 'Equals sign not aligned with surrounding assignments; expected %s but found %s'; 288 } 289 290 $errorData = array( 291 $expectedText, 292 $foundText, 293 ); 294 295 if ($this->error === true) { 296 $fix = $phpcsFile->addFixableError($error, $assignment, $type, $errorData); 297 } else { 298 $fix = $phpcsFile->addFixableWarning($error, $assignment, $type.'Warning', $errorData); 299 } 300 301 $errorGenerated = true; 302 303 if ($fix === true && $data['found'] !== null) { 304 $newContent = str_repeat(' ', $data['expected']); 305 if ($data['found'] === 0) { 306 $phpcsFile->fixer->addContentBefore($assignment, $newContent); 307 } else { 308 $phpcsFile->fixer->replaceToken(($assignment - 1), $newContent); 309 } 310 } 311 }//end foreach 312 313 if ($numAssignments > 1) { 314 if ($errorGenerated === true) { 315 $phpcsFile->recordMetric($stackPtr, 'Adjacent assignments aligned', 'no'); 316 } else { 317 $phpcsFile->recordMetric($stackPtr, 'Adjacent assignments aligned', 'yes'); 318 } 319 } 320 321 if ($stopped !== null) { 322 return $this->checkAlignment($phpcsFile, $stopped); 323 } else { 324 return $assignment; 325 } 326 327 }//end checkAlignment() 328 329 330}//end class 331