3
* Processes pattern strings and checks that the code conforms to the pattern.
8
* @package PHP_CodeSniffer
9
* @author Greg Sherwood <gsherwood@squiz.net>
10
* @author Marc McIntyre <mmcintyre@squiz.net>
11
* @copyright 2006-2012 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
16
if (class_exists('PHP_CodeSniffer_Standards_IncorrectPatternException', true) === false) {
17
$error = 'Class PHP_CodeSniffer_Standards_IncorrectPatternException not found';
18
throw new PHP_CodeSniffer_Exception($error);
22
* Processes pattern strings and checks that the code conforms to the pattern.
24
* This test essentially checks that code is correctly formatted with whitespace.
27
* @package PHP_CodeSniffer
28
* @author Greg Sherwood <gsherwood@squiz.net>
29
* @author Marc McIntyre <mmcintyre@squiz.net>
30
* @copyright 2006-2012 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: 1.5.0RC2
33
* @link http://pear.php.net/package/PHP_CodeSniffer
35
abstract class PHP_CodeSniffer_Standards_AbstractPatternSniff implements PHP_CodeSniffer_Sniff
39
* If true, comments will be ignored if they are found in the code.
43
public $ignoreComments = false;
46
* The current file being checked.
50
protected $currFile = '';
53
* The parsed patterns array.
57
private $_parsedPatterns = array();
60
* Tokens that this sniff wishes to process outside of the patterns.
63
* @see registerSupplementary()
64
* @see processSupplementary()
66
private $_supplementaryTokens = array();
69
* Positions in the stack where errors have occurred.
73
private $_errorPos = array();
77
* Constructs a PHP_CodeSniffer_Standards_AbstractPatternSniff.
79
* @param boolean $ignoreComments If true, comments will be ignored.
81
public function __construct($ignoreComments=null)
83
// This is here for backwards compatibility.
84
if ($ignoreComments !== null) {
85
$this->ignoreComments = $ignoreComments;
88
$this->_supplementaryTokens = $this->registerSupplementary();
94
* Registers the tokens to listen to.
96
* Classes extending <i>AbstractPatternTest</i> should implement the
97
* <i>getPatterns()</i> method to register the patterns they wish to test.
102
public final function register()
104
$listenTypes = array();
105
$patterns = $this->getPatterns();
107
foreach ($patterns as $pattern) {
108
$parsedPattern = $this->_parse($pattern);
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;
116
$patternArray = array(
117
'listen_pos' => $pos,
118
'pattern' => $parsedPattern,
119
'pattern_code' => $pattern,
122
if (isset($this->_parsedPatterns[$tokenType]) === false) {
123
$this->_parsedPatterns[$tokenType] = array();
126
$this->_parsedPatterns[$tokenType][] = $patternArray;
130
return array_unique(array_merge($listenTypes, $this->_supplementaryTokens));
136
* Returns the token types that the specified pattern is checking for.
138
* Returned array is in the format:
141
* T_WHITESPACE => 0, // 0 is the position where the T_WHITESPACE token
142
* // should occur in the pattern.
146
* @param array $pattern The parsed pattern to find the acquire the token
149
* @return array(int => int)
151
private function _getPatternTokenTypes($pattern)
153
$tokenTypes = array();
154
foreach ($pattern as $pos => $patternInfo) {
155
if ($patternInfo['type'] === 'token') {
156
if (isset($tokenTypes[$patternInfo['token']]) === false) {
157
$tokenTypes[$patternInfo['token']] = $pos;
164
}//end _getPatternTokenTypes()
168
* Returns the position in the pattern that this test should register as
169
* a listener for the pattern.
171
* @param array $pattern The pattern to acquire the listener for.
173
* @return int The postition in the pattern that this test should register
175
* @throws PHP_CodeSniffer_Exception If we could not determine a token
178
private function _getListenerTokenPos($pattern)
180
$tokenTypes = $this->_getPatternTokenTypes($pattern);
181
$tokenCodes = array_keys($tokenTypes);
182
$token = PHP_CodeSniffer_Tokens::getHighestWeightedToken($tokenCodes);
184
// If we could not get a token.
185
if ($token === false) {
186
$error = 'Could not determine a token to listen for';
187
throw new PHP_CodeSniffer_Exception($error);
190
return $tokenTypes[$token];
192
}//end _getListenerTokenPos()
196
* Processes the test.
198
* @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where the
200
* @param int $stackPtr The postion in the tokens stack
201
* where the listening token type was
207
public final function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
209
$file = $phpcsFile->getFilename();
210
if ($this->currFile !== $file) {
211
// We have changed files, so clean up.
212
$this->_errorPos = array();
213
$this->currFile = $file;
216
$tokens = $phpcsFile->getTokens();
218
if (in_array($tokens[$stackPtr]['code'], $this->_supplementaryTokens) === true) {
219
$this->processSupplementary($phpcsFile, $stackPtr);
222
$type = $tokens[$stackPtr]['code'];
224
// If the type is not set, then it must have been a token registered
225
// with registerSupplementary().
226
if (isset($this->_parsedPatterns[$type]) === false) {
230
$allErrors = array();
232
// Loop over each pattern that is listening to the current token type
233
// that we are processing.
234
foreach ($this->_parsedPatterns[$type] as $patternInfo) {
235
// If processPattern returns false, then the pattern that we are
236
// checking the code with must not be designed to check that code.
237
$errors = $this->processPattern($patternInfo, $phpcsFile, $stackPtr);
238
if ($errors === false) {
239
// The pattern didn't match.
241
} else if (empty($errors) === true) {
242
// The pattern matched, but there were no errors.
246
foreach ($errors as $stackPtr => $error) {
247
if (isset($this->_errorPos[$stackPtr]) === false) {
248
$this->_errorPos[$stackPtr] = true;
249
$allErrors[$stackPtr] = $error;
254
foreach ($allErrors as $stackPtr => $error) {
255
$phpcsFile->addError($error, $stackPtr);
262
* Processes the pattern and verifies the code at $stackPtr.
264
* @param array $patternInfo Information about the pattern used
265
* for checking, which includes are
266
* parsed token representation of the
268
* @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where the
270
* @param int $stackPtr The postion in the tokens stack where
271
* the listening token type was found.
273
* @return array(errors)
275
protected function processPattern(
277
PHP_CodeSniffer_File $phpcsFile,
280
$tokens = $phpcsFile->getTokens();
281
$pattern = $patternInfo['pattern'];
282
$patternCode = $patternInfo['pattern_code'];
286
$ignoreTokens = array(T_WHITESPACE);
287
if ($this->ignoreComments === true) {
289
= array_merge($ignoreTokens, PHP_CodeSniffer_Tokens::$commentTokens);
292
$origStackPtr = $stackPtr;
295
if ($patternInfo['listen_pos'] > 0) {
298
for ($i = ($patternInfo['listen_pos'] - 1); $i >= 0; $i--) {
299
if ($pattern[$i]['type'] === 'token') {
300
if ($pattern[$i]['token'] === T_WHITESPACE) {
301
if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
302
$found = $tokens[$stackPtr]['content'].$found;
305
// Only check the size of the whitespace if this is not
306
// the first token. We don't care about the size of
307
// leading whitespace, just that there is some.
309
if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) {
314
// Check to see if this important token is the same as the
315
// previous important token in the pattern. If it is not,
316
// then the pattern cannot be for this piece of code.
317
$prev = $phpcsFile->findPrevious(
325
|| $tokens[$prev]['code'] !== $pattern[$i]['token']
330
// If we skipped past some whitespace tokens, then add them
331
// to the found string.
332
$tokenContent = $phpcsFile->getTokensAsString(
334
($stackPtr - $prev - 1)
337
$found = $tokens[$prev]['content'].$tokenContent.$found;
339
if (isset($pattern[($i - 1)]) === true
340
&& $pattern[($i - 1)]['type'] === 'skip'
344
$stackPtr = ($prev - 1);
347
} else if ($pattern[$i]['type'] === 'skip') {
348
// Skip to next piece of relevant code.
349
if ($pattern[$i]['to'] === 'parenthesis_closer') {
350
$to = 'parenthesis_opener';
352
$to = 'scope_opener';
355
// Find the previous opener.
356
$next = $phpcsFile->findPrevious(
363
if ($next === false || isset($tokens[$next][$to]) === false) {
364
// If there was not opener, then we must be
365
// using the wrong pattern.
369
if ($to === 'parenthesis_opener') {
375
$found = '...'.$found;
377
// Skip to the opening token.
378
$stackPtr = ($tokens[$next][$to] - 1);
379
} else if ($pattern[$i]['type'] === 'string') {
381
} else if ($pattern[$i]['type'] === 'newline') {
382
if ($this->ignoreComments === true
383
&& in_array($tokens[$stackPtr]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true
385
$startComment = $phpcsFile->findPrevious(
386
PHP_CodeSniffer_Tokens::$commentTokens,
392
if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1)]['line']) {
396
$tokenContent = $phpcsFile->getTokensAsString(
398
($stackPtr - $startComment + 1)
401
$found = $tokenContent.$found;
402
$stackPtr = ($startComment - 1);
405
if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
406
if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar) {
407
$found = $tokens[$stackPtr]['content'].$found;
409
// This may just be an indent that comes after a newline
410
// so check the token before to make sure. If it is a newline, we
411
// can ignore the error here.
412
if ($tokens[($stackPtr - 1)]['content'] !== $phpcsFile->eolChar) {
418
$found = 'EOL'.$found;
421
$found = $tokens[$stackPtr]['content'].$found;
425
if ($hasError === false && $pattern[($i - 1)]['type'] !== 'newline') {
426
// Make sure they only have 1 newline.
427
$prev = $phpcsFile->findPrevious($ignoreTokens, ($stackPtr - 1), null, true);
428
if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
436
$stackPtr = $origStackPtr;
437
$lastAddedStackPtr = null;
438
$patternLen = count($pattern);
440
for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
441
if ($pattern[$i]['type'] === 'token') {
442
if ($pattern[$i]['token'] === T_WHITESPACE) {
443
if ($this->ignoreComments === true) {
444
// If we are ignoring comments, check to see if this current
445
// token is a comment. If so skip it.
446
if (in_array($tokens[$stackPtr]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) {
450
// If the next token is a comment, the we need to skip the
451
// current token as we should allow a space before a
452
// comment for readability.
453
if (in_array($tokens[($stackPtr + 1)]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) {
459
if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
460
if (isset($pattern[($i + 1)]) === false) {
461
// This is the last token in the pattern, so just compare
462
// the next token of content.
463
$tokenContent = $tokens[$stackPtr]['content'];
465
// Get all the whitespace to the next token.
466
$next = $phpcsFile->findNext(
467
PHP_CodeSniffer_Tokens::$emptyTokens,
473
$tokenContent = $phpcsFile->getTokensAsString(
478
$lastAddedStackPtr = $stackPtr;
482
if ($stackPtr !== $lastAddedStackPtr) {
483
$found .= $tokenContent;
486
if ($stackPtr !== $lastAddedStackPtr) {
487
$found .= $tokens[$stackPtr]['content'];
488
$lastAddedStackPtr = $stackPtr;
492
if (isset($pattern[($i + 1)]) === true
493
&& $pattern[($i + 1)]['type'] === 'skip'
495
// The next token is a skip token, so we just need to make
496
// sure the whitespace we found has *at least* the
497
// whitespace required.
498
if (strpos($tokenContent, $pattern[$i]['value']) !== 0) {
502
if ($tokenContent !== $pattern[$i]['value']) {
507
// Check to see if this important token is the same as the
508
// next important token in the pattern. If it is not, then
509
// the pattern cannot be for this piece of code.
510
$next = $phpcsFile->findNext(
518
|| $tokens[$next]['code'] !== $pattern[$i]['token']
520
// The next important token did not match the pattern.
524
if ($lastAddedStackPtr !== null) {
525
if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET
526
|| $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET)
527
&& isset($tokens[$next]['scope_condition']) === true
528
&& $tokens[$next]['scope_condition'] > $lastAddedStackPtr
530
// This is a brace, but the owner of it is after the current
531
// token, which means it does not belong to any token in
532
// our pattern. This means the pattern is not for us.
536
if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS
537
|| $tokens[$next]['code'] === T_CLOSE_PARENTHESIS)
538
&& isset($tokens[$next]['parenthesis_owner']) === true
539
&& $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
541
// This is a bracket, but the owner of it is after the current
542
// token, which means it does not belong to any token in
543
// our pattern. This means the pattern is not for us.
548
// If we skipped past some whitespace tokens, then add them
549
// to the found string.
550
if (($next - $stackPtr) > 0) {
552
for ($j = $stackPtr; $j < $next; $j++) {
553
$found .= $tokens[$j]['content'];
554
if (in_array($tokens[$j]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) {
559
// If we are not ignoring comments, this additional
560
// whitespace or comment is not allowed. If we are
561
// ignoring comments, there needs to be at least one
562
// comment for this to be allowed.
563
if ($this->ignoreComments === false
564
|| ($this->ignoreComments === true
565
&& $hasComment === false)
570
// Even when ignoring comments, we are not allowed to include
571
// newlines without the pattern specifying them, so
572
// everything should be on the same line.
573
if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
578
if ($next !== $lastAddedStackPtr) {
579
$found .= $tokens[$next]['content'];
580
$lastAddedStackPtr = $next;
583
if (isset($pattern[($i + 1)]) === true
584
&& $pattern[($i + 1)]['type'] === 'skip'
588
$stackPtr = ($next + 1);
591
} else if ($pattern[$i]['type'] === 'skip') {
592
if ($pattern[$i]['to'] === 'unknown') {
593
$next = $phpcsFile->findNext(
594
$pattern[($i + 1)]['token'],
598
if ($next === false) {
599
// Couldn't find the next token, sowe we must
600
// be using the wrong pattern.
607
// Find the previous opener.
608
$next = $phpcsFile->findPrevious(
609
PHP_CodeSniffer_Tokens::$blockOpeners,
614
|| isset($tokens[$next][$pattern[$i]['to']]) === false
616
// If there was not opener, then we must
617
// be using the wrong pattern.
622
if ($pattern[$i]['to'] === 'parenthesis_closer') {
628
// Skip to the closing token.
629
$stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
631
} else if ($pattern[$i]['type'] === 'string') {
632
if ($tokens[$stackPtr]['code'] !== T_STRING) {
636
if ($stackPtr !== $lastAddedStackPtr) {
638
$lastAddedStackPtr = $stackPtr;
642
} else if ($pattern[$i]['type'] === 'newline') {
643
// Find the next token that contains a newline character.
645
for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) {
646
if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) {
652
if ($newline === 0) {
653
// We didn't find a newline character in the rest of the file.
654
$next = ($phpcsFile->numTokens - 1);
657
if ($this->ignoreComments === false) {
658
// The newline character cannot be part of a comment.
659
if (in_array($tokens[$newline]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) {
664
if ($newline === $stackPtr) {
665
$next = ($stackPtr + 1);
667
// Check that there were no significant tokens that we
668
// skipped over to find our newline character.
669
$next = $phpcsFile->findNext(
676
if ($next < $newline) {
677
// We skipped a non-ignored token.
680
$next = ($newline + 1);
685
if ($stackPtr !== $lastAddedStackPtr) {
686
$found .= $phpcsFile->getTokensAsString(
691
$diff = ($next - $stackPtr);
692
$lastAddedStackPtr = ($next - 1);
699
if ($hasError === true) {
700
$error = $this->prepareError($found, $patternCode);
701
$errors[$origStackPtr] = $error;
706
}//end processPattern()
710
* Prepares an error for the specified patternCode.
712
* @param string $found The actual found string in the code.
713
* @param string $patternCode The expected pattern code.
715
* @return string The error message.
717
protected function prepareError($found, $patternCode)
719
$found = str_replace("\r\n", '\n', $found);
720
$found = str_replace("\n", '\n', $found);
721
$found = str_replace("\r", '\n', $found);
722
$found = str_replace('EOL', '\n', $found);
723
$expected = str_replace('EOL', '\n', $patternCode);
725
$error = "Expected \"$expected\"; found \"$found\"";
729
}//end prepareError()
733
* Returns the patterns that should be checked.
735
* @return array(string)
737
protected abstract function getPatterns();
741
* Registers any supplementary tokens that this test might wish to process.
743
* A sniff may wish to register supplementary tests when it wishes to group
744
* an arbitary validation that cannot be performed using a pattern, with
745
* other pattern tests.
748
* @see processSupplementary()
750
protected function registerSupplementary()
754
}//end registerSupplementary()
758
* Processes any tokens registered with registerSupplementary().
760
* @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where to
762
* @param int $stackPtr The position in the tokens stack to
766
* @see registerSupplementary()
768
protected function processSupplementary(
769
PHP_CodeSniffer_File $phpcsFile,
773
}//end processSupplementary()
777
* Parses a pattern string into an array of pattern steps.
779
* @param string $pattern The pattern to parse.
781
* @return array The parsed pattern array.
782
* @see _createSkipPattern()
783
* @see _createTokenPattern()
785
private function _parse($pattern)
788
$length = strlen($pattern);
792
for ($i = 0; $i < $length; $i++) {
794
$specialPattern = false;
795
$isLastChar = ($i === ($length - 1));
796
$oldFirstToken = $firstToken;
798
if (substr($pattern, $i, 3) === '...') {
799
// It's a skip pattern. The skip pattern requires the
800
// content of the token in the "from" position and the token
802
$specialPattern = $this->_createSkipPattern($pattern, ($i - 1));
803
$lastToken = ($i - $firstToken);
804
$firstToken = ($i + 3);
807
if ($specialPattern['to'] !== 'unknown') {
810
} else if (substr($pattern, $i, 3) === 'abc') {
811
$specialPattern = array('type' => 'string');
812
$lastToken = ($i - $firstToken);
813
$firstToken = ($i + 3);
815
} else if (substr($pattern, $i, 3) === 'EOL') {
816
$specialPattern = array('type' => 'newline');
817
$lastToken = ($i - $firstToken);
818
$firstToken = ($i + 3);
822
if ($specialPattern !== false || $isLastChar === true) {
823
// If we are at the end of the string, don't worry about a limit.
824
if ($isLastChar === true) {
825
// Get the string from the end of the last skip pattern, if any,
826
// to the end of the pattern string.
827
$str = substr($pattern, $oldFirstToken);
829
// Get the string from the end of the last special pattern,
830
// if any, to the start of this special pattern.
831
if ($lastToken === 0) {
832
// Note that if the last special token was zero characters ago,
833
// there will be nothing to process so we can skip this bit.
834
// This happens if you have something like: EOL... in your pattern.
837
$str = substr($pattern, $oldFirstToken, $lastToken);
842
$tokenPatterns = $this->_createTokenPattern($str);
843
foreach ($tokenPatterns as $tokenPattern) {
844
$patterns[] = $tokenPattern;
848
// Make sure we don't skip the last token.
849
if ($isLastChar === false && $i === ($length - 1)) {
854
// Add the skip pattern *after* we have processed
855
// all the tokens from the end of the last skip pattern
856
// to the start of this skip pattern.
857
if ($specialPattern !== false) {
858
$patterns[] = $specialPattern;
868
* Creates a skip pattern.
870
* @param string $pattern The pattern being parsed.
871
* @param string $from The token content that the skip pattern starts from.
873
* @return array The pattern step.
874
* @see _createTokenPattern()
877
private function _createSkipPattern($pattern, $from)
879
$skip = array('type' => 'skip');
881
$nestedParenthesis = 0;
883
for ($start = $from; $start >= 0; $start--) {
884
switch ($pattern[$start]) {
886
if ($nestedParenthesis === 0) {
887
$skip['to'] = 'parenthesis_closer';
890
$nestedParenthesis--;
893
if ($nestedBraces === 0) {
894
$skip['to'] = 'scope_closer';
903
$nestedParenthesis++;
907
if (isset($skip['to']) === true) {
912
if (isset($skip['to']) === false) {
913
$skip['to'] = 'unknown';
918
}//end _createSkipPattern()
922
* Creates a token pattern.
924
* @param string $str The tokens string that the pattern should match.
926
* @return array The pattern step.
927
* @see _createSkipPattern()
930
private function _createTokenPattern($str)
932
// Don't add a space after the closing php tag as it will add a new
934
$tokens = token_get_all('<?php '.$str.'?>');
936
// Remove the <?php tag from the front and the end php tag from the back.
937
$tokens = array_slice($tokens, 1, (count($tokens) - 2));
939
foreach ($tokens as &$token) {
940
$token = PHP_CodeSniffer::standardiseToken($token);
944
foreach ($tokens as $patternInfo) {
947
'token' => $patternInfo['code'],
948
'value' => $patternInfo['content'],
954
}//end _createTokenPattern()