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