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