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