1<?php 2/** 3 * Generic_Sniffs_Whitespace_ScopeIndentSniff. 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 * Generic_Sniffs_Whitespace_ScopeIndentSniff. 18 * 19 * Checks that control structures are structured correctly, and their content 20 * is indented correctly. This sniff will throw errors if tabs are used 21 * for indentation rather than spaces. 22 * 23 * @category PHP 24 * @package PHP_CodeSniffer 25 * @author Greg Sherwood <gsherwood@squiz.net> 26 * @author Marc McIntyre <mmcintyre@squiz.net> 27 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 28 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 29 * @version Release: @package_version@ 30 * @link http://pear.php.net/package/PHP_CodeSniffer 31 */ 32class Generic_Sniffs_WhiteSpace_ScopeIndentSniff implements PHP_CodeSniffer_Sniff 33{ 34 35 /** 36 * A list of tokenizers this sniff supports. 37 * 38 * @var array 39 */ 40 public $supportedTokenizers = array( 41 'PHP', 42 'JS', 43 ); 44 45 /** 46 * The number of spaces code should be indented. 47 * 48 * @var int 49 */ 50 public $indent = 4; 51 52 /** 53 * Does the indent need to be exactly right? 54 * 55 * If TRUE, indent needs to be exactly $indent spaces. If FALSE, 56 * indent needs to be at least $indent spaces (but can be more). 57 * 58 * @var bool 59 */ 60 public $exact = false; 61 62 /** 63 * Should tabs be used for indenting? 64 * 65 * If TRUE, fixes will be made using tabs instead of spaces. 66 * The size of each tab is important, so it should be specified 67 * using the --tab-width CLI argument. 68 * 69 * @var bool 70 */ 71 public $tabIndent = false; 72 73 /** 74 * The --tab-width CLI value that is being used. 75 * 76 * @var int 77 */ 78 private $_tabWidth = null; 79 80 /** 81 * List of tokens not needing to be checked for indentation. 82 * 83 * Useful to allow Sniffs based on this to easily ignore/skip some 84 * tokens from verification. For example, inline HTML sections 85 * or PHP open/close tags can escape from here and have their own 86 * rules elsewhere. 87 * 88 * @var int[] 89 */ 90 public $ignoreIndentationTokens = array(); 91 92 /** 93 * List of tokens not needing to be checked for indentation. 94 * 95 * This is a cached copy of the public version of this var, which 96 * can be set in a ruleset file, and some core ignored tokens. 97 * 98 * @var int[] 99 */ 100 private $_ignoreIndentationTokens = array(); 101 102 /** 103 * Any scope openers that should not cause an indent. 104 * 105 * @var int[] 106 */ 107 protected $nonIndentingScopes = array(); 108 109 /** 110 * Show debug output for this sniff. 111 * 112 * @var bool 113 */ 114 private $_debug = false; 115 116 117 /** 118 * Returns an array of tokens this test wants to listen for. 119 * 120 * @return array 121 */ 122 public function register() 123 { 124 if (defined('PHP_CODESNIFFER_IN_TESTS') === true) { 125 $this->_debug = false; 126 } 127 128 return array(T_OPEN_TAG); 129 130 }//end register() 131 132 133 /** 134 * Processes this test, when one of its tokens is encountered. 135 * 136 * @param PHP_CodeSniffer_File $phpcsFile All the tokens found in the document. 137 * @param int $stackPtr The position of the current token 138 * in the stack passed in $tokens. 139 * 140 * @return void 141 */ 142 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 143 { 144 $debug = PHP_CodeSniffer::getConfigData('scope_indent_debug'); 145 if ($debug !== null) { 146 $this->_debug = (bool) $debug; 147 } 148 149 if ($this->_tabWidth === null) { 150 $cliValues = $phpcsFile->phpcs->cli->getCommandLineValues(); 151 if (isset($cliValues['tabWidth']) === false || $cliValues['tabWidth'] === 0) { 152 // We have no idea how wide tabs are, so assume 4 spaces for fixing. 153 // It shouldn't really matter because indent checks elsewhere in the 154 // standard should fix things up. 155 $this->_tabWidth = 4; 156 } else { 157 $this->_tabWidth = $cliValues['tabWidth']; 158 } 159 } 160 161 $currentIndent = 0; 162 $lastOpenTag = $stackPtr; 163 $lastCloseTag = null; 164 $openScopes = array(); 165 $adjustments = array(); 166 $setIndents = array(); 167 168 $tokens = $phpcsFile->getTokens(); 169 $first = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr); 170 $trimmed = ltrim($tokens[$first]['content']); 171 if ($trimmed === '') { 172 $currentIndent = ($tokens[$stackPtr]['column'] - 1); 173 } else { 174 $currentIndent = (strlen($tokens[$first]['content']) - strlen($trimmed)); 175 } 176 177 if ($this->_debug === true) { 178 $line = $tokens[$stackPtr]['line']; 179 echo "Start with token $stackPtr on line $line with indent $currentIndent".PHP_EOL; 180 } 181 182 if (empty($this->_ignoreIndentationTokens) === true) { 183 $this->_ignoreIndentationTokens = array(T_INLINE_HTML => true); 184 foreach ($this->ignoreIndentationTokens as $token) { 185 if (is_int($token) === false) { 186 if (defined($token) === false) { 187 continue; 188 } 189 190 $token = constant($token); 191 } 192 193 $this->_ignoreIndentationTokens[$token] = true; 194 } 195 }//end if 196 197 $this->exact = (bool) $this->exact; 198 $this->tabIndent = (bool) $this->tabIndent; 199 200 for ($i = ($stackPtr + 1); $i < $phpcsFile->numTokens; $i++) { 201 if ($i === false) { 202 // Something has gone very wrong; maybe a parse error. 203 break; 204 } 205 206 $checkToken = null; 207 $checkIndent = null; 208 209 $exact = (bool) $this->exact; 210 if ($exact === true && isset($tokens[$i]['nested_parenthesis']) === true) { 211 // Don't check indents exactly between parenthesis as they 212 // tend to have custom rules, such as with multi-line function calls 213 // and control structure conditions. 214 $exact = false; 215 } 216 217 // Detect line changes and figure out where the indent is. 218 if ($tokens[$i]['column'] === 1) { 219 $trimmed = ltrim($tokens[$i]['content']); 220 if ($trimmed === '') { 221 if (isset($tokens[($i + 1)]) === true 222 && $tokens[$i]['line'] === $tokens[($i + 1)]['line'] 223 ) { 224 $checkToken = ($i + 1); 225 $tokenIndent = ($tokens[($i + 1)]['column'] - 1); 226 } 227 } else { 228 $checkToken = $i; 229 $tokenIndent = (strlen($tokens[$i]['content']) - strlen($trimmed)); 230 } 231 } 232 233 // Closing parenthesis should just be indented to at least 234 // the same level as where they were opened (but can be more). 235 if (($checkToken !== null 236 && $tokens[$checkToken]['code'] === T_CLOSE_PARENTHESIS 237 && isset($tokens[$checkToken]['parenthesis_opener']) === true) 238 || ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS 239 && isset($tokens[$i]['parenthesis_opener']) === true) 240 ) { 241 if ($checkToken !== null) { 242 $parenCloser = $checkToken; 243 } else { 244 $parenCloser = $i; 245 } 246 247 if ($this->_debug === true) { 248 $line = $tokens[$i]['line']; 249 echo "Closing parenthesis found on line $line".PHP_EOL; 250 } 251 252 $parenOpener = $tokens[$parenCloser]['parenthesis_opener']; 253 if ($tokens[$parenCloser]['line'] !== $tokens[$parenOpener]['line']) { 254 $parens = 0; 255 if (isset($tokens[$parenCloser]['nested_parenthesis']) === true 256 && empty($tokens[$parenCloser]['nested_parenthesis']) === false 257 ) { 258 end($tokens[$parenCloser]['nested_parenthesis']); 259 $parens = key($tokens[$parenCloser]['nested_parenthesis']); 260 if ($this->_debug === true) { 261 $line = $tokens[$parens]['line']; 262 echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; 263 } 264 } 265 266 $condition = 0; 267 if (isset($tokens[$parenCloser]['conditions']) === true 268 && empty($tokens[$parenCloser]['conditions']) === false 269 ) { 270 end($tokens[$parenCloser]['conditions']); 271 $condition = key($tokens[$parenCloser]['conditions']); 272 if ($this->_debug === true) { 273 $line = $tokens[$condition]['line']; 274 $type = $tokens[$condition]['type']; 275 echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; 276 } 277 } 278 279 if ($parens > $condition) { 280 if ($this->_debug === true) { 281 echo "\t* using parenthesis *".PHP_EOL; 282 } 283 284 $parenOpener = $parens; 285 $condition = 0; 286 } else if ($condition > 0) { 287 if ($this->_debug === true) { 288 echo "\t* using condition *".PHP_EOL; 289 } 290 291 $parenOpener = $condition; 292 $parens = 0; 293 } 294 295 $exact = false; 296 297 $lastOpenTagConditions = array_keys($tokens[$lastOpenTag]['conditions']); 298 $lastOpenTagCondition = array_pop($lastOpenTagConditions); 299 300 if ($condition > 0 && $lastOpenTagCondition === $condition) { 301 if ($this->_debug === true) { 302 echo "\t* open tag is inside condition; using open tag *".PHP_EOL; 303 } 304 305 $checkIndent = ($tokens[$lastOpenTag]['column'] - 1); 306 if (isset($adjustments[$condition]) === true) { 307 $checkIndent += $adjustments[$condition]; 308 } 309 310 $currentIndent = $checkIndent; 311 312 if ($this->_debug === true) { 313 $type = $tokens[$lastOpenTag]['type']; 314 echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $lastOpenTag ($type)".PHP_EOL; 315 } 316 } else if ($condition > 0 317 && isset($tokens[$condition]['scope_opener']) === true 318 && isset($setIndents[$tokens[$condition]['scope_opener']]) === true 319 ) { 320 $checkIndent = $setIndents[$tokens[$condition]['scope_opener']]; 321 if (isset($adjustments[$condition]) === true) { 322 $checkIndent += $adjustments[$condition]; 323 } 324 325 $currentIndent = $checkIndent; 326 327 if ($this->_debug === true) { 328 $type = $tokens[$condition]['type']; 329 echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $condition ($type)".PHP_EOL; 330 } 331 } else { 332 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $parenOpener, true); 333 334 $checkIndent = ($tokens[$first]['column'] - 1); 335 if (isset($adjustments[$first]) === true) { 336 $checkIndent += $adjustments[$first]; 337 } 338 339 if ($this->_debug === true) { 340 $line = $tokens[$first]['line']; 341 $type = $tokens[$first]['type']; 342 echo "\t* first token on line $line is $first ($type) *".PHP_EOL; 343 } 344 345 if ($first === $tokens[$parenCloser]['parenthesis_opener']) { 346 // This is unlikely to be the start of the statement, so look 347 // back further to find it. 348 $first--; 349 } 350 351 $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); 352 if ($prev !== $first) { 353 // This is not the start of the statement. 354 if ($this->_debug === true) { 355 $line = $tokens[$prev]['line']; 356 $type = $tokens[$prev]['type']; 357 echo "\t* previous is $type on line $line *".PHP_EOL; 358 } 359 360 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); 361 $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); 362 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); 363 if ($this->_debug === true) { 364 $line = $tokens[$first]['line']; 365 $type = $tokens[$first]['type']; 366 echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; 367 } 368 } 369 370 if (isset($tokens[$first]['scope_closer']) === true 371 && $tokens[$first]['scope_closer'] === $first 372 ) { 373 if ($this->_debug === true) { 374 echo "\t* first token is a scope closer *".PHP_EOL; 375 } 376 377 if (isset($tokens[$first]['scope_condition']) === true) { 378 $scopeCloser = $first; 379 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $tokens[$scopeCloser]['scope_condition'], true); 380 381 $currentIndent = ($tokens[$first]['column'] - 1); 382 if (isset($adjustments[$first]) === true) { 383 $currentIndent += $adjustments[$first]; 384 } 385 386 // Make sure it is divisible by our expected indent. 387 if ($tokens[$tokens[$scopeCloser]['scope_condition']]['code'] !== T_CLOSURE) { 388 $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); 389 } 390 391 $setIndents[$first] = $currentIndent; 392 393 if ($this->_debug === true) { 394 $type = $tokens[$first]['type']; 395 echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; 396 } 397 }//end if 398 } else { 399 // Don't force current indent to divisible because there could be custom 400 // rules in place between parenthesis, such as with arrays. 401 $currentIndent = ($tokens[$first]['column'] - 1); 402 if (isset($adjustments[$first]) === true) { 403 $currentIndent += $adjustments[$first]; 404 } 405 406 $setIndents[$first] = $currentIndent; 407 408 if ($this->_debug === true) { 409 $type = $tokens[$first]['type']; 410 echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; 411 } 412 }//end if 413 }//end if 414 } else if ($this->_debug === true) { 415 echo "\t * ignoring single-line definition *".PHP_EOL; 416 }//end if 417 }//end if 418 419 // Closing short array bracket should just be indented to at least 420 // the same level as where it was opened (but can be more). 421 if ($tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY 422 || ($checkToken !== null 423 && $tokens[$checkToken]['code'] === T_CLOSE_SHORT_ARRAY) 424 ) { 425 if ($checkToken !== null) { 426 $arrayCloser = $checkToken; 427 } else { 428 $arrayCloser = $i; 429 } 430 431 if ($this->_debug === true) { 432 $line = $tokens[$arrayCloser]['line']; 433 echo "Closing short array bracket found on line $line".PHP_EOL; 434 } 435 436 $arrayOpener = $tokens[$arrayCloser]['bracket_opener']; 437 if ($tokens[$arrayCloser]['line'] !== $tokens[$arrayOpener]['line']) { 438 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $arrayOpener, true); 439 $checkIndent = ($tokens[$first]['column'] - 1); 440 if (isset($adjustments[$first]) === true) { 441 $checkIndent += $adjustments[$first]; 442 } 443 444 $exact = false; 445 446 if ($this->_debug === true) { 447 $line = $tokens[$first]['line']; 448 $type = $tokens[$first]['type']; 449 echo "\t* first token on line $line is $first ($type) *".PHP_EOL; 450 } 451 452 if ($first === $tokens[$arrayCloser]['bracket_opener']) { 453 // This is unlikely to be the start of the statement, so look 454 // back further to find it. 455 $first--; 456 } 457 458 $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); 459 if ($prev !== $first) { 460 // This is not the start of the statement. 461 if ($this->_debug === true) { 462 $line = $tokens[$prev]['line']; 463 $type = $tokens[$prev]['type']; 464 echo "\t* previous is $type on line $line *".PHP_EOL; 465 } 466 467 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); 468 $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); 469 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); 470 if ($this->_debug === true) { 471 $line = $tokens[$first]['line']; 472 $type = $tokens[$first]['type']; 473 echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; 474 } 475 } else if ($tokens[$first]['code'] === T_WHITESPACE) { 476 $first = $phpcsFile->findNext(T_WHITESPACE, ($first + 1), null, true); 477 } 478 479 if (isset($tokens[$first]['scope_closer']) === true 480 && $tokens[$first]['scope_closer'] === $first 481 ) { 482 // The first token is a scope closer and would have already 483 // been processed and set the indent level correctly, so 484 // don't adjust it again. 485 if ($this->_debug === true) { 486 echo "\t* first token is a scope closer; ignoring closing short array bracket *".PHP_EOL; 487 } 488 489 if (isset($setIndents[$first]) === true) { 490 $currentIndent = $setIndents[$first]; 491 if ($this->_debug === true) { 492 echo "\t=> indent reset to $currentIndent".PHP_EOL; 493 } 494 } 495 } else { 496 // Don't force current indent to be divisible because there could be custom 497 // rules in place for arrays. 498 $currentIndent = ($tokens[$first]['column'] - 1); 499 if (isset($adjustments[$first]) === true) { 500 $currentIndent += $adjustments[$first]; 501 } 502 503 $setIndents[$first] = $currentIndent; 504 505 if ($this->_debug === true) { 506 $type = $tokens[$first]['type']; 507 echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; 508 } 509 }//end if 510 } else if ($this->_debug === true) { 511 echo "\t * ignoring single-line definition *".PHP_EOL; 512 }//end if 513 }//end if 514 515 // Adjust lines within scopes while auto-fixing. 516 if ($checkToken !== null 517 && $exact === false 518 && (empty($tokens[$checkToken]['conditions']) === false 519 || (isset($tokens[$checkToken]['scope_opener']) === true 520 && $tokens[$checkToken]['scope_opener'] === $checkToken)) 521 ) { 522 if (empty($tokens[$checkToken]['conditions']) === false) { 523 end($tokens[$checkToken]['conditions']); 524 $condition = key($tokens[$checkToken]['conditions']); 525 } else { 526 $condition = $tokens[$checkToken]['scope_condition']; 527 } 528 529 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $condition, true); 530 531 if (isset($adjustments[$first]) === true 532 && (($adjustments[$first] < 0 && $tokenIndent > $currentIndent) 533 || ($adjustments[$first] > 0 && $tokenIndent < $currentIndent)) 534 ) { 535 $padding = ($tokenIndent + $adjustments[$first]); 536 if ($padding > 0) { 537 if ($this->tabIndent === true) { 538 $numTabs = floor($padding / $this->_tabWidth); 539 $numSpaces = ($padding - ($numTabs * $this->_tabWidth)); 540 $padding = str_repeat("\t", $numTabs).str_repeat(' ', $numSpaces); 541 } else { 542 $padding = str_repeat(' ', $padding); 543 } 544 } else { 545 $padding = ''; 546 } 547 548 if ($checkToken === $i) { 549 $phpcsFile->fixer->replaceToken($checkToken, $padding.$trimmed); 550 } else { 551 // Easier to just replace the entire indent. 552 $phpcsFile->fixer->replaceToken(($checkToken - 1), $padding); 553 } 554 555 if ($this->_debug === true) { 556 $length = strlen($padding); 557 $line = $tokens[$checkToken]['line']; 558 $type = $tokens[$checkToken]['type']; 559 echo "Indent adjusted to $length for $type on line $line".PHP_EOL; 560 } 561 562 $adjustments[$checkToken] = $adjustments[$first]; 563 564 if ($this->_debug === true) { 565 $line = $tokens[$checkToken]['line']; 566 $type = $tokens[$checkToken]['type']; 567 echo "\t=> Add adjustment of ".$adjustments[$checkToken]." for token $checkToken ($type) on line $line".PHP_EOL; 568 } 569 }//end if 570 }//end if 571 572 // Scope closers reset the required indent to the same level as the opening condition. 573 if (($checkToken !== null 574 && isset($openScopes[$checkToken]) === true 575 || (isset($tokens[$checkToken]['scope_condition']) === true 576 && isset($tokens[$checkToken]['scope_closer']) === true 577 && $tokens[$checkToken]['scope_closer'] === $checkToken 578 && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line'])) 579 || ($checkToken === null 580 && isset($openScopes[$i]) === true 581 || (isset($tokens[$i]['scope_condition']) === true 582 && isset($tokens[$i]['scope_closer']) === true 583 && $tokens[$i]['scope_closer'] === $i 584 && $tokens[$i]['line'] !== $tokens[$tokens[$i]['scope_opener']]['line'])) 585 ) { 586 if ($this->_debug === true) { 587 if ($checkToken === null) { 588 $type = $tokens[$tokens[$i]['scope_condition']]['type']; 589 $line = $tokens[$i]['line']; 590 } else { 591 $type = $tokens[$tokens[$checkToken]['scope_condition']]['type']; 592 $line = $tokens[$checkToken]['line']; 593 } 594 595 echo "Close scope ($type) on line $line".PHP_EOL; 596 } 597 598 $scopeCloser = $checkToken; 599 if ($scopeCloser === null) { 600 $scopeCloser = $i; 601 } else { 602 array_pop($openScopes); 603 } 604 605 if (isset($tokens[$scopeCloser]['scope_condition']) === true) { 606 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $tokens[$scopeCloser]['scope_condition'], true); 607 608 $currentIndent = ($tokens[$first]['column'] - 1); 609 if (isset($adjustments[$first]) === true) { 610 $currentIndent += $adjustments[$first]; 611 } 612 613 // Make sure it is divisible by our expected indent. 614 if ($tokens[$tokens[$scopeCloser]['scope_condition']]['code'] !== T_CLOSURE) { 615 $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); 616 } 617 618 $setIndents[$scopeCloser] = $currentIndent; 619 620 if ($this->_debug === true) { 621 $type = $tokens[$scopeCloser]['type']; 622 echo "\t=> indent set to $currentIndent by token $scopeCloser ($type)".PHP_EOL; 623 } 624 625 // We only check the indent of scope closers if they are 626 // curly braces because other constructs tend to have different rules. 627 if ($tokens[$scopeCloser]['code'] === T_CLOSE_CURLY_BRACKET) { 628 $exact = true; 629 } else { 630 $checkToken = null; 631 } 632 }//end if 633 }//end if 634 635 // Handle scope for JS object notation. 636 if ($phpcsFile->tokenizerType === 'JS' 637 && (($checkToken !== null 638 && $tokens[$checkToken]['code'] === T_CLOSE_OBJECT 639 && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['bracket_opener']]['line']) 640 || ($checkToken === null 641 && $tokens[$i]['code'] === T_CLOSE_OBJECT 642 && $tokens[$i]['line'] !== $tokens[$tokens[$i]['bracket_opener']]['line'])) 643 ) { 644 if ($this->_debug === true) { 645 $line = $tokens[$i]['line']; 646 echo "Close JS object on line $line".PHP_EOL; 647 } 648 649 $scopeCloser = $checkToken; 650 if ($scopeCloser === null) { 651 $scopeCloser = $i; 652 } else { 653 array_pop($openScopes); 654 } 655 656 $parens = 0; 657 if (isset($tokens[$scopeCloser]['nested_parenthesis']) === true 658 && empty($tokens[$scopeCloser]['nested_parenthesis']) === false 659 ) { 660 end($tokens[$scopeCloser]['nested_parenthesis']); 661 $parens = key($tokens[$scopeCloser]['nested_parenthesis']); 662 if ($this->_debug === true) { 663 $line = $tokens[$parens]['line']; 664 echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; 665 } 666 } 667 668 $condition = 0; 669 if (isset($tokens[$scopeCloser]['conditions']) === true 670 && empty($tokens[$scopeCloser]['conditions']) === false 671 ) { 672 end($tokens[$scopeCloser]['conditions']); 673 $condition = key($tokens[$scopeCloser]['conditions']); 674 if ($this->_debug === true) { 675 $line = $tokens[$condition]['line']; 676 $type = $tokens[$condition]['type']; 677 echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; 678 } 679 } 680 681 if ($parens > $condition) { 682 if ($this->_debug === true) { 683 echo "\t* using parenthesis *".PHP_EOL; 684 } 685 686 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $parens, true); 687 $condition = 0; 688 } else if ($condition > 0) { 689 if ($this->_debug === true) { 690 echo "\t* using condition *".PHP_EOL; 691 } 692 693 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $condition, true); 694 $parens = 0; 695 } else { 696 if ($this->_debug === true) { 697 $line = $tokens[$tokens[$scopeCloser]['bracket_opener']]['line']; 698 echo "\t* token is not in parenthesis or condition; using opener on line $line *".PHP_EOL; 699 } 700 701 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $tokens[$scopeCloser]['bracket_opener'], true); 702 }//end if 703 704 $currentIndent = ($tokens[$first]['column'] - 1); 705 if (isset($adjustments[$first]) === true) { 706 $currentIndent += $adjustments[$first]; 707 } 708 709 if ($parens > 0 || $condition > 0) { 710 $checkIndent = ($tokens[$first]['column'] - 1); 711 if (isset($adjustments[$first]) === true) { 712 $checkIndent += $adjustments[$first]; 713 } 714 715 if ($condition > 0) { 716 $checkIndent += $this->indent; 717 $currentIndent += $this->indent; 718 $exact = true; 719 } 720 } else { 721 $checkIndent = $currentIndent; 722 } 723 724 // Make sure it is divisible by our expected indent. 725 $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); 726 $checkIndent = (int) (ceil($checkIndent / $this->indent) * $this->indent); 727 $setIndents[$first] = $currentIndent; 728 729 if ($this->_debug === true) { 730 $type = $tokens[$first]['type']; 731 echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; 732 } 733 }//end if 734 735 if ($checkToken !== null 736 && isset(PHP_CodeSniffer_Tokens::$scopeOpeners[$tokens[$checkToken]['code']]) === true 737 && in_array($tokens[$checkToken]['code'], $this->nonIndentingScopes) === false 738 && isset($tokens[$checkToken]['scope_opener']) === true 739 ) { 740 $exact = true; 741 742 $lastOpener = null; 743 if (empty($openScopes) === false) { 744 end($openScopes); 745 $lastOpener = current($openScopes); 746 } 747 748 // A scope opener that shares a closer with another token (like multiple 749 // CASEs using the same BREAK) needs to reduce the indent level so its 750 // indent is checked correctly. It will then increase the indent again 751 // (as all openers do) after being checked. 752 if ($lastOpener !== null 753 && isset($tokens[$lastOpener]['scope_closer']) === true 754 && $tokens[$lastOpener]['level'] === $tokens[$checkToken]['level'] 755 && $tokens[$lastOpener]['scope_closer'] === $tokens[$checkToken]['scope_closer'] 756 ) { 757 $currentIndent -= $this->indent; 758 $setIndents[$lastOpener] = $currentIndent; 759 if ($this->_debug === true) { 760 $line = $tokens[$i]['line']; 761 $type = $tokens[$lastOpener]['type']; 762 echo "Shared closer found on line $line".PHP_EOL; 763 echo "\t=> indent set to $currentIndent by token $lastOpener ($type)".PHP_EOL; 764 } 765 } 766 767 if ($tokens[$checkToken]['code'] === T_CLOSURE 768 && $tokenIndent > $currentIndent 769 ) { 770 // The opener is indented more than needed, which is fine. 771 // But just check that it is divisible by our expected indent. 772 $checkIndent = (int) (ceil($tokenIndent / $this->indent) * $this->indent); 773 $exact = false; 774 775 if ($this->_debug === true) { 776 $line = $tokens[$i]['line']; 777 echo "Closure found on line $line".PHP_EOL; 778 echo "\t=> checking indent of $checkIndent; main indent remains at $currentIndent".PHP_EOL; 779 } 780 } 781 }//end if 782 783 // Method prefix indentation has to be exact or else if will break 784 // the rest of the function declaration, and potentially future ones. 785 if ($checkToken !== null 786 && isset(PHP_CodeSniffer_Tokens::$methodPrefixes[$tokens[$checkToken]['code']]) === true 787 && $tokens[($checkToken + 1)]['code'] !== T_DOUBLE_COLON 788 ) { 789 $exact = true; 790 } 791 792 // JS property indentation has to be exact or else if will break 793 // things like function and object indentation. 794 if ($checkToken !== null && $tokens[$checkToken]['code'] === T_PROPERTY) { 795 $exact = true; 796 } 797 798 // PHP tags needs to be indented to exact column positions 799 // so they don't cause problems with indent checks for the code 800 // within them, but they don't need to line up with the current indent. 801 if ($checkToken !== null 802 && ($tokens[$checkToken]['code'] === T_OPEN_TAG 803 || $tokens[$checkToken]['code'] === T_OPEN_TAG_WITH_ECHO 804 || $tokens[$checkToken]['code'] === T_CLOSE_TAG) 805 ) { 806 $exact = true; 807 $checkIndent = ($tokens[$checkToken]['column'] - 1); 808 $checkIndent = (int) (ceil($checkIndent / $this->indent) * $this->indent); 809 } 810 811 // Special case for ELSE statements that are not on the same 812 // line as the previous IF statements closing brace. They still need 813 // to have the same indent or it will break code after the block. 814 if ($checkToken !== null && $tokens[$checkToken]['code'] === T_ELSE) { 815 $exact = true; 816 } 817 818 if ($checkIndent === null) { 819 $checkIndent = $currentIndent; 820 } 821 822 /* 823 The indent of the line is checked by the following IF block. 824 825 Up until now, we've just been figuring out what the indent 826 of this line should be. 827 828 After this IF block, we adjust the indent again for 829 the checking of future line. 830 */ 831 832 $adjusted = false; 833 if ($checkToken !== null 834 && isset($this->_ignoreIndentationTokens[$tokens[$checkToken]['code']]) === false 835 && (($tokenIndent !== $checkIndent && $exact === true) 836 || ($tokenIndent < $checkIndent && $exact === false)) 837 ) { 838 $type = 'IncorrectExact'; 839 $error = 'Line indented incorrectly; expected '; 840 if ($exact === false) { 841 $error .= 'at least '; 842 $type = 'Incorrect'; 843 } 844 845 if ($this->tabIndent === true) { 846 $error .= '%s tabs, found %s'; 847 $data = array( 848 floor($checkIndent / $this->_tabWidth), 849 floor($tokenIndent / $this->_tabWidth), 850 ); 851 } else { 852 $error .= '%s spaces, found %s'; 853 $data = array( 854 $checkIndent, 855 $tokenIndent, 856 ); 857 } 858 859 if ($this->_debug === true) { 860 $line = $tokens[$checkToken]['line']; 861 $message = vsprintf($error, $data); 862 echo "[Line $line] $message".PHP_EOL; 863 } 864 865 $fix = $phpcsFile->addFixableError($error, $checkToken, $type, $data); 866 if ($fix === true || $this->_debug === true) { 867 $padding = ''; 868 if ($this->tabIndent === true) { 869 $numTabs = floor($checkIndent / $this->_tabWidth); 870 if ($numTabs > 0) { 871 $numSpaces = ($checkIndent - ($numTabs * $this->_tabWidth)); 872 $padding = str_repeat("\t", $numTabs).str_repeat(' ', $numSpaces); 873 } 874 } else if ($checkIndent > 0) { 875 $padding = str_repeat(' ', $checkIndent); 876 } 877 878 if ($checkToken === $i) { 879 $accepted = $phpcsFile->fixer->replaceToken($checkToken, $padding.$trimmed); 880 } else { 881 // Easier to just replace the entire indent. 882 $accepted = $phpcsFile->fixer->replaceToken(($checkToken - 1), $padding); 883 } 884 885 if ($accepted === true) { 886 $adjustments[$checkToken] = ($checkIndent - $tokenIndent); 887 if ($this->_debug === true) { 888 $line = $tokens[$checkToken]['line']; 889 $type = $tokens[$checkToken]['type']; 890 echo "\t=> Add adjustment of ".$adjustments[$checkToken]." for token $checkToken ($type) on line $line".PHP_EOL; 891 } 892 } 893 } else { 894 // Assume the change would be applied and continue 895 // checking indents under this assumption. This gives more 896 // technically accurate error messages. 897 $adjustments[$checkToken] = ($checkIndent - $tokenIndent); 898 }//end if 899 }//end if 900 901 if ($checkToken !== null) { 902 $i = $checkToken; 903 } 904 905 // Completely skip here/now docs as the indent is a part of the 906 // content itself. 907 if ($tokens[$i]['code'] === T_START_HEREDOC 908 || $tokens[$i]['code'] === T_START_NOWDOC 909 ) { 910 $i = $phpcsFile->findNext(array(T_END_HEREDOC, T_END_NOWDOC), ($i + 1)); 911 continue; 912 } 913 914 // Completely skip multi-line strings as the indent is a part of the 915 // content itself. 916 if ($tokens[$i]['code'] === T_CONSTANT_ENCAPSED_STRING 917 || $tokens[$i]['code'] === T_DOUBLE_QUOTED_STRING 918 ) { 919 $i = $phpcsFile->findNext($tokens[$i]['code'], ($i + 1), null, true); 920 $i--; 921 continue; 922 } 923 924 // Completely skip doc comments as they tend to have complex 925 // indentation rules. 926 if ($tokens[$i]['code'] === T_DOC_COMMENT_OPEN_TAG) { 927 $i = $tokens[$i]['comment_closer']; 928 continue; 929 } 930 931 // Open tags reset the indent level. 932 if ($tokens[$i]['code'] === T_OPEN_TAG 933 || $tokens[$i]['code'] === T_OPEN_TAG_WITH_ECHO 934 ) { 935 if ($this->_debug === true) { 936 $line = $tokens[$i]['line']; 937 echo "Open PHP tag found on line $line".PHP_EOL; 938 } 939 940 if ($checkToken === null) { 941 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); 942 $currentIndent = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content']))); 943 } else { 944 $currentIndent = ($tokens[$i]['column'] - 1); 945 } 946 947 $lastOpenTag = $i; 948 949 if (isset($adjustments[$i]) === true) { 950 $currentIndent += $adjustments[$i]; 951 } 952 953 // Make sure it is divisible by our expected indent. 954 $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); 955 $setIndents[$i] = $currentIndent; 956 957 if ($this->_debug === true) { 958 $type = $tokens[$i]['type']; 959 echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; 960 } 961 962 continue; 963 }//end if 964 965 // Close tags reset the indent level, unless they are closing a tag 966 // opened on the same line. 967 if ($tokens[$i]['code'] === T_CLOSE_TAG) { 968 if ($this->_debug === true) { 969 $line = $tokens[$i]['line']; 970 echo "Close PHP tag found on line $line".PHP_EOL; 971 } 972 973 if ($tokens[$lastOpenTag]['line'] !== $tokens[$i]['line']) { 974 $currentIndent = ($tokens[$i]['column'] - 1); 975 $lastCloseTag = $i; 976 } else { 977 if ($lastCloseTag === null) { 978 $currentIndent = 0; 979 } else { 980 $currentIndent = ($tokens[$lastCloseTag]['column'] - 1); 981 } 982 } 983 984 if (isset($adjustments[$i]) === true) { 985 $currentIndent += $adjustments[$i]; 986 } 987 988 // Make sure it is divisible by our expected indent. 989 $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); 990 $setIndents[$i] = $currentIndent; 991 992 if ($this->_debug === true) { 993 $type = $tokens[$i]['type']; 994 echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; 995 } 996 997 continue; 998 }//end if 999 1000 // Anon classes and functions set the indent based on their own indent level. 1001 if ($tokens[$i]['code'] === T_CLOSURE || $tokens[$i]['code'] === T_ANON_CLASS) { 1002 $closer = $tokens[$i]['scope_closer']; 1003 if ($tokens[$i]['line'] === $tokens[$closer]['line']) { 1004 if ($this->_debug === true) { 1005 $type = str_replace('_', ' ', strtolower(substr($tokens[$i]['type'], 2))); 1006 $line = $tokens[$i]['line']; 1007 echo "* ignoring single-line $type on line $line".PHP_EOL; 1008 } 1009 1010 $i = $closer; 1011 continue; 1012 } 1013 1014 if ($this->_debug === true) { 1015 $type = str_replace('_', ' ', strtolower(substr($tokens[$i]['type'], 2))); 1016 $line = $tokens[$i]['line']; 1017 echo "Open $type on line $line".PHP_EOL; 1018 } 1019 1020 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); 1021 $currentIndent = (($tokens[$first]['column'] - 1) + $this->indent); 1022 1023 if (isset($adjustments[$first]) === true) { 1024 $currentIndent += $adjustments[$first]; 1025 } 1026 1027 // Make sure it is divisible by our expected indent. 1028 $currentIndent = (int) (floor($currentIndent / $this->indent) * $this->indent); 1029 $i = $tokens[$i]['scope_opener']; 1030 $setIndents[$i] = $currentIndent; 1031 1032 if ($this->_debug === true) { 1033 $type = $tokens[$i]['type']; 1034 echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; 1035 } 1036 1037 continue; 1038 }//end if 1039 1040 // Scope openers increase the indent level. 1041 if (isset($tokens[$i]['scope_condition']) === true 1042 && isset($tokens[$i]['scope_opener']) === true 1043 && $tokens[$i]['scope_opener'] === $i 1044 ) { 1045 $closer = $tokens[$i]['scope_closer']; 1046 if ($tokens[$i]['line'] === $tokens[$closer]['line']) { 1047 if ($this->_debug === true) { 1048 $line = $tokens[$i]['line']; 1049 $type = $tokens[$i]['type']; 1050 echo "* ignoring single-line $type on line $line".PHP_EOL; 1051 } 1052 1053 $i = $closer; 1054 continue; 1055 } 1056 1057 $condition = $tokens[$tokens[$i]['scope_condition']]['code']; 1058 if (isset(PHP_CodeSniffer_Tokens::$scopeOpeners[$condition]) === true 1059 && in_array($condition, $this->nonIndentingScopes) === false 1060 ) { 1061 if ($this->_debug === true) { 1062 $line = $tokens[$i]['line']; 1063 $type = $tokens[$tokens[$i]['scope_condition']]['type']; 1064 echo "Open scope ($type) on line $line".PHP_EOL; 1065 } 1066 1067 $currentIndent += $this->indent; 1068 $setIndents[$i] = $currentIndent; 1069 $openScopes[$tokens[$i]['scope_closer']] = $tokens[$i]['scope_condition']; 1070 1071 if ($this->_debug === true) { 1072 $type = $tokens[$i]['type']; 1073 echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; 1074 } 1075 1076 continue; 1077 } 1078 }//end if 1079 1080 // JS objects set the indent level. 1081 if ($phpcsFile->tokenizerType === 'JS' 1082 && $tokens[$i]['code'] === T_OBJECT 1083 ) { 1084 $closer = $tokens[$i]['bracket_closer']; 1085 if ($tokens[$i]['line'] === $tokens[$closer]['line']) { 1086 if ($this->_debug === true) { 1087 $line = $tokens[$i]['line']; 1088 echo "* ignoring single-line JS object on line $line".PHP_EOL; 1089 } 1090 1091 $i = $closer; 1092 continue; 1093 } 1094 1095 if ($this->_debug === true) { 1096 $line = $tokens[$i]['line']; 1097 echo "Open JS object on line $line".PHP_EOL; 1098 } 1099 1100 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); 1101 $currentIndent = (($tokens[$first]['column'] - 1) + $this->indent); 1102 if (isset($adjustments[$first]) === true) { 1103 $currentIndent += $adjustments[$first]; 1104 } 1105 1106 // Make sure it is divisible by our expected indent. 1107 $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); 1108 $setIndents[$first] = $currentIndent; 1109 1110 if ($this->_debug === true) { 1111 $type = $tokens[$first]['type']; 1112 echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; 1113 } 1114 1115 continue; 1116 }//end if 1117 1118 // Closing an anon class or function. 1119 if (isset($tokens[$i]['scope_condition']) === true 1120 && $tokens[$i]['scope_closer'] === $i 1121 && ($tokens[$tokens[$i]['scope_condition']]['code'] === T_CLOSURE 1122 || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS) 1123 ) { 1124 if ($this->_debug === true) { 1125 $type = str_replace('_', ' ', strtolower(substr($tokens[$tokens[$i]['scope_condition']]['type'], 2))); 1126 $line = $tokens[$i]['line']; 1127 echo "Close $type on line $line".PHP_EOL; 1128 } 1129 1130 $prev = false; 1131 1132 $object = 0; 1133 if ($phpcsFile->tokenizerType === 'JS') { 1134 $conditions = $tokens[$i]['conditions']; 1135 krsort($conditions, SORT_NUMERIC); 1136 foreach ($conditions as $token => $condition) { 1137 if ($condition === T_OBJECT) { 1138 $object = $token; 1139 break; 1140 } 1141 } 1142 1143 if ($this->_debug === true && $object !== 0) { 1144 $line = $tokens[$object]['line']; 1145 echo "\t* token is inside JS object $object on line $line *".PHP_EOL; 1146 } 1147 } 1148 1149 $parens = 0; 1150 if (isset($tokens[$i]['nested_parenthesis']) === true 1151 && empty($tokens[$i]['nested_parenthesis']) === false 1152 ) { 1153 end($tokens[$i]['nested_parenthesis']); 1154 $parens = key($tokens[$i]['nested_parenthesis']); 1155 if ($this->_debug === true) { 1156 $line = $tokens[$parens]['line']; 1157 echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; 1158 } 1159 } 1160 1161 $condition = 0; 1162 if (isset($tokens[$i]['conditions']) === true 1163 && empty($tokens[$i]['conditions']) === false 1164 ) { 1165 end($tokens[$i]['conditions']); 1166 $condition = key($tokens[$i]['conditions']); 1167 if ($this->_debug === true) { 1168 $line = $tokens[$condition]['line']; 1169 $type = $tokens[$condition]['type']; 1170 echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; 1171 } 1172 } 1173 1174 if ($parens > $object && $parens > $condition) { 1175 if ($this->_debug === true) { 1176 echo "\t* using parenthesis *".PHP_EOL; 1177 } 1178 1179 $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($parens - 1), null, true); 1180 $object = 0; 1181 $condition = 0; 1182 } else if ($object > 0 && $object >= $condition) { 1183 if ($this->_debug === true) { 1184 echo "\t* using object *".PHP_EOL; 1185 } 1186 1187 $prev = $object; 1188 $parens = 0; 1189 $condition = 0; 1190 } else if ($condition > 0) { 1191 if ($this->_debug === true) { 1192 echo "\t* using condition *".PHP_EOL; 1193 } 1194 1195 $prev = $condition; 1196 $object = 0; 1197 $parens = 0; 1198 }//end if 1199 1200 if ($prev === false) { 1201 $prev = $phpcsFile->findPrevious(array(T_EQUAL, T_RETURN), ($tokens[$i]['scope_condition'] - 1), null, false, null, true); 1202 if ($prev === false) { 1203 $prev = $i; 1204 if ($this->_debug === true) { 1205 echo "\t* could not find a previous T_EQUAL or T_RETURN token; will use current token *".PHP_EOL; 1206 } 1207 } 1208 } 1209 1210 if ($this->_debug === true) { 1211 $line = $tokens[$prev]['line']; 1212 $type = $tokens[$prev]['type']; 1213 echo "\t* previous token is $type on line $line *".PHP_EOL; 1214 } 1215 1216 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); 1217 if ($this->_debug === true) { 1218 $line = $tokens[$first]['line']; 1219 $type = $tokens[$first]['type']; 1220 echo "\t* first token on line $line is $first ($type) *".PHP_EOL; 1221 } 1222 1223 $prev = $phpcsFile->findStartOfStatement($first); 1224 if ($prev !== $first) { 1225 // This is not the start of the statement. 1226 if ($this->_debug === true) { 1227 $line = $tokens[$prev]['line']; 1228 $type = $tokens[$prev]['type']; 1229 echo "\t* amended previous is $type on line $line *".PHP_EOL; 1230 } 1231 1232 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); 1233 if ($this->_debug === true) { 1234 $line = $tokens[$first]['line']; 1235 $type = $tokens[$first]['type']; 1236 echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; 1237 } 1238 } 1239 1240 $currentIndent = ($tokens[$first]['column'] - 1); 1241 if ($object > 0 || $condition > 0) { 1242 $currentIndent += $this->indent; 1243 } 1244 1245 if (isset($tokens[$first]['scope_closer']) === true 1246 && $tokens[$first]['scope_closer'] === $first 1247 ) { 1248 if ($this->_debug === true) { 1249 echo "\t* first token is a scope closer *".PHP_EOL; 1250 } 1251 1252 if ($condition === 0 || $tokens[$condition]['scope_opener'] < $first) { 1253 $currentIndent = $setIndents[$first]; 1254 } else if ($this->_debug === true) { 1255 echo "\t* ignoring scope closer *".PHP_EOL; 1256 } 1257 } 1258 1259 // Make sure it is divisible by our expected indent. 1260 $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); 1261 $setIndents[$first] = $currentIndent; 1262 1263 if ($this->_debug === true) { 1264 $type = $tokens[$first]['type']; 1265 echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; 1266 } 1267 }//end if 1268 }//end for 1269 1270 // Don't process the rest of the file. 1271 return $phpcsFile->numTokens; 1272 1273 }//end process() 1274 1275 1276}//end class 1277