Source for file AbstractPatternSniff.php
Documentation is available at AbstractPatternSniff.php
* Processes pattern strings and checks that the code conforms to the pattern.
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
namespace PHP_CodeSniffer\Sniffs;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Util\Tokens;
use PHP_CodeSniffer\Tokenizers\PHP;
use PHP_CodeSniffer\Exceptions\RuntimeException;
abstract class AbstractPatternSniff implements Sniff
* If true, comments will be ignored if they are found in the code.
public $ignoreComments = false;
* The current file being checked.
protected $currFile = '';
* The parsed patterns array.
private $parsedPatterns = array ();
* Tokens that this sniff wishes to process outside of the patterns.
* @see registerSupplementary()
* @see processSupplementary()
private $supplementaryTokens = array ();
* Positions in the stack where errors have occurred.
private $errorPos = array ();
* Constructs a AbstractPatternSniff.
* @param boolean $ignoreComments If true, comments will be ignored.
public function __construct ($ignoreComments=null )
// This is here for backwards compatibility.
if ($ignoreComments !== null ) {
$this->ignoreComments = $ignoreComments;
$this->supplementaryTokens = $this->registerSupplementary ();
* Registers the tokens to listen to.
* Classes extending <i>AbstractPatternTest</i> should implement the
* <i>getPatterns()</i> method to register the patterns they wish to test.
final public function register ()
$patterns = $this->getPatterns ();
foreach ($patterns as $pattern) {
$parsedPattern = $this->parse ($pattern);
// Find a token position in the pattern that we can use
$pos = $this->getListenerTokenPos ($parsedPattern);
$tokenType = $parsedPattern[$pos]['token'];
$listenTypes[] = $tokenType;
'pattern' => $parsedPattern,
'pattern_code' => $pattern,
if (isset ($this->parsedPatterns[$tokenType]) === false ) {
$this->parsedPatterns[$tokenType] = array ();
$this->parsedPatterns[$tokenType][] = $patternArray;
* Returns the token types that the specified pattern is checking for.
* Returned array is in the format:
* T_WHITESPACE => 0, // 0 is the position where the T_WHITESPACE token
* // should occur in the pattern.
* @param array $pattern The parsed pattern to find the acquire the token
* @return array<int, int>
private function getPatternTokenTypes ($pattern)
foreach ($pattern as $pos => $patternInfo) {
if ($patternInfo['type'] === 'token') {
if (isset ($tokenTypes[$patternInfo['token']]) === false ) {
$tokenTypes[$patternInfo['token']] = $pos;
}//end getPatternTokenTypes()
* Returns the position in the pattern that this test should register as
* a listener for the pattern.
* @param array $pattern The pattern to acquire the listener for.
* @return int The position in the pattern that this test should register
* @throws RuntimeException If we could not determine a token to listen for.
private function getListenerTokenPos ($pattern)
$tokenTypes = $this->getPatternTokenTypes ($pattern);
$token = Tokens ::getHighestWeightedToken ($tokenCodes);
// If we could not get a token.
$error = 'Could not determine a token to listen for';
throw new RuntimeException ($error);
return $tokenTypes[$token];
}//end getListenerTokenPos()
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* @param int $stackPtr The position in the tokens stack
* where the listening token type
final public function process (File $phpcsFile, $stackPtr)
$file = $phpcsFile->getFilename ();
if ($this->currFile !== $file) {
// We have changed files, so clean up.
$this->errorPos = array ();
$tokens = $phpcsFile->getTokens ();
if (in_array($tokens[$stackPtr]['code'], $this->supplementaryTokens) === true ) {
$this->processSupplementary ($phpcsFile, $stackPtr);
$type = $tokens[$stackPtr]['code'];
// If the type is not set, then it must have been a token registered
// with registerSupplementary().
if (isset ($this->parsedPatterns[$type]) === false ) {
// Loop over each pattern that is listening to the current token type
// that we are processing.
foreach ($this->parsedPatterns[$type] as $patternInfo) {
// If processPattern returns false, then the pattern that we are
// checking the code with must not be designed to check that code.
$errors = $this->processPattern ($patternInfo, $phpcsFile, $stackPtr);
// The pattern didn't match.
} else if (empty ($errors) === true ) {
// The pattern matched, but there were no errors.
foreach ($errors as $stackPtr => $error) {
if (isset ($this->errorPos[$stackPtr]) === false ) {
$this->errorPos[$stackPtr] = true;
$allErrors[$stackPtr] = $error;
foreach ($allErrors as $stackPtr => $error) {
$phpcsFile->addError ($error, $stackPtr, 'Found');
* Processes the pattern and verifies the code at $stackPtr.
* @param array $patternInfo Information about the pattern used
* for checking, which includes are
* parsed token representation of the
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* @param int $stackPtr The position in the tokens stack where
* the listening token type was found.
protected function processPattern ($patternInfo, File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens ();
$pattern = $patternInfo['pattern'];
$patternCode = $patternInfo['pattern_code'];
$ignoreTokens = array (T_WHITESPACE );
if ($this->ignoreComments === true ) {
$origStackPtr = $stackPtr;
if ($patternInfo['listen_pos'] > 0 ) {
for ($i = ($patternInfo['listen_pos'] - 1 ); $i >= 0; $i-- ) {
if ($pattern[$i]['type'] === 'token') {
if ($pattern[$i]['token'] === T_WHITESPACE ) {
if ($tokens[$stackPtr]['code'] === T_WHITESPACE ) {
$found = $tokens[$stackPtr]['content']. $found;
// Only check the size of the whitespace if this is not
// the first token. We don't care about the size of
// leading whitespace, just that there is some.
if ($tokens[$stackPtr]['content'] !== $pattern[$i]['value']) {
// Check to see if this important token is the same as the
// previous important token in the pattern. If it is not,
// then the pattern cannot be for this piece of code.
$prev = $phpcsFile->findPrevious (
|| $tokens[$prev]['code'] !== $pattern[$i]['token']
// If we skipped past some whitespace tokens, then add them
$tokenContent = $phpcsFile->getTokensAsString (
$found = $tokens[$prev]['content']. $tokenContent. $found;
if (isset ($pattern[($i - 1 )]) === true
&& $pattern[($i - 1 )]['type'] === 'skip'
} else if ($pattern[$i]['type'] === 'skip') {
// Skip to next piece of relevant code.
if ($pattern[$i]['to'] === 'parenthesis_closer') {
$to = 'parenthesis_opener';
// Find the previous opener.
$next = $phpcsFile->findPrevious (
if ($next === false || isset ($tokens[$next][$to]) === false ) {
// If there was not opener, then we must be
// using the wrong pattern.
if ($to === 'parenthesis_opener') {
// Skip to the opening token.
$stackPtr = ($tokens[$next][$to] - 1 );
} else if ($pattern[$i]['type'] === 'string') {
} else if ($pattern[$i]['type'] === 'newline') {
if ($this->ignoreComments === true
&& isset (Tokens ::$commentTokens[$tokens[$stackPtr]['code']]) === true
$startComment = $phpcsFile->findPrevious (
if ($tokens[$startComment]['line'] !== $tokens[($startComment + 1 )]['line']) {
$tokenContent = $phpcsFile->getTokensAsString (
($stackPtr - $startComment + 1 )
$found = $tokenContent. $found;
$stackPtr = ($startComment - 1 );
if ($tokens[$stackPtr]['code'] === T_WHITESPACE ) {
if ($tokens[$stackPtr]['content'] !== $phpcsFile->eolChar ) {
$found = $tokens[$stackPtr]['content']. $found;
// This may just be an indent that comes after a newline
// so check the token before to make sure. If it is a newline, we
// can ignore the error here.
if (($tokens[($stackPtr - 1 )]['content'] !== $phpcsFile->eolChar )
&& ($this->ignoreComments === true
&& isset (Tokens ::$commentTokens[$tokens[($stackPtr - 1 )]['code']]) === false )
$found = $tokens[$stackPtr]['content']. $found;
if ($hasError === false && $pattern[($i - 1 )]['type'] !== 'newline') {
// Make sure they only have 1 newline.
$prev = $phpcsFile->findPrevious ($ignoreTokens, ($stackPtr - 1 ), null , true );
if ($prev !== false && $tokens[$prev]['line'] !== $tokens[$stackPtr]['line']) {
$stackPtr = $origStackPtr;
$lastAddedStackPtr = null;
$patternLen = count($pattern);
for ($i = $patternInfo['listen_pos']; $i < $patternLen; $i++ ) {
if (isset ($tokens[$stackPtr]) === false ) {
if ($pattern[$i]['type'] === 'token') {
if ($pattern[$i]['token'] === T_WHITESPACE ) {
if ($this->ignoreComments === true ) {
// If we are ignoring comments, check to see if this current
// token is a comment. If so skip it.
if (isset (Tokens ::$commentTokens[$tokens[$stackPtr]['code']]) === true ) {
// If the next token is a comment, the we need to skip the
// current token as we should allow a space before a
// comment for readability.
if (isset ($tokens[($stackPtr + 1 )]) === true
&& isset (Tokens ::$commentTokens[$tokens[($stackPtr + 1 )]['code']]) === true
if ($tokens[$stackPtr]['code'] === T_WHITESPACE ) {
if (isset ($pattern[($i + 1 )]) === false ) {
// This is the last token in the pattern, so just compare
// the next token of content.
$tokenContent = $tokens[$stackPtr]['content'];
// Get all the whitespace to the next token.
$next = $phpcsFile->findNext (
$tokenContent = $phpcsFile->getTokensAsString (
$lastAddedStackPtr = $stackPtr;
if ($stackPtr !== $lastAddedStackPtr) {
if ($stackPtr !== $lastAddedStackPtr) {
$found .= $tokens[$stackPtr]['content'];
$lastAddedStackPtr = $stackPtr;
if (isset ($pattern[($i + 1 )]) === true
&& $pattern[($i + 1 )]['type'] === 'skip'
// The next token is a skip token, so we just need to make
// sure the whitespace we found has *at least* the
if (strpos($tokenContent, $pattern[$i]['value']) !== 0 ) {
if ($tokenContent !== $pattern[$i]['value']) {
// Check to see if this important token is the same as the
// next important token in the pattern. If it is not, then
// the pattern cannot be for this piece of code.
$next = $phpcsFile->findNext (
|| $tokens[$next]['code'] !== $pattern[$i]['token']
// The next important token did not match the pattern.
if ($lastAddedStackPtr !== null ) {
&& isset ($tokens[$next]['scope_condition']) === true
&& $tokens[$next]['scope_condition'] > $lastAddedStackPtr
// This is a brace, but the owner of it is after the current
// token, which means it does not belong to any token in
// our pattern. This means the pattern is not for us.
&& isset ($tokens[$next]['parenthesis_owner']) === true
&& $tokens[$next]['parenthesis_owner'] > $lastAddedStackPtr
// This is a bracket, but the owner of it is after the current
// token, which means it does not belong to any token in
// our pattern. This means the pattern is not for us.
// If we skipped past some whitespace tokens, then add them
if (($next - $stackPtr) > 0 ) {
for ($j = $stackPtr; $j < $next; $j++ ) {
$found .= $tokens[$j]['content'];
if (isset (Tokens ::$commentTokens[$tokens[$j]['code']]) === true ) {
// If we are not ignoring comments, this additional
// whitespace or comment is not allowed. If we are
// ignoring comments, there needs to be at least one
// comment for this to be allowed.
if ($this->ignoreComments === false
|| ($this->ignoreComments === true
&& $hasComment === false )
// Even when ignoring comments, we are not allowed to include
// newlines without the pattern specifying them, so
// everything should be on the same line.
if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) {
if ($next !== $lastAddedStackPtr) {
$found .= $tokens[$next]['content'];
$lastAddedStackPtr = $next;
if (isset ($pattern[($i + 1 )]) === true
&& $pattern[($i + 1 )]['type'] === 'skip'
} else if ($pattern[$i]['type'] === 'skip') {
if ($pattern[$i]['to'] === 'unknown') {
$next = $phpcsFile->findNext (
$pattern[($i + 1 )]['token'],
// Couldn't find the next token, so we must
// be using the wrong pattern.
// Find the previous opener.
$next = $phpcsFile->findPrevious (
|| isset ($tokens[$next][$pattern[$i]['to']]) === false
// If there was not opener, then we must
// be using the wrong pattern.
if ($pattern[$i]['to'] === 'parenthesis_closer') {
// Skip to the closing token.
$stackPtr = ($tokens[$next][$pattern[$i]['to']] + 1 );
} else if ($pattern[$i]['type'] === 'string') {
if ($tokens[$stackPtr]['code'] !== T_STRING ) {
if ($stackPtr !== $lastAddedStackPtr) {
$lastAddedStackPtr = $stackPtr;
} else if ($pattern[$i]['type'] === 'newline') {
// Find the next token that contains a newline character.
for ($j = $stackPtr; $j < $phpcsFile->numTokens; $j++ ) {
if (strpos($tokens[$j]['content'], $phpcsFile->eolChar ) !== false ) {
// We didn't find a newline character in the rest of the file.
$next = ($phpcsFile->numTokens - 1 );
if ($this->ignoreComments === false ) {
// The newline character cannot be part of a comment.
if (isset (Tokens ::$commentTokens[$tokens[$newline]['code']]) === true ) {
if ($newline === $stackPtr) {
// Check that there were no significant tokens that we
// skipped over to find our newline character.
$next = $phpcsFile->findNext (
// We skipped a non-ignored token.
if ($stackPtr !== $lastAddedStackPtr) {
$found .= $phpcsFile->getTokensAsString (
$diff = ($next - $stackPtr);
$lastAddedStackPtr = ($next - 1 );
if ($hasError === true ) {
$error = $this->prepareError ($found, $patternCode);
$errors[$origStackPtr] = $error;
* Prepares an error for the specified patternCode.
* @param string $found The actual found string in the code.
* @param string $patternCode The expected pattern code.
* @return string The error message.
protected function prepareError ($found, $patternCode)
$error = " Expected \"$expected\"; found \"$found\"";
* Returns the patterns that should be checked.
abstract protected function getPatterns ();
* Registers any supplementary tokens that this test might wish to process.
* A sniff may wish to register supplementary tests when it wishes to group
* an arbitrary validation that cannot be performed using a pattern, with
* @see processSupplementary()
protected function registerSupplementary ()
}//end registerSupplementary()
* Processes any tokens registered with registerSupplementary().
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where to
* @param int $stackPtr The position in the tokens stack to
* @see registerSupplementary()
protected function processSupplementary (File $phpcsFile, $stackPtr)
}//end processSupplementary()
* Parses a pattern string into an array of pattern steps.
* @param string $pattern The pattern to parse.
* @return array The parsed pattern array.
* @see createSkipPattern()
* @see createTokenPattern()
private function parse ($pattern)
for ($i = 0; $i < $length; $i++ ) {
$isLastChar = ($i === ($length - 1 ));
$oldFirstToken = $firstToken;
if (substr($pattern, $i, 3 ) === '...') {
// It's a skip pattern. The skip pattern requires the
// content of the token in the "from" position and the token
$specialPattern = $this->createSkipPattern ($pattern, ($i - 1 ));
$lastToken = ($i - $firstToken);
if ($specialPattern['to'] !== 'unknown') {
} else if (substr($pattern, $i, 3 ) === 'abc') {
$specialPattern = array ('type' => 'string');
$lastToken = ($i - $firstToken);
} else if (substr($pattern, $i, 3 ) === 'EOL') {
$specialPattern = array ('type' => 'newline');
$lastToken = ($i - $firstToken);
if ($specialPattern !== false || $isLastChar === true ) {
// If we are at the end of the string, don't worry about a limit.
if ($isLastChar === true ) {
// Get the string from the end of the last skip pattern, if any,
// to the end of the pattern string.
$str = substr($pattern, $oldFirstToken);
// Get the string from the end of the last special pattern,
// if any, to the start of this special pattern.
// Note that if the last special token was zero characters ago,
// there will be nothing to process so we can skip this bit.
// This happens if you have something like: EOL... in your pattern.
$str = substr($pattern, $oldFirstToken, $lastToken);
$tokenPatterns = $this->createTokenPattern ($str);
foreach ($tokenPatterns as $tokenPattern) {
$patterns[] = $tokenPattern;
// Make sure we don't skip the last token.
if ($isLastChar === false && $i === ($length - 1 )) {
// Add the skip pattern *after* we have processed
// all the tokens from the end of the last skip pattern
// to the start of this skip pattern.
if ($specialPattern !== false ) {
$patterns[] = $specialPattern;
* Creates a skip pattern.
* @param string $pattern The pattern being parsed.
* @param string $from The token content that the skip pattern starts from.
* @return array The pattern step.
* @see createTokenPattern()
private function createSkipPattern ($pattern, $from)
$skip = array ('type' => 'skip');
for ($start = $from; $start >= 0; $start-- ) {
switch ($pattern[$start]) {
if ($nestedParenthesis === 0 ) {
$skip['to'] = 'parenthesis_closer';
if ($nestedBraces === 0 ) {
$skip['to'] = 'scope_closer';
if (isset ($skip['to']) === true ) {
if (isset ($skip['to']) === false ) {
}//end createSkipPattern()
* Creates a token pattern.
* @param string $str The tokens string that the pattern should match.
* @return array The pattern step.
* @see createSkipPattern()
private function createTokenPattern ($str)
// Don't add a space after the closing php tag as it will add a new
$tokenizer = new PHP ('<?php '. $str. '?>', null );
// Remove the <?php tag from the front and the end php tag from the back.
$tokens = $tokenizer->getTokens ();
foreach ($tokens as $patternInfo) {
'token' => $patternInfo['code'],
'value' => $patternInfo['content'],
}//end createTokenPattern()
Documentation generated on Mon, 11 Mar 2019 15:27:14 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.
|