1<?php 2/** 3 * Processes pattern strings and checks that the code conforms to the pattern. 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 16if (class_exists('PHP_CodeSniffer_Standards_IncorrectPatternException', true) === false) { 17 $error = 'Class PHP_CodeSniffer_Standards_IncorrectPatternException not found'; 18 throw new PHP_CodeSniffer_Exception($error); 19} 20 21/** 22 * Processes pattern strings and checks that the code conforms to the pattern. 23 * 24 * This test essentially checks that code is correctly formatted with whitespace. 25 * 26 * @category PHP 27 * @package PHP_CodeSniffer 28 * @author Greg Sherwood <gsherwood@squiz.net> 29 * @author Marc McIntyre <mmcintyre@squiz.net> 30 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 31 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 32 * @version Release: @package_version@ 33 * @link http://pear.php.net/package/PHP_CodeSniffer 34 */ 35abstract class PHP_CodeSniffer_Standards_AbstractPatternSniff implements PHP_CodeSniffer_Sniff 36{ 37 38 /** 39 * If true, comments will be ignored if they are found in the code. 40 * 41 * @var boolean 42 */ 43 public $ignoreComments = false; 44 45 /** 46 * The current file being checked. 47 * 48 * @var string 49 */ 50 protected $currFile = ''; 51 52 /** 53 * The parsed patterns array. 54 * 55 * @var array 56 */ 57 private $_parsedPatterns = array(); 58 59 /** 60 * Tokens that this sniff wishes to process outside of the patterns. 61 * 62 * @var array(int) 63 * @see registerSupplementary() 64 * @see processSupplementary() 65 */ 66 private $_supplementaryTokens = array(); 67 68 /** 69 * Positions in the stack where errors have occurred. 70 * 71 * @var array() 72 */ 73 private $_errorPos = array(); 74 75 76 /** 77 * Constructs a PHP_CodeSniffer_Standards_AbstractPatternSniff. 78 * 79 * @param boolean $ignoreComments If true, comments will be ignored. 80 */ 81 public function __construct($ignoreComments=null) 82 { 83 // This is here for backwards compatibility. 84 if ($ignoreComments !== null) { 85 $this->ignoreComments = $ignoreComments; 86 } 87 88 $this->_supplementaryTokens = $this->registerSupplementary(); 89 90 }//end __construct() 91 92 93 /** 94 * Registers the tokens to listen to. 95 * 96 * Classes extending <i>AbstractPatternTest</i> should implement the 97 * <i>getPatterns()</i> method to register the patterns they wish to test. 98 * 99 * @return int[] 100 * @see process() 101 */ 102 public final function register() 103 { 104 $listenTypes = array(); 105 $patterns = $this->getPatterns(); 106 107 foreach ($patterns as $pattern) { 108 $parsedPattern = $this->_parse($pattern); 109 110 // Find a token position in the pattern that we can use 111 // for a listener token. 112 $pos = $this->_getListenerTokenPos($parsedPattern); 113 $tokenType = $parsedPattern[$pos]['token']; 114 $listenTypes[] = $tokenType; 115 116 $patternArray = array( 117 'listen_pos' => $pos, 118 'pattern' => $parsedPattern, 119 'pattern_code' => $pattern, 120 ); 121 122 if (isset($this->_parsedPatterns[$tokenType]) === false) { 123 $this->_parsedPatterns[$tokenType] = array(); 124 } 125 126 $this->_parsedPatterns[$tokenType][] = $patternArray; 127 }//end foreach 128 129 return array_unique(array_merge($listenTypes, $this->_supplementaryTokens)); 130 131 }//end register() 132 133 134 /** 135 * Returns the token types that the specified pattern is checking for. 136 * 137 * Returned array is in the format: 138 * <code> 139 * array( 140 * T_WHITESPACE => 0, // 0 is the position where the T_WHITESPACE token 141 * // should occur in the pattern. 142 * ); 143 * </code> 144 * 145 * @param array $pattern The parsed pattern to find the acquire the token 146 * types from. 147 * 148 * @return array<int, int> 149 */ 150 private function _getPatternTokenTypes($pattern) 151 { 152 $tokenTypes = array(); 153 foreach ($pattern as $pos => $patternInfo) { 154 if ($patternInfo['type'] === 'token') { 155 if (isset($tokenTypes[$patternInfo['token']]) === false) { 156 $tokenTypes[$patternInfo['token']] = $pos; 157 } 158 } 159 } 160 161 return $tokenTypes; 162 163 }//end _getPatternTokenTypes() 164 165 166 /** 167 * Returns the position in the pattern that this test should register as 168 * a listener for the pattern. 169 * 170 * @param array $pattern The pattern to acquire the listener for. 171 * 172 * @return int The position in the pattern that this test should register 173 * as the listener. 174 * @throws PHP_CodeSniffer_Exception If we could not determine a token 175 * to listen for. 176 */ 177 private function _getListenerTokenPos($pattern) 178 { 179 $tokenTypes = $this->_getPatternTokenTypes($pattern); 180 $tokenCodes = array_keys($tokenTypes); 181 $token = PHP_CodeSniffer_Tokens::getHighestWeightedToken($tokenCodes); 182 183 // If we could not get a token. 184 if ($token === false) { 185 $error = 'Could not determine a token to listen for'; 186 throw new PHP_CodeSniffer_Exception($error); 187 } 188 189 return $tokenTypes[$token]; 190 191 }//end _getListenerTokenPos() 192 193 194 /** 195 * Processes the test. 196 * 197 * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where the 198 * token occurred. 199 * @param int $stackPtr The position in the tokens stack 200 * where the listening token type was 201 * found. 202 * 203 * @return void 204 * @see register() 205 */ 206 public final function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 207 { 208 $file = $phpcsFile->getFilename(); 209 if ($this->currFile !== $file) { 210 // We have changed files, so clean up. 211 $this->_errorPos = array(); 212 $this->currFile = $file; 213 } 214 215 $tokens = $phpcsFile->getTokens(); 216 217 if (in_array($tokens[$stackPtr]['code'], $this->_supplementaryTokens) === true) { 218 $this->processSupplementary($phpcsFile, $stackPtr); 219 } 220 221 $type = $tokens[$stackPtr]['code']; 222 223 // If the type is not set, then it must have been a token registered 224 // with registerSupplementary(). 225 if (isset($this->_parsedPatterns[$type]) === false) { 226 return; 227 } 228 229 $allErrors = array(); 230 231 // Loop over each pattern that is listening to the current token type 232 // that we are processing. 233 foreach ($this->_parsedPatterns[$type] as $patternInfo) { 234 // If processPattern returns false, then the pattern that we are 235 // checking the code with must not be designed to check that code. 236 $errors = $this->processPattern($patternInfo, $phpcsFile, $stackPtr); 237 if ($errors === false) { 238 // The pattern didn't match. 239 continue; 240 } else if (empty($errors) === true) { 241 // The pattern matched, but there were no errors. 242 break; 243 } 244 245 foreach ($errors as $stackPtr => $error) { 246 if (isset($this->_errorPos[$stackPtr]) === false) { 247 $this->_errorPos[$stackPtr] = true; 248 $allErrors[$stackPtr] = $error; 249 } 250 } 251 } 252 253 foreach ($allErrors as $stackPtr => $error) { 254 $phpcsFile->addError($error, $stackPtr, 'Found'); 255 } 256 257 }//end process() 258 259 260 /** 261 * Processes the pattern and verifies the code at $stackPtr. 262 * 263 * @param array $patternInfo Information about the pattern used 264 * for checking, which includes are 265 * parsed token representation of the 266 * pattern. 267 * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where the 268 * token occurred. 269 * @param int $stackPtr The position in the tokens stack where 270 * the listening token type was found. 271 * 272 * @return array 273 */ 274 protected function processPattern( 275 $patternInfo, 276 PHP_CodeSniffer_File $phpcsFile, 277 $stackPtr 278 ) { 279 $tokens = $phpcsFile->getTokens(); 280 $pattern = $patternInfo['pattern']; 281 $patternCode = $patternInfo['pattern_code']; 282 $errors = array(); 283 $found = ''; 284 285 $ignoreTokens = array(T_WHITESPACE); 286 if ($this->ignoreComments === true) { 287 $ignoreTokens 288 = array_merge($ignoreTokens, PHP_CodeSniffer_Tokens::$commentTokens); 289 } 290 291 $origStackPtr = $stackPtr; 292 $hasError = false; 293 294 if ($patternInfo['listen_pos'] > 0) { 295 $stackPtr--; 296 297 for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) { 298 if ($pattern[$i]['type'] === 'token') { 299 if ($pattern[$i]['token'] === T_WHITESPACE) { 300 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) { 301 $found = $tokens[$stackPtr]['content'].$found; 302 } 303 304 // Only check the size of the whitespace if this is not 305 // the first token. We don't care about the size of 306 // leading whitespace, just that there is some. 307 if ($i !== 0) { 308 if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) { 309 $hasError = true; 310 } 311 } 312 } else { 313 // Check to see if this important token is the same as the 314 // previous important token in the pattern. If it is not, 315 // then the pattern cannot be for this piece of code. 316 $prev = $phpcsFile->findPrevious( 317 $ignoreTokens, 318 $stackPtr, 319 null, 320 true 321 ); 322 323 if ($prev === false 324 || $tokens[$prev]['code'] !== $pattern[$i]['token'] 325 ) { 326 return false; 327 } 328 329 // If we skipped past some whitespace tokens, then add them 330 // to the found string. 331 $tokenContent = $phpcsFile->getTokensAsString( 332 ($prev + 1), 333 ($stackPtr - $prev - 1) 334 ); 335 336 $found = $tokens[$prev]['content'].$tokenContent.$found; 337 338 if (isset($pattern[($i - 1)]) === true 339 && $pattern[($i - 1)]['type'] === 'skip' 340 ) { 341 $stackPtr = $prev; 342 } else { 343 $stackPtr = ($prev - 1); 344 } 345 }//end if 346 } else if ($pattern[$i]['type'] === 'skip') { 347 // Skip to next piece of relevant code. 348 if ($pattern[$i]['to'] === 'parenthesis_closer') { 349 $to = 'parenthesis_opener'; 350 } else { 351 $to = 'scope_opener'; 352 } 353 354 // Find the previous opener. 355 $next = $phpcsFile->findPrevious( 356 $ignoreTokens, 357 $stackPtr, 358 null, 359 true 360 ); 361 362 if ($next === false || isset($tokens[$next][$to]) === false) { 363 // If there was not opener, then we must be 364 // using the wrong pattern. 365 return false; 366 } 367 368 if ($to === 'parenthesis_opener') { 369 $found = '{'.$found; 370 } else { 371 $found = '('.$found; 372 } 373 374 $found = '...'.$found; 375 376 // Skip to the opening token. 377 $stackPtr = ($tokens[$next][$to] - 1); 378 } else if ($pattern[$i]['type'] === 'string') { 379 $found = 'abc'; 380 } else if ($pattern[$i]['type'] === 'newline') { 381 if ($this->ignoreComments === true 382 && isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true 383 ) { 384 $startComment = $phpcsFile->findPrevious( 385 PHP_CodeSniffer_Tokens::$commentTokens, 386 ($stackPtr - 1), 387 null, 388 true 389 ); 390 391 if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) { 392 $startComment++; 393 } 394 395 $tokenContent = $phpcsFile->getTokensAsString( 396 $startComment, 397 ($stackPtr - $startComment + 1) 398 ); 399 400 $found = $tokenContent.$found; 401 $stackPtr = ($startComment - 1); 402 } 403 404 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) { 405 if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) { 406 $found = $tokens[$stackPtr]['content'].$found; 407 408 // This may just be an indent that comes after a newline 409 // so check the token before to make sure. If it is a newline, we 410 // can ignore the error here. 411 if (($tokens[($stackPtr - 1)]['content'] !== $phpcsFile->eolChar) 412 && ($this->ignoreComments === true 413 && isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[($stackPtr - 1)]['code']]) === false) 414 ) { 415 $hasError = true; 416 } else { 417 $stackPtr--; 418 } 419 } else { 420 $found = 'EOL'.$found; 421 } 422 } else { 423 $found = $tokens[$stackPtr]['content'].$found; 424 $hasError = true; 425 }//end if 426 427 if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') { 428 // Make sure they only have 1 newline. 429 $prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true); 430 if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) { 431 $hasError = true; 432 } 433 } 434 }//end if 435 }//end for 436 }//end if 437 438 $stackPtr = $origStackPtr; 439 $lastAddedStackPtr = null; 440 $patternLen = count($pattern); 441 442 for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) { 443 if (isset($tokens[$stackPtr]) === false) { 444 break; 445 } 446 447 if ($pattern[$i]['type'] === 'token') { 448 if ($pattern[$i]['token'] === T_WHITESPACE) { 449 if ($this->ignoreComments === true) { 450 // If we are ignoring comments, check to see if this current 451 // token is a comment. If so skip it. 452 if (isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true) { 453 continue; 454 } 455 456 // If the next token is a comment, the we need to skip the 457 // current token as we should allow a space before a 458 // comment for readability. 459 if (isset($tokens[($stackPtr + 1)]) === true 460 && isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[($stackPtr + 1)]['code']]) === true 461 ) { 462 continue; 463 } 464 } 465 466 $tokenContent = ''; 467 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) { 468 if (isset($pattern[($i + 1)]) === false) { 469 // This is the last token in the pattern, so just compare 470 // the next token of content. 471 $tokenContent = $tokens[$stackPtr]['content']; 472 } else { 473 // Get all the whitespace to the next token. 474 $next = $phpcsFile->findNext( 475 PHP_CodeSniffer_Tokens::$emptyTokens, 476 $stackPtr, 477 null, 478 true 479 ); 480 481 $tokenContent = $phpcsFile->getTokensAsString( 482 $stackPtr, 483 ($next - $stackPtr) 484 ); 485 486 $lastAddedStackPtr = $stackPtr; 487 $stackPtr = $next; 488 }//end if 489 490 if ($stackPtr !== $lastAddedStackPtr) { 491 $found .= $tokenContent; 492 } 493 } else { 494 if ($stackPtr !== $lastAddedStackPtr) { 495 $found .= $tokens[$stackPtr]['content']; 496 $lastAddedStackPtr = $stackPtr; 497 } 498 }//end if 499 500 if (isset($pattern[($i + 1)]) === true 501 && $pattern[($i + 1)]['type'] === 'skip' 502 ) { 503 // The next token is a skip token, so we just need to make 504 // sure the whitespace we found has *at least* the 505 // whitespace required. 506 if (strpos($tokenContent, $pattern[$i]['value']) !== 0) { 507 $hasError = true; 508 } 509 } else { 510 if ($tokenContent !== $pattern[$i]['value']) { 511 $hasError = true; 512 } 513 } 514 } else { 515 // Check to see if this important token is the same as the 516 // next important token in the pattern. If it is not, then 517 // the pattern cannot be for this piece of code. 518 $next = $phpcsFile->findNext( 519 $ignoreTokens, 520 $stackPtr, 521 null, 522 true 523 ); 524 525 if ($next === false 526 || $tokens[$next]['code'] !== $pattern[$i]['token'] 527 ) { 528 // The next important token did not match the pattern. 529 return false; 530 } 531 532 if ($lastAddedStackPtr !== null) { 533 if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET 534 || $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET) 535 && isset($tokens[$next]['scope_condition']) === true 536 && $tokens[$next]['scope_condition'] > $lastAddedStackPtr 537 ) { 538 // This is a brace, but the owner of it is after the current 539 // token, which means it does not belong to any token in 540 // our pattern. This means the pattern is not for us. 541 return false; 542 } 543 544 if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS 545 || $tokens[$next]['code'] === T_CLOSE_PARENTHESIS) 546 && isset($tokens[$next]['parenthesis_owner']) === true 547 && $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr 548 ) { 549 // This is a bracket, but the owner of it is after the current 550 // token, which means it does not belong to any token in 551 // our pattern. This means the pattern is not for us. 552 return false; 553 } 554 }//end if 555 556 // If we skipped past some whitespace tokens, then add them 557 // to the found string. 558 if (($next - $stackPtr) > 0) { 559 $hasComment = false; 560 for ($j = $stackPtr; $j < $next; $j++) { 561 $found .= $tokens[$j]['content']; 562 if (isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[$j]['code']]) === true) { 563 $hasComment = true; 564 } 565 } 566 567 // If we are not ignoring comments, this additional 568 // whitespace or comment is not allowed. If we are 569 // ignoring comments, there needs to be at least one 570 // comment for this to be allowed. 571 if ($this->ignoreComments === false 572 || ($this->ignoreComments === true 573 && $hasComment === false) 574 ) { 575 $hasError = true; 576 } 577 578 // Even when ignoring comments, we are not allowed to include 579 // newlines without the pattern specifying them, so 580 // everything should be on the same line. 581 if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) { 582 $hasError = true; 583 } 584 }//end if 585 586 if ($next !== $lastAddedStackPtr) { 587 $found .= $tokens[$next]['content']; 588 $lastAddedStackPtr = $next; 589 } 590 591 if (isset($pattern[($i + 1)]) === true 592 && $pattern[($i + 1)]['type'] === 'skip' 593 ) { 594 $stackPtr = $next; 595 } else { 596 $stackPtr = ($next + 1); 597 } 598 }//end if 599 } else if ($pattern[$i]['type'] === 'skip') { 600 if ($pattern[$i]['to'] === 'unknown') { 601 $next = $phpcsFile->findNext( 602 $pattern[($i + 1)]['token'], 603 $stackPtr 604 ); 605 606 if ($next === false) { 607 // Couldn't find the next token, so we must 608 // be using the wrong pattern. 609 return false; 610 } 611 612 $found .= '...'; 613 $stackPtr = $next; 614 } else { 615 // Find the previous opener. 616 $next = $phpcsFile->findPrevious( 617 PHP_CodeSniffer_Tokens::$blockOpeners, 618 $stackPtr 619 ); 620 621 if ($next === false 622 || isset($tokens[$next][$pattern[$i]['to']]) === false 623 ) { 624 // If there was not opener, then we must 625 // be using the wrong pattern. 626 return false; 627 } 628 629 $found .= '...'; 630 if ($pattern[$i]['to'] === 'parenthesis_closer') { 631 $found .= ')'; 632 } else { 633 $found .= '}'; 634 } 635 636 // Skip to the closing token. 637 $stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1); 638 }//end if 639 } else if ($pattern[$i]['type'] === 'string') { 640 if ($tokens[$stackPtr]['code'] !== T_STRING) { 641 $hasError = true; 642 } 643 644 if ($stackPtr !== $lastAddedStackPtr) { 645 $found .= 'abc'; 646 $lastAddedStackPtr = $stackPtr; 647 } 648 649 $stackPtr++; 650 } else if ($pattern[$i]['type'] === 'newline') { 651 // Find the next token that contains a newline character. 652 $newline = 0; 653 for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) { 654 if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) { 655 $newline = $j; 656 break; 657 } 658 } 659 660 if ($newline === 0) { 661 // We didn't find a newline character in the rest of the file. 662 $next = ($phpcsFile->numTokens - 1); 663 $hasError = true; 664 } else { 665 if ($this->ignoreComments === false) { 666 // The newline character cannot be part of a comment. 667 if (isset(PHP_CodeSniffer_Tokens::$commentTokens[$tokens[$newline]['code']]) === true) { 668 $hasError = true; 669 } 670 } 671 672 if ($newline === $stackPtr) { 673 $next = ($stackPtr + 1); 674 } else { 675 // Check that there were no significant tokens that we 676 // skipped over to find our newline character. 677 $next = $phpcsFile->findNext( 678 $ignoreTokens, 679 $stackPtr, 680 null, 681 true 682 ); 683 684 if ($next < $newline) { 685 // We skipped a non-ignored token. 686 $hasError = true; 687 } else { 688 $next = ($newline + 1); 689 } 690 } 691 }//end if 692 693 if ($stackPtr !== $lastAddedStackPtr) { 694 $found .= $phpcsFile->getTokensAsString( 695 $stackPtr, 696 ($next - $stackPtr) 697 ); 698 699 $diff = ($next - $stackPtr); 700 $lastAddedStackPtr = ($next - 1); 701 } 702 703 $stackPtr = $next; 704 }//end if 705 }//end for 706 707 if ($hasError === true) { 708 $error = $this->prepareError($found, $patternCode); 709 $errors[$origStackPtr] = $error; 710 } 711 712 return $errors; 713 714 }//end processPattern() 715 716 717 /** 718 * Prepares an error for the specified patternCode. 719 * 720 * @param string $found The actual found string in the code. 721 * @param string $patternCode The expected pattern code. 722 * 723 * @return string The error message. 724 */ 725 protected function prepareError($found, $patternCode) 726 { 727 $found = str_replace("\r\n", '\n', $found); 728 $found = str_replace("\n", '\n', $found); 729 $found = str_replace("\r", '\n', $found); 730 $found = str_replace("\t", '\t', $found); 731 $found = str_replace('EOL', '\n', $found); 732 $expected = str_replace('EOL', '\n', $patternCode); 733 734 $error = "Expected \"$expected\"; found \"$found\""; 735 736 return $error; 737 738 }//end prepareError() 739 740 741 /** 742 * Returns the patterns that should be checked. 743 * 744 * @return string[] 745 */ 746 protected abstract function getPatterns(); 747 748 749 /** 750 * Registers any supplementary tokens that this test might wish to process. 751 * 752 * A sniff may wish to register supplementary tests when it wishes to group 753 * an arbitrary validation that cannot be performed using a pattern, with 754 * other pattern tests. 755 * 756 * @return int[] 757 * @see processSupplementary() 758 */ 759 protected function registerSupplementary() 760 { 761 return array(); 762 763 }//end registerSupplementary() 764 765 766 /** 767 * Processes any tokens registered with registerSupplementary(). 768 * 769 * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where to 770 * process the skip. 771 * @param int $stackPtr The position in the tokens stack to 772 * process. 773 * 774 * @return void 775 * @see registerSupplementary() 776 */ 777 protected function processSupplementary( 778 PHP_CodeSniffer_File $phpcsFile, 779 $stackPtr 780 ) { 781 782 }//end processSupplementary() 783 784 785 /** 786 * Parses a pattern string into an array of pattern steps. 787 * 788 * @param string $pattern The pattern to parse. 789 * 790 * @return array The parsed pattern array. 791 * @see _createSkipPattern() 792 * @see _createTokenPattern() 793 */ 794 private function _parse($pattern) 795 { 796 $patterns = array(); 797 $length = strlen($pattern); 798 $lastToken = 0; 799 $firstToken = 0; 800 801 for ($i = 0; $i < $length; $i++) { 802 $specialPattern = false; 803 $isLastChar = ($i === ($length - 1)); 804 $oldFirstToken = $firstToken; 805 806 if (substr($pattern, $i, 3) === '...') { 807 // It's a skip pattern. The skip pattern requires the 808 // content of the token in the "from" position and the token 809 // to skip to. 810 $specialPattern = $this->_createSkipPattern($pattern, ($i - 1)); 811 $lastToken = ($i - $firstToken); 812 $firstToken = ($i + 3); 813 $i = ($i + 2); 814 815 if ($specialPattern['to'] !== 'unknown') { 816 $firstToken++; 817 } 818 } else if (substr($pattern, $i, 3) === 'abc') { 819 $specialPattern = array('type' => 'string'); 820 $lastToken = ($i - $firstToken); 821 $firstToken = ($i + 3); 822 $i = ($i + 2); 823 } else if (substr($pattern, $i, 3) === 'EOL') { 824 $specialPattern = array('type' => 'newline'); 825 $lastToken = ($i - $firstToken); 826 $firstToken = ($i + 3); 827 $i = ($i + 2); 828 }//end if 829 830 if ($specialPattern !== false || $isLastChar === true) { 831 // If we are at the end of the string, don't worry about a limit. 832 if ($isLastChar === true) { 833 // Get the string from the end of the last skip pattern, if any, 834 // to the end of the pattern string. 835 $str = substr($pattern, $oldFirstToken); 836 } else { 837 // Get the string from the end of the last special pattern, 838 // if any, to the start of this special pattern. 839 if ($lastToken === 0) { 840 // Note that if the last special token was zero characters ago, 841 // there will be nothing to process so we can skip this bit. 842 // This happens if you have something like: EOL... in your pattern. 843 $str = ''; 844 } else { 845 $str = substr($pattern, $oldFirstToken, $lastToken); 846 } 847 } 848 849 if ($str !== '') { 850 $tokenPatterns = $this->_createTokenPattern($str); 851 foreach ($tokenPatterns as $tokenPattern) { 852 $patterns[] = $tokenPattern; 853 } 854 } 855 856 // Make sure we don't skip the last token. 857 if ($isLastChar === false && $i === ($length - 1)) { 858 $i--; 859 } 860 }//end if 861 862 // Add the skip pattern *after* we have processed 863 // all the tokens from the end of the last skip pattern 864 // to the start of this skip pattern. 865 if ($specialPattern !== false) { 866 $patterns[] = $specialPattern; 867 } 868 }//end for 869 870 return $patterns; 871 872 }//end _parse() 873 874 875 /** 876 * Creates a skip pattern. 877 * 878 * @param string $pattern The pattern being parsed. 879 * @param string $from The token content that the skip pattern starts from. 880 * 881 * @return array The pattern step. 882 * @see _createTokenPattern() 883 * @see _parse() 884 */ 885 private function _createSkipPattern($pattern, $from) 886 { 887 $skip = array('type' => 'skip'); 888 889 $nestedParenthesis = 0; 890 $nestedBraces = 0; 891 for ($start = $from; $start >= 0; $start--) { 892 switch ($pattern[$start]) { 893 case '(': 894 if ($nestedParenthesis === 0) { 895 $skip['to'] = 'parenthesis_closer'; 896 } 897 898 $nestedParenthesis--; 899 break; 900 case '{': 901 if ($nestedBraces === 0) { 902 $skip['to'] = 'scope_closer'; 903 } 904 905 $nestedBraces--; 906 break; 907 case '}': 908 $nestedBraces++; 909 break; 910 case ')': 911 $nestedParenthesis++; 912 break; 913 }//end switch 914 915 if (isset($skip['to']) === true) { 916 break; 917 } 918 }//end for 919 920 if (isset($skip['to']) === false) { 921 $skip['to'] = 'unknown'; 922 } 923 924 return $skip; 925 926 }//end _createSkipPattern() 927 928 929 /** 930 * Creates a token pattern. 931 * 932 * @param string $str The tokens string that the pattern should match. 933 * 934 * @return array The pattern step. 935 * @see _createSkipPattern() 936 * @see _parse() 937 */ 938 private function _createTokenPattern($str) 939 { 940 // Don't add a space after the closing php tag as it will add a new 941 // whitespace token. 942 $tokenizer = new PHP_CodeSniffer_Tokenizers_PHP(); 943 $tokens = $tokenizer->tokenizeString('<?php '.$str.'?>'); 944 945 // Remove the <?php tag from the front and the end php tag from the back. 946 $tokens = array_slice($tokens, 1, (count($tokens) - 2)); 947 948 $patterns = array(); 949 foreach ($tokens as $patternInfo) { 950 $patterns[] = array( 951 'type' => 'token', 952 'token' => $patternInfo['code'], 953 'value' => $patternInfo['content'], 954 ); 955 } 956 957 return $patterns; 958 959 }//end _createTokenPattern() 960 961 962}//end class 963