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-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
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-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: 1.5.4
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.
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)
413
&& ($this->ignoreComments === true && in_array($tokens[($stackPtr - 1)]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === false)
420
$found = 'EOL'.$found;
423
$found = $tokens[$stackPtr]['content'].$found;
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']) {
438
$stackPtr = $origStackPtr;
439
$lastAddedStackPtr = null;
440
$patternLen = count($pattern);
442
for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++) {
443
if ($pattern[$i]['type'] === 'token') {
444
if ($pattern[$i]['token'] === T_WHITESPACE) {
445
if ($this->ignoreComments === true) {
446
// If we are ignoring comments, check to see if this current
447
// token is a comment. If so skip it.
448
if (in_array($tokens[$stackPtr]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) {
452
// If the next token is a comment, the we need to skip the
453
// current token as we should allow a space before a
454
// comment for readability.
455
if (in_array($tokens[($stackPtr + 1)]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) {
461
if ($tokens[$stackPtr]['code'] === T_WHITESPACE) {
462
if (isset($pattern[($i + 1)]) === false) {
463
// This is the last token in the pattern, so just compare
464
// the next token of content.
465
$tokenContent = $tokens[$stackPtr]['content'];
467
// Get all the whitespace to the next token.
468
$next = $phpcsFile->findNext(
469
PHP_CodeSniffer_Tokens::$emptyTokens,
475
$tokenContent = $phpcsFile->getTokensAsString(
480
$lastAddedStackPtr = $stackPtr;
484
if ($stackPtr !== $lastAddedStackPtr) {
485
$found .= $tokenContent;
488
if ($stackPtr !== $lastAddedStackPtr) {
489
$found .= $tokens[$stackPtr]['content'];
490
$lastAddedStackPtr = $stackPtr;
494
if (isset($pattern[($i + 1)]) === true
495
&& $pattern[($i + 1)]['type'] === 'skip'
497
// The next token is a skip token, so we just need to make
498
// sure the whitespace we found has *at least* the
499
// whitespace required.
500
if (strpos($tokenContent, $pattern[$i]['value']) !== 0) {
504
if ($tokenContent !== $pattern[$i]['value']) {
509
// Check to see if this important token is the same as the
510
// next important token in the pattern. If it is not, then
511
// the pattern cannot be for this piece of code.
512
$next = $phpcsFile->findNext(
520
|| $tokens[$next]['code'] !== $pattern[$i]['token']
522
// The next important token did not match the pattern.
526
if ($lastAddedStackPtr !== null) {
527
if (($tokens[$next]['code'] === T_OPEN_CURLY_BRACKET
528
|| $tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET)
529
&& isset($tokens[$next]['scope_condition']) === true
530
&& $tokens[$next]['scope_condition'] > $lastAddedStackPtr
532
// This is a brace, but the owner of it is after the current
533
// token, which means it does not belong to any token in
534
// our pattern. This means the pattern is not for us.
538
if (($tokens[$next]['code'] === T_OPEN_PARENTHESIS
539
|| $tokens[$next]['code'] === T_CLOSE_PARENTHESIS)
540
&& isset($tokens[$next]['parenthesis_owner']) === true
541
&& $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
543
// This is a bracket, but the owner of it is after the current
544
// token, which means it does not belong to any token in
545
// our pattern. This means the pattern is not for us.
550
// If we skipped past some whitespace tokens, then add them
551
// to the found string.
552
if (($next - $stackPtr) > 0) {
554
for ($j = $stackPtr; $j < $next; $j++) {
555
$found .= $tokens[$j]['content'];
556
if (in_array($tokens[$j]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) {
561
// If we are not ignoring comments, this additional
562
// whitespace or comment is not allowed. If we are
563
// ignoring comments, there needs to be at least one
564
// comment for this to be allowed.
565
if ($this->ignoreComments === false
566
|| ($this->ignoreComments === true
567
&& $hasComment === false)
572
// Even when ignoring comments, we are not allowed to include
573
// newlines without the pattern specifying them, so
574
// everything should be on the same line.
575
if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
580
if ($next !== $lastAddedStackPtr) {
581
$found .= $tokens[$next]['content'];
582
$lastAddedStackPtr = $next;
585
if (isset($pattern[($i + 1)]) === true
586
&& $pattern[($i + 1)]['type'] === 'skip'
590
$stackPtr = ($next + 1);
593
} else if ($pattern[$i]['type'] === 'skip') {
594
if ($pattern[$i]['to'] === 'unknown') {
595
$next = $phpcsFile->findNext(
596
$pattern[($i + 1)]['token'],
600
if ($next === false) {
601
// Couldn't find the next token, sowe we must
602
// be using the wrong pattern.
609
// Find the previous opener.
610
$next = $phpcsFile->findPrevious(
611
PHP_CodeSniffer_Tokens::$blockOpeners,
616
|| isset($tokens[$next][$pattern[$i]['to']]) === false
618
// If there was not opener, then we must
619
// be using the wrong pattern.
624
if ($pattern[$i]['to'] === 'parenthesis_closer') {
630
// Skip to the closing token.
631
$stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1);
633
} else if ($pattern[$i]['type'] === 'string') {
634
if ($tokens[$stackPtr]['code'] !== T_STRING) {
638
if ($stackPtr !== $lastAddedStackPtr) {
640
$lastAddedStackPtr = $stackPtr;
644
} else if ($pattern[$i]['type'] === 'newline') {
645
// Find the next token that contains a newline character.
647
for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++) {
648
if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) {
654
if ($newline === 0) {
655
// We didn't find a newline character in the rest of the file.
656
$next = ($phpcsFile->numTokens - 1);
659
if ($this->ignoreComments === false) {
660
// The newline character cannot be part of a comment.
661
if (in_array($tokens[$newline]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) {
666
if ($newline === $stackPtr) {
667
$next = ($stackPtr + 1);
669
// Check that there were no significant tokens that we
670
// skipped over to find our newline character.
671
$next = $phpcsFile->findNext(
678
if ($next < $newline) {
679
// We skipped a non-ignored token.
682
$next = ($newline + 1);
687
if ($stackPtr !== $lastAddedStackPtr) {
688
$found .= $phpcsFile->getTokensAsString(
693
$diff = ($next - $stackPtr);
694
$lastAddedStackPtr = ($next - 1);
701
if ($hasError === true) {
702
$error = $this->prepareError($found, $patternCode);
703
$errors[$origStackPtr] = $error;
708
}//end processPattern()
712
* Prepares an error for the specified patternCode.
714
* @param string $found The actual found string in the code.
715
* @param string $patternCode The expected pattern code.
717
* @return string The error message.
719
protected function prepareError($found, $patternCode)
721
$found = str_replace("\r\n", '\n', $found);
722
$found = str_replace("\n", '\n', $found);
723
$found = str_replace("\r", '\n', $found);
724
$found = str_replace("\t", '\t', $found);
725
$found = str_replace('EOL', '\n', $found);
726
$expected = str_replace('EOL', '\n', $patternCode);
728
$error = "Expected \"$expected\"; found \"$found\"";
732
}//end prepareError()
736
* Returns the patterns that should be checked.
740
protected abstract function getPatterns();
744
* Registers any supplementary tokens that this test might wish to process.
746
* A sniff may wish to register supplementary tests when it wishes to group
747
* an arbitary validation that cannot be performed using a pattern, with
748
* other pattern tests.
751
* @see processSupplementary()
753
protected function registerSupplementary()
757
}//end registerSupplementary()
761
* Processes any tokens registered with registerSupplementary().
763
* @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where to
765
* @param int $stackPtr The position in the tokens stack to
769
* @see registerSupplementary()
771
protected function processSupplementary(
772
PHP_CodeSniffer_File $phpcsFile,
776
}//end processSupplementary()
780
* Parses a pattern string into an array of pattern steps.
782
* @param string $pattern The pattern to parse.
784
* @return array The parsed pattern array.
785
* @see _createSkipPattern()
786
* @see _createTokenPattern()
788
private function _parse($pattern)
791
$length = strlen($pattern);
795
for ($i = 0; $i < $length; $i++) {
797
$specialPattern = false;
798
$isLastChar = ($i === ($length - 1));
799
$oldFirstToken = $firstToken;
801
if (substr($pattern, $i, 3) === '...') {
802
// It's a skip pattern. The skip pattern requires the
803
// content of the token in the "from" position and the token
805
$specialPattern = $this->_createSkipPattern($pattern, ($i - 1));
806
$lastToken = ($i - $firstToken);
807
$firstToken = ($i + 3);
810
if ($specialPattern['to'] !== 'unknown') {
813
} else if (substr($pattern, $i, 3) === 'abc') {
814
$specialPattern = array('type' => 'string');
815
$lastToken = ($i - $firstToken);
816
$firstToken = ($i + 3);
818
} else if (substr($pattern, $i, 3) === 'EOL') {
819
$specialPattern = array('type' => 'newline');
820
$lastToken = ($i - $firstToken);
821
$firstToken = ($i + 3);
825
if ($specialPattern !== false || $isLastChar === true) {
826
// If we are at the end of the string, don't worry about a limit.
827
if ($isLastChar === true) {
828
// Get the string from the end of the last skip pattern, if any,
829
// to the end of the pattern string.
830
$str = substr($pattern, $oldFirstToken);
832
// Get the string from the end of the last special pattern,
833
// if any, to the start of this special pattern.
834
if ($lastToken === 0) {
835
// Note that if the last special token was zero characters ago,
836
// there will be nothing to process so we can skip this bit.
837
// This happens if you have something like: EOL... in your pattern.
840
$str = substr($pattern, $oldFirstToken, $lastToken);
845
$tokenPatterns = $this->_createTokenPattern($str);
846
foreach ($tokenPatterns as $tokenPattern) {
847
$patterns[] = $tokenPattern;
851
// Make sure we don't skip the last token.
852
if ($isLastChar === false && $i === ($length - 1)) {
857
// Add the skip pattern *after* we have processed
858
// all the tokens from the end of the last skip pattern
859
// to the start of this skip pattern.
860
if ($specialPattern !== false) {
861
$patterns[] = $specialPattern;
871
* Creates a skip pattern.
873
* @param string $pattern The pattern being parsed.
874
* @param string $from The token content that the skip pattern starts from.
876
* @return array The pattern step.
877
* @see _createTokenPattern()
880
private function _createSkipPattern($pattern, $from)
882
$skip = array('type' => 'skip');
884
$nestedParenthesis = 0;
886
for ($start = $from; $start >= 0; $start--) {
887
switch ($pattern[$start]) {
889
if ($nestedParenthesis === 0) {
890
$skip['to'] = 'parenthesis_closer';
893
$nestedParenthesis--;
896
if ($nestedBraces === 0) {
897
$skip['to'] = 'scope_closer';
906
$nestedParenthesis++;
910
if (isset($skip['to']) === true) {
915
if (isset($skip['to']) === false) {
916
$skip['to'] = 'unknown';
921
}//end _createSkipPattern()
925
* Creates a token pattern.
927
* @param string $str The tokens string that the pattern should match.
929
* @return array The pattern step.
930
* @see _createSkipPattern()
933
private function _createTokenPattern($str)
935
// Don't add a space after the closing php tag as it will add a new
937
$tokens = token_get_all('<?php '.$str.'?>');
939
// Remove the <?php tag from the front and the end php tag from the back.
940
$tokens = array_slice($tokens, 1, (count($tokens) - 2));
942
foreach ($tokens as &$token) {
943
$token = PHP_CodeSniffer::standardiseToken($token);
947
foreach ($tokens as $patternInfo) {
950
'token' => $patternInfo['code'],
951
'value' => $patternInfo['content'],
957
}//end _createTokenPattern()