comparison vendor/squizlabs/php_codesniffer/src/Sniffs/AbstractPatternSniff.php @ 17:129ea1e6d783

Update, including to Drupal core 8.6.10
author Chris Cannam
date Thu, 28 Feb 2019 13:21:36 +0000
parents
children af1871eacc83
comparison
equal deleted inserted replaced
16:c2387f117808 17:129ea1e6d783
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
10 namespace PHP_CodeSniffer\Sniffs;
11
12 use PHP_CodeSniffer\Files\File;
13 use PHP_CodeSniffer\Util\Tokens;
14 use PHP_CodeSniffer\Tokenizers\PHP;
15 use PHP_CodeSniffer\Exceptions\RuntimeException;
16
17 abstract 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 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) {
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];
264 if ($this->ignoreComments === true) {
265 $ignoreTokens
266 = array_merge($ignoreTokens, Tokens::$commentTokens);
267 }
268
269 $origStackPtr = $stackPtr;
270 $hasError = false;
271
272 if ($patternInfo['listen_pos'] > 0) {
273 $stackPtr--;
274
275 for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) {
276 if ($pattern[$i]['type'] === 'token') {
277 if ($pattern[$i]['token'] === T_WHITESPACE) {
278 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
279 $found = $tokens[$stackPtr]['content'].$found;
280 }
281
282 // Only check the size of the whitespace if this is not
283 // the first token. We don't care about the size of
284 // leading whitespace, just that there is some.
285 if ($i !== 0) {
286 if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) {
287 $hasError = true;
288 }
289 }
290 } else {
291 // Check to see if this important token is the same as the
292 // previous important token in the pattern. If it is not,
293 // then the pattern cannot be for this piece of code.
294 $prev = $phpcsFile->findPrevious(
295 $ignoreTokens,
296 $stackPtr,
297 null,
298 true
299 );
300
301 if ($prev === false
302 || $tokens[$prev]['code'] !== $pattern[$i]['token']
303 ) {
304 return false;
305 }
306
307 // If we skipped past some whitespace tokens, then add them
308 // to the found string.
309 $tokenContent = $phpcsFile->getTokensAsString(
310 ($prev + 1),
311 ($stackPtr - $prev - 1)
312 );
313
314 $found = $tokens[$prev]['content'].$tokenContent.$found;
315
316 if (isset($pattern[($i - 1)]) === true
317 && $pattern[($i - 1)]['type'] === 'skip'
318 ) {
319 $stackPtr = $prev;
320 } else {
321 $stackPtr = ($prev - 1);
322 }
323 }//end if
324 } else if ($pattern[$i]['type'] === 'skip') {
325 // Skip to next piece of relevant code.
326 if ($pattern[$i]['to'] === 'parenthesis_closer') {
327 $to = 'parenthesis_opener';
328 } else {
329 $to = 'scope_opener';
330 }
331
332 // Find the previous opener.
333 $next = $phpcsFile->findPrevious(
334 $ignoreTokens,
335 $stackPtr,
336 null,
337 true
338 );
339
340 if ($next === false || isset($tokens[$next][$to]) === false) {
341 // If there was not opener, then we must be
342 // using the wrong pattern.
343 return false;
344 }
345
346 if ($to === 'parenthesis_opener') {
347 $found = '{'.$found;
348 } else {
349 $found = '('.$found;
350 }
351
352 $found = '...'.$found;
353
354 // Skip to the opening token.
355 $stackPtr = ($tokens[$next][$to] - 1);
356 } else if ($pattern[$i]['type'] === 'string') {
357 $found = 'abc';
358 } else if ($pattern[$i]['type'] === 'newline') {
359 if ($this->ignoreComments === true
360 && isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true
361 ) {
362 $startComment = $phpcsFile->findPrevious(
363 Tokens::$commentTokens,
364 ($stackPtr - 1),
365 null,
366 true
367 );
368
369 if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) {
370 $startComment++;
371 }
372
373 $tokenContent = $phpcsFile->getTokensAsString(
374 $startComment,
375 ($stackPtr - $startComment + 1)
376 );
377
378 $found = $tokenContent.$found;
379 $stackPtr = ($startComment - 1);
380 }
381
382 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
383 if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) {
384 $found = $tokens[$stackPtr]['content'].$found;
385
386 // This may just be an indent that comes after a newline
387 // so check the token before to make sure. If it is a newline, we
388 // can ignore the error here.
389 if (($tokens[($stackPtr - 1)]['content'] !== $phpcsFile->eolChar)
390 && ($this->ignoreComments === true
391 && isset(Tokens::$commentTokens[$tokens[($stackPtr - 1)]['code']]) === false)
392 ) {
393 $hasError = true;
394 } else {
395 $stackPtr--;
396 }
397 } else {
398 $found = 'EOL'.$found;
399 }
400 } else {
401 $found = $tokens[$stackPtr]['content'].$found;
402 $hasError = true;
403 }//end if
404
405 if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') {
406 // Make sure they only have 1 newline.
407 $prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
408 if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
409 $hasError = true;
410 }
411 }
412 }//end if
413 }//end for
414 }//end if
415
416 $stackPtr = $origStackPtr;
417 $lastAddedStackPtr = null;
418 $patternLen = count($pattern);
419
420 for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
421 if (isset($tokens[$stackPtr]) === false) {
422 break;
423 }
424
425 if ($pattern[$i]['type'] === 'token') {
426 if ($pattern[$i]['token'] === T_WHITESPACE) {
427 if ($this->ignoreComments === true) {
428 // If we are ignoring comments, check to see if this current
429 // token is a comment. If so skip it.
430 if (isset(Tokens::$commentTokens[$tokens[$stackPtr]['code']]) === true) {
431 continue;
432 }
433
434 // If the next token is a comment, the we need to skip the
435 // current token as we should allow a space before a
436 // comment for readability.
437 if (isset($tokens[($stackPtr + 1)]) === true
438 && isset(Tokens::$commentTokens[$tokens[($stackPtr + 1)]['code']]) === true
439 ) {
440 continue;
441 }
442 }
443
444 $tokenContent = '';
445 if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
446 if (isset($pattern[($i + 1)]) === false) {
447 // This is the last token in the pattern, so just compare
448 // the next token of content.
449 $tokenContent = $tokens[$stackPtr]['content'];
450 } else {
451 // Get all the whitespace to the next token.
452 $next = $phpcsFile->findNext(
453 Tokens::$emptyTokens,
454 $stackPtr,
455 null,
456 true
457 );
458
459 $tokenContent = $phpcsFile->getTokensAsString(
460 $stackPtr,
461 ($next - $stackPtr)
462 );
463
464 $lastAddedStackPtr = $stackPtr;
465 $stackPtr = $next;
466 }//end if
467
468 if ($stackPtr !== $lastAddedStackPtr) {
469 $found .= $tokenContent;
470 }
471 } else {
472 if ($stackPtr !== $lastAddedStackPtr) {
473 $found .= $tokens[$stackPtr]['content'];
474 $lastAddedStackPtr = $stackPtr;
475 }
476 }//end if
477
478 if (isset($pattern[($i + 1)]) === true
479 && $pattern[($i + 1)]['type'] === 'skip'
480 ) {
481 // The next token is a skip token, so we just need to make
482 // sure the whitespace we found has *at least* the
483 // whitespace required.
484 if (strpos($tokenContent, $pattern[$i]['value']) !== 0) {
485 $hasError = true;
486 }
487 } else {
488 if ($tokenContent !== $pattern[$i]['value']) {
489 $hasError = true;
490 }
491 }
492 } else {
493 // Check to see if this important token is the same as the
494 // next important token in the pattern. If it is not, then
495 // the pattern cannot be for this piece of code.
496 $next = $phpcsFile->findNext(
497 $ignoreTokens,
498 $stackPtr,
499 null,
500 true
501 );
502
503 if ($next === false
504 || $tokens[$next]['code'] !== $pattern[$i]['token']
505 ) {
506 // The next important token did not match the pattern.
507 return false;
508 }
509
510 if ($lastAddedStackPtr !== null) {
511 if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET
512 || $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET)
513 && isset($tokens[$next]['scope_condition']) === true
514 && $tokens[$next]['scope_condition'] > $lastAddedStackPtr
515 ) {
516 // This is a brace, but the owner of it is after the current
517 // token, which means it does not belong to any token in
518 // our pattern. This means the pattern is not for us.
519 return false;
520 }
521
522 if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS
523 || $tokens[$next]['code'] === T_CLOSE_PARENTHESIS)
524 && isset($tokens[$next]['parenthesis_owner']) === true
525 && $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
526 ) {
527 // This is a bracket, but the owner of it is after the current
528 // token, which means it does not belong to any token in
529 // our pattern. This means the pattern is not for us.
530 return false;
531 }
532 }//end if
533
534 // If we skipped past some whitespace tokens, then add them
535 // to the found string.
536 if (($next - $stackPtr) > 0) {
537 $hasComment = false;
538 for ($j = $stackPtr; $j < $next; $j++) {
539 $found .= $tokens[$j]['content'];
540 if (isset(Tokens::$commentTokens[$tokens[$j]['code']]) === true) {
541 $hasComment = true;
542 }
543 }
544
545 // If we are not ignoring comments, this additional
546 // whitespace or comment is not allowed. If we are
547 // ignoring comments, there needs to be at least one
548 // comment for this to be allowed.
549 if ($this->ignoreComments === false
550 || ($this->ignoreComments === true
551 && $hasComment === false)
552 ) {
553 $hasError = true;
554 }
555
556 // Even when ignoring comments, we are not allowed to include
557 // newlines without the pattern specifying them, so
558 // everything should be on the same line.
559 if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
560 $hasError = true;
561 }
562 }//end if
563
564 if ($next !== $lastAddedStackPtr) {
565 $found .= $tokens[$next]['content'];
566 $lastAddedStackPtr = $next;
567 }
568
569 if (isset($pattern[($i + 1)]) === true
570 && $pattern[($i + 1)]['type'] === 'skip'
571 ) {
572 $stackPtr = $next;
573 } else {
574 $stackPtr = ($next + 1);
575 }
576 }//end if
577 } else if ($pattern[$i]['type'] === 'skip') {
578 if ($pattern[$i]['to'] === 'unknown') {
579 $next = $phpcsFile->findNext(
580 $pattern[($i + 1)]['token'],
581 $stackPtr
582 );
583
584 if ($next === false) {
585 // Couldn't find the next token, so we must
586 // be using the wrong pattern.
587 return false;
588 }
589
590 $found .= '...';
591 $stackPtr = $next;
592 } else {
593 // Find the previous opener.
594 $next = $phpcsFile->findPrevious(
595 Tokens::$blockOpeners,
596 $stackPtr
597 );
598
599 if ($next === false
600 || isset($tokens[$next][$pattern[$i]['to']]) === false
601 ) {
602 // If there was not opener, then we must
603 // be using the wrong pattern.
604 return false;
605 }
606
607 $found .= '...';
608 if ($pattern[$i]['to'] === 'parenthesis_closer') {
609 $found .= ')';
610 } else {
611 $found .= '}';
612 }
613
614 // Skip to the closing token.
615 $stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
616 }//end if
617 } else if ($pattern[$i]['type'] === 'string') {
618 if ($tokens[$stackPtr]['code'] !== T_STRING) {
619 $hasError = true;
620 }
621
622 if ($stackPtr !== $lastAddedStackPtr) {
623 $found .= 'abc';
624 $lastAddedStackPtr = $stackPtr;
625 }
626
627 $stackPtr++;
628 } else if ($pattern[$i]['type'] === 'newline') {
629 // Find the next token that contains a newline character.
630 $newline = 0;
631 for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) {
632 if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) {
633 $newline = $j;
634 break;
635 }
636 }
637
638 if ($newline === 0) {
639 // We didn't find a newline character in the rest of the file.
640 $next = ($phpcsFile->numTokens - 1);
641 $hasError = true;
642 } else {
643 if ($this->ignoreComments === false) {
644 // The newline character cannot be part of a comment.
645 if (isset(Tokens::$commentTokens[$tokens[$newline]['code']]) === true) {
646 $hasError = true;
647 }
648 }
649
650 if ($newline === $stackPtr) {
651 $next = ($stackPtr + 1);
652 } else {
653 // Check that there were no significant tokens that we
654 // skipped over to find our newline character.
655 $next = $phpcsFile->findNext(
656 $ignoreTokens,
657 $stackPtr,
658 null,
659 true
660 );
661
662 if ($next < $newline) {
663 // We skipped a non-ignored token.
664 $hasError = true;
665 } else {
666 $next = ($newline + 1);
667 }
668 }
669 }//end if
670
671 if ($stackPtr !== $lastAddedStackPtr) {
672 $found .= $phpcsFile->getTokensAsString(
673 $stackPtr,
674 ($next - $stackPtr)
675 );
676
677 $lastAddedStackPtr = ($next - 1);
678 }
679
680 $stackPtr = $next;
681 }//end if
682 }//end for
683
684 if ($hasError === true) {
685 $error = $this->prepareError($found, $patternCode);
686 $errors[$origStackPtr] = $error;
687 }
688
689 return $errors;
690
691 }//end processPattern()
692
693
694 /**
695 * Prepares an error for the specified patternCode.
696 *
697 * @param string $found The actual found string in the code.
698 * @param string $patternCode The expected pattern code.
699 *
700 * @return string The error message.
701 */
702 protected function prepareError($found, $patternCode)
703 {
704 $found = str_replace("\r\n", '\n', $found);
705 $found = str_replace("\n", '\n', $found);
706 $found = str_replace("\r", '\n', $found);
707 $found = str_replace("\t", '\t', $found);
708 $found = str_replace('EOL', '\n', $found);
709 $expected = str_replace('EOL', '\n', $patternCode);
710
711 $error = "Expected \"$expected\"; found \"$found\"";
712
713 return $error;
714
715 }//end prepareError()
716
717
718 /**
719 * Returns the patterns that should be checked.
720 *
721 * @return string[]
722 */
723 abstract protected function getPatterns();
724
725
726 /**
727 * Registers any supplementary tokens that this test might wish to process.
728 *
729 * A sniff may wish to register supplementary tests when it wishes to group
730 * an arbitrary validation that cannot be performed using a pattern, with
731 * other pattern tests.
732 *
733 * @return int[]
734 * @see processSupplementary()
735 */
736 protected function registerSupplementary()
737 {
738 return [];
739
740 }//end registerSupplementary()
741
742
743 /**
744 * Processes any tokens registered with registerSupplementary().
745 *
746 * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where to
747 * process the skip.
748 * @param int $stackPtr The position in the tokens stack to
749 * process.
750 *
751 * @return void
752 * @see registerSupplementary()
753 */
754 protected function processSupplementary(File $phpcsFile, $stackPtr)
755 {
756
757 }//end processSupplementary()
758
759
760 /**
761 * Parses a pattern string into an array of pattern steps.
762 *
763 * @param string $pattern The pattern to parse.
764 *
765 * @return array The parsed pattern array.
766 * @see createSkipPattern()
767 * @see createTokenPattern()
768 */
769 private function parse($pattern)
770 {
771 $patterns = [];
772 $length = strlen($pattern);
773 $lastToken = 0;
774 $firstToken = 0;
775
776 for ($i = 0; $i < $length; $i++) {
777 $specialPattern = false;
778 $isLastChar = ($i === ($length - 1));
779 $oldFirstToken = $firstToken;
780
781 if (substr($pattern, $i, 3) === '...') {
782 // It's a skip pattern. The skip pattern requires the
783 // content of the token in the "from" position and the token
784 // to skip to.
785 $specialPattern = $this->createSkipPattern($pattern, ($i - 1));
786 $lastToken = ($i - $firstToken);
787 $firstToken = ($i + 3);
788 $i += 2;
789
790 if ($specialPattern['to'] !== 'unknown') {
791 $firstToken++;
792 }
793 } else if (substr($pattern, $i, 3) === 'abc') {
794 $specialPattern = ['type' => 'string'];
795 $lastToken = ($i - $firstToken);
796 $firstToken = ($i + 3);
797 $i += 2;
798 } else if (substr($pattern, $i, 3) === 'EOL') {
799 $specialPattern = ['type' => 'newline'];
800 $lastToken = ($i - $firstToken);
801 $firstToken = ($i + 3);
802 $i += 2;
803 }//end if
804
805 if ($specialPattern !== false || $isLastChar === true) {
806 // If we are at the end of the string, don't worry about a limit.
807 if ($isLastChar === true) {
808 // Get the string from the end of the last skip pattern, if any,
809 // to the end of the pattern string.
810 $str = substr($pattern, $oldFirstToken);
811 } else {
812 // Get the string from the end of the last special pattern,
813 // if any, to the start of this special pattern.
814 if ($lastToken === 0) {
815 // Note that if the last special token was zero characters ago,
816 // there will be nothing to process so we can skip this bit.
817 // This happens if you have something like: EOL... in your pattern.
818 $str = '';
819 } else {
820 $str = substr($pattern, $oldFirstToken, $lastToken);
821 }
822 }
823
824 if ($str !== '') {
825 $tokenPatterns = $this->createTokenPattern($str);
826 foreach ($tokenPatterns as $tokenPattern) {
827 $patterns[] = $tokenPattern;
828 }
829 }
830
831 // Make sure we don't skip the last token.
832 if ($isLastChar === false && $i === ($length - 1)) {
833 $i--;
834 }
835 }//end if
836
837 // Add the skip pattern *after* we have processed
838 // all the tokens from the end of the last skip pattern
839 // to the start of this skip pattern.
840 if ($specialPattern !== false) {
841 $patterns[] = $specialPattern;
842 }
843 }//end for
844
845 return $patterns;
846
847 }//end parse()
848
849
850 /**
851 * Creates a skip pattern.
852 *
853 * @param string $pattern The pattern being parsed.
854 * @param string $from The token content that the skip pattern starts from.
855 *
856 * @return array The pattern step.
857 * @see createTokenPattern()
858 * @see parse()
859 */
860 private function createSkipPattern($pattern, $from)
861 {
862 $skip = ['type' => 'skip'];
863
864 $nestedParenthesis = 0;
865 $nestedBraces = 0;
866 for ($start = $from; $start >= 0; $start--) {
867 switch ($pattern[$start]) {
868 case '(':
869 if ($nestedParenthesis === 0) {
870 $skip['to'] = 'parenthesis_closer';
871 }
872
873 $nestedParenthesis--;
874 break;
875 case '{':
876 if ($nestedBraces === 0) {
877 $skip['to'] = 'scope_closer';
878 }
879
880 $nestedBraces--;
881 break;
882 case '}':
883 $nestedBraces++;
884 break;
885 case ')':
886 $nestedParenthesis++;
887 break;
888 }//end switch
889
890 if (isset($skip['to']) === true) {
891 break;
892 }
893 }//end for
894
895 if (isset($skip['to']) === false) {
896 $skip['to'] = 'unknown';
897 }
898
899 return $skip;
900
901 }//end createSkipPattern()
902
903
904 /**
905 * Creates a token pattern.
906 *
907 * @param string $str The tokens string that the pattern should match.
908 *
909 * @return array The pattern step.
910 * @see createSkipPattern()
911 * @see parse()
912 */
913 private function createTokenPattern($str)
914 {
915 // Don't add a space after the closing php tag as it will add a new
916 // whitespace token.
917 $tokenizer = new PHP('<?php '.$str.'?>', null);
918
919 // Remove the <?php tag from the front and the end php tag from the back.
920 $tokens = $tokenizer->getTokens();
921 $tokens = array_slice($tokens, 1, (count($tokens) - 2));
922
923 $patterns = [];
924 foreach ($tokens as $patternInfo) {
925 $patterns[] = [
926 'type' => 'token',
927 'token' => $patternInfo['code'],
928 'value' => $patternInfo['content'],
929 ];
930 }
931
932 return $patterns;
933
934 }//end createTokenPattern()
935
936
937 }//end class