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