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