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: /** Chris@0: * A PHP_CodeSniffer_File object represents a PHP source file and the tokens Chris@0: * associated with it. Chris@0: * Chris@0: * It provides a means for traversing the token stack, along with Chris@0: * other token related operations. If a PHP_CodeSniffer_Sniff finds and error or Chris@0: * warning within a PHP_CodeSniffer_File, you can raise an error using the Chris@0: * addError() or addWarning() methods. Chris@0: * Chris@0: * Token Information Chris@0: * Chris@0: * Each token within the stack contains information about itself: Chris@0: * Chris@0: * Chris@0: * array( Chris@0: * 'code' => 301, // the token type code (see token_get_all()) Chris@0: * 'content' => 'if', // the token content Chris@0: * 'type' => 'T_IF', // the token name Chris@0: * 'line' => 56, // the line number when the token is located Chris@0: * 'column' => 12, // the column in the line where this token Chris@0: * // starts (starts from 1) Chris@0: * 'level' => 2 // the depth a token is within the scopes open Chris@0: * 'conditions' => array( // a list of scope condition token Chris@0: * // positions => codes that Chris@0: * 2 => 50, // opened the scopes that this token exists Chris@0: * 9 => 353, // in (see conditional tokens section below) Chris@0: * ), Chris@0: * ); Chris@0: * Chris@0: * Chris@0: * Conditional Tokens Chris@0: * Chris@0: * In addition to the standard token fields, conditions contain information to Chris@0: * determine where their scope begins and ends: Chris@0: * Chris@0: * Chris@0: * array( Chris@0: * 'scope_condition' => 38, // the token position of the condition Chris@0: * 'scope_opener' => 41, // the token position that started the scope Chris@0: * 'scope_closer' => 70, // the token position that ended the scope Chris@0: * ); Chris@0: * Chris@0: * Chris@0: * The condition, the scope opener and the scope closer each contain this Chris@0: * information. Chris@0: * Chris@0: * Parenthesis Tokens Chris@0: * Chris@0: * Each parenthesis token (T_OPEN_PARENTHESIS and T_CLOSE_PARENTHESIS) has a Chris@0: * reference to their opening and closing parenthesis, one being itself, the Chris@0: * other being its opposite. Chris@0: * Chris@0: * Chris@0: * array( Chris@0: * 'parenthesis_opener' => 34, Chris@0: * 'parenthesis_closer' => 40, Chris@0: * ); Chris@0: * Chris@0: * Chris@0: * Some tokens can "own" a set of parenthesis. For example a T_FUNCTION token Chris@0: * has parenthesis around its argument list. These tokens also have the Chris@0: * parenthesis_opener and and parenthesis_closer indices. Not all parenthesis Chris@0: * have owners, for example parenthesis used for arithmetic operations and Chris@0: * function calls. The parenthesis tokens that have an owner have the following Chris@0: * auxiliary array indices. Chris@0: * Chris@0: * Chris@0: * array( Chris@0: * 'parenthesis_opener' => 34, Chris@0: * 'parenthesis_closer' => 40, Chris@0: * 'parenthesis_owner' => 33, Chris@0: * ); Chris@0: * Chris@0: * Chris@0: * Each token within a set of parenthesis also has an array index Chris@0: * 'nested_parenthesis' which is an array of the Chris@0: * left parenthesis => right parenthesis token positions. Chris@0: * Chris@0: * Chris@0: * 'nested_parenthesis' => array( Chris@0: * 12 => 15 Chris@0: * 11 => 14 Chris@0: * ); Chris@0: * Chris@0: * Chris@0: * Extended Tokens Chris@0: * Chris@0: * PHP_CodeSniffer extends and augments some of the tokens created by Chris@0: * token_get_all(). A full list of these tokens can be seen in the Chris@0: * Tokens.php file. 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: class PHP_CodeSniffer_File Chris@0: { Chris@0: Chris@0: /** Chris@0: * The absolute path to the file associated with this object. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@0: private $_file = ''; Chris@0: Chris@0: /** Chris@0: * The EOL character this file uses. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@0: public $eolChar = ''; Chris@0: Chris@0: /** Chris@0: * The PHP_CodeSniffer object controlling this run. Chris@0: * Chris@0: * @var PHP_CodeSniffer Chris@0: */ Chris@0: public $phpcs = null; Chris@0: Chris@0: /** Chris@0: * The Fixer object to control fixing errors. Chris@0: * Chris@0: * @var PHP_CodeSniffer_Fixer Chris@0: */ Chris@0: public $fixer = null; Chris@0: Chris@0: /** Chris@0: * The tokenizer being used for this file. Chris@0: * Chris@0: * @var object Chris@0: */ Chris@0: public $tokenizer = null; Chris@0: Chris@0: /** Chris@0: * The tokenizer being used for this file. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@0: public $tokenizerType = 'PHP'; Chris@0: Chris@0: /** Chris@0: * The number of tokens in this file. Chris@0: * Chris@0: * Stored here to save calling count() everywhere. Chris@0: * Chris@0: * @var int Chris@0: */ Chris@0: public $numTokens = 0; Chris@0: Chris@0: /** Chris@0: * The tokens stack map. Chris@0: * Chris@0: * Note that the tokens in this array differ in format to the tokens Chris@0: * produced by token_get_all(). Tokens are initially produced with Chris@0: * token_get_all(), then augmented so that it's easier to process them. Chris@0: * Chris@0: * @var array() Chris@0: * @see Tokens.php Chris@0: */ Chris@0: private $_tokens = array(); Chris@0: Chris@0: /** Chris@0: * The errors raised from PHP_CodeSniffer_Sniffs. Chris@0: * Chris@0: * @var array() Chris@0: * @see getErrors() Chris@0: */ Chris@0: private $_errors = array(); Chris@0: Chris@0: /** Chris@0: * The warnings raised from PHP_CodeSniffer_Sniffs. Chris@0: * Chris@0: * @var array() Chris@0: * @see getWarnings() Chris@0: */ Chris@0: private $_warnings = array(); Chris@0: Chris@0: /** Chris@0: * The metrics recorded from PHP_CodeSniffer_Sniffs. Chris@0: * Chris@0: * @var array() Chris@0: * @see getMetrics() Chris@0: */ Chris@0: private $_metrics = array(); Chris@0: Chris@0: /** Chris@0: * Record the errors and warnings raised. Chris@0: * Chris@0: * @var bool Chris@0: */ Chris@0: private $_recordErrors = true; Chris@0: Chris@0: /** Chris@0: * An array of lines that are being ignored. Chris@0: * Chris@0: * @var array() Chris@0: */ Chris@0: private static $_ignoredLines = array(); Chris@0: Chris@0: /** Chris@0: * An array of sniffs that are being ignored. Chris@0: * Chris@0: * @var array() Chris@0: */ Chris@0: private $_ignoredListeners = array(); Chris@0: Chris@0: /** Chris@0: * An array of message codes that are being ignored. Chris@0: * Chris@0: * @var array() Chris@0: */ Chris@0: private $_ignoredCodes = array(); Chris@0: Chris@0: /** Chris@0: * The total number of errors raised. Chris@0: * Chris@0: * @var int Chris@0: */ Chris@0: private $_errorCount = 0; Chris@0: Chris@0: /** Chris@0: * The total number of warnings raised. Chris@0: * Chris@0: * @var int Chris@0: */ Chris@0: private $_warningCount = 0; Chris@0: Chris@0: /** Chris@0: * The total number of errors/warnings that can be fixed. Chris@0: * Chris@0: * @var int Chris@0: */ Chris@0: private $_fixableCount = 0; Chris@0: Chris@0: /** Chris@0: * An array of sniffs listening to this file's processing. Chris@0: * Chris@0: * @var array(PHP_CodeSniffer_Sniff) Chris@0: */ Chris@0: private $_listeners = array(); Chris@0: Chris@0: /** Chris@0: * The class name of the sniff currently processing the file. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@0: private $_activeListener = ''; Chris@0: Chris@0: /** Chris@0: * An array of sniffs being processed and how long they took. Chris@0: * Chris@0: * @var array() Chris@0: */ Chris@0: private $_listenerTimes = array(); Chris@0: Chris@0: /** Chris@0: * An array of rules from the ruleset.xml file. Chris@0: * Chris@0: * This value gets set by PHP_CodeSniffer when the object is created. Chris@0: * It may be empty, indicating that the ruleset does not override Chris@0: * any of the default sniff settings. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: protected $ruleset = array(); Chris@0: Chris@0: Chris@0: /** Chris@0: * Constructs a PHP_CodeSniffer_File. Chris@0: * Chris@0: * @param string $file The absolute path to the file to process. Chris@0: * @param array(string) $listeners The initial listeners listening to processing of this file. Chris@0: * to processing of this file. Chris@0: * @param array $ruleset An array of rules from the ruleset.xml file. Chris@0: * ruleset.xml file. Chris@0: * @param PHP_CodeSniffer $phpcs The PHP_CodeSniffer object controlling this run. Chris@0: * this run. Chris@0: * Chris@0: * @throws PHP_CodeSniffer_Exception If the register() method does Chris@0: * not return an array. Chris@0: */ Chris@0: public function __construct( Chris@0: $file, Chris@0: array $listeners, Chris@0: array $ruleset, Chris@0: PHP_CodeSniffer $phpcs Chris@0: ) { Chris@0: $this->_file = trim($file); Chris@0: $this->_listeners = $listeners; Chris@0: $this->ruleset = $ruleset; Chris@0: $this->phpcs = $phpcs; Chris@0: $this->fixer = new PHP_CodeSniffer_Fixer(); Chris@0: Chris@0: if (PHP_CODESNIFFER_INTERACTIVE === false) { Chris@0: $cliValues = $phpcs->cli->getCommandLineValues(); Chris@0: if (isset($cliValues['showSources']) === true Chris@0: && $cliValues['showSources'] !== true Chris@0: ) { Chris@0: $recordErrors = false; Chris@0: foreach ($cliValues['reports'] as $report => $output) { Chris@0: $reportClass = $phpcs->reporting->factory($report); Chris@0: if (property_exists($reportClass, 'recordErrors') === false Chris@0: || $reportClass->recordErrors === true Chris@0: ) { Chris@0: $recordErrors = true; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: $this->_recordErrors = $recordErrors; Chris@0: } Chris@0: } Chris@0: Chris@0: }//end __construct() Chris@0: Chris@0: Chris@0: /** Chris@0: * Sets the name of the currently active sniff. Chris@0: * Chris@0: * @param string $activeListener The class name of the current sniff. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function setActiveListener($activeListener) Chris@0: { Chris@0: $this->_activeListener = $activeListener; Chris@0: Chris@0: }//end setActiveListener() Chris@0: Chris@0: Chris@0: /** Chris@0: * Adds a listener to the token stack that listens to the specific tokens. Chris@0: * Chris@0: * When PHP_CodeSniffer encounters on the the tokens specified in $tokens, Chris@0: * it invokes the process method of the sniff. Chris@0: * Chris@0: * @param PHP_CodeSniffer_Sniff $listener The listener to add to the Chris@0: * listener stack. Chris@0: * @param array(int) $tokens The token types the listener wishes to Chris@0: * listen to. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function addTokenListener(PHP_CodeSniffer_Sniff $listener, array $tokens) Chris@0: { Chris@0: $class = get_class($listener); Chris@0: foreach ($tokens as $token) { Chris@0: if (isset($this->_listeners[$token]) === false) { Chris@0: $this->_listeners[$token] = array(); Chris@0: } Chris@0: Chris@0: if (isset($this->_listeners[$token][$class]) === false) { Chris@0: $this->_listeners[$token][$class] = $listener; Chris@0: } Chris@0: } Chris@0: Chris@0: }//end addTokenListener() Chris@0: Chris@0: Chris@0: /** Chris@0: * Removes a listener from listening from the specified tokens. Chris@0: * Chris@0: * @param PHP_CodeSniffer_Sniff $listener The listener to remove from the Chris@0: * listener stack. Chris@0: * @param array(int) $tokens The token types the listener wishes to Chris@0: * stop listen to. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function removeTokenListener( Chris@0: PHP_CodeSniffer_Sniff $listener, Chris@0: array $tokens Chris@0: ) { Chris@0: $class = get_class($listener); Chris@0: foreach ($tokens as $token) { Chris@0: if (isset($this->_listeners[$token]) === false) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: unset($this->_listeners[$token][$class]); Chris@0: } Chris@0: Chris@0: }//end removeTokenListener() Chris@0: Chris@0: Chris@0: /** Chris@0: * Rebuilds the list of listeners to ensure their state is cleared. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function refreshTokenListeners() Chris@0: { Chris@0: $this->phpcs->populateTokenListeners(); Chris@0: $this->_listeners = $this->phpcs->getTokenSniffs(); Chris@0: Chris@0: }//end refreshTokenListeners() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the token stack for this file. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: public function getTokens() Chris@0: { Chris@0: return $this->_tokens; Chris@0: Chris@0: }//end getTokens() Chris@0: Chris@0: Chris@0: /** Chris@0: * Starts the stack traversal and tells listeners when tokens are found. Chris@0: * Chris@0: * @param string $contents The contents to parse. If NULL, the content Chris@0: * is taken from the file system. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function start($contents=null) Chris@0: { Chris@0: $this->_errors = array(); Chris@0: $this->_warnings = array(); Chris@0: $this->_errorCount = 0; Chris@0: $this->_warningCount = 0; Chris@0: $this->_fixableCount = 0; Chris@0: Chris@0: // Reset the ignored lines because lines numbers may have changed Chris@0: // if we are fixing this file. Chris@0: self::$_ignoredLines = array(); Chris@0: Chris@0: try { Chris@0: $this->eolChar = self::detectLineEndings($this->_file, $contents); Chris@0: } catch (PHP_CodeSniffer_Exception $e) { Chris@0: $this->addWarning($e->getMessage(), null, 'Internal.DetectLineEndings'); Chris@0: return; Chris@0: } Chris@0: Chris@0: // If this is standard input, see if a filename was passed in as well. Chris@0: // This is done by including: phpcs_input_file: [file path] Chris@0: // as the first line of content. Chris@0: if ($this->_file === 'STDIN') { Chris@0: $cliValues = $this->phpcs->cli->getCommandLineValues(); Chris@0: if ($cliValues['stdinPath'] !== '') { Chris@0: $this->_file = $cliValues['stdinPath']; Chris@0: } else if ($contents !== null && substr($contents, 0, 17) === 'phpcs_input_file:') { Chris@0: $eolPos = strpos($contents, $this->eolChar); Chris@0: $filename = trim(substr($contents, 17, ($eolPos - 17))); Chris@0: $contents = substr($contents, ($eolPos + strlen($this->eolChar))); Chris@0: $this->_file = $filename; Chris@0: } Chris@0: } Chris@0: Chris@0: $this->_parse($contents); Chris@0: $this->fixer->startFile($this); Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 2) { Chris@0: echo "\t*** START TOKEN PROCESSING ***".PHP_EOL; Chris@0: } Chris@0: Chris@0: $foundCode = false; Chris@0: $listeners = $this->phpcs->getSniffs(); Chris@0: $listenerIgnoreTo = array(); Chris@0: $inTests = defined('PHP_CODESNIFFER_IN_TESTS'); Chris@0: Chris@0: // Foreach of the listeners that have registered to listen for this Chris@0: // token, get them to process it. Chris@0: foreach ($this->_tokens as $stackPtr => $token) { Chris@0: // Check for ignored lines. Chris@0: if ($token['code'] === T_COMMENT Chris@0: || $token['code'] === T_DOC_COMMENT_TAG Chris@0: || ($inTests === true && $token['code'] === T_INLINE_HTML) Chris@0: ) { Chris@0: if (strpos($token['content'], '@codingStandards') !== false) { Chris@0: if (strpos($token['content'], '@codingStandardsIgnoreFile') !== false) { Chris@0: // Ignoring the whole file, just a little late. Chris@0: $this->_errors = array(); Chris@0: $this->_warnings = array(); Chris@0: $this->_errorCount = 0; Chris@0: $this->_warningCount = 0; Chris@0: $this->_fixableCount = 0; Chris@0: return; Chris@0: } else if (strpos($token['content'], '@codingStandardsChangeSetting') !== false) { Chris@0: $start = strpos($token['content'], '@codingStandardsChangeSetting'); Chris@0: $comment = substr($token['content'], ($start + 30)); Chris@0: $parts = explode(' ', $comment); Chris@0: if (count($parts) >= 3 Chris@0: && isset($this->phpcs->sniffCodes[$parts[0]]) === true Chris@0: ) { Chris@0: $listenerCode = array_shift($parts); Chris@0: $propertyCode = array_shift($parts); Chris@0: $propertyValue = rtrim(implode(' ', $parts), " */\r\n"); Chris@0: $listenerClass = $this->phpcs->sniffCodes[$listenerCode]; Chris@0: $this->phpcs->setSniffProperty($listenerClass, $propertyCode, $propertyValue); Chris@0: } Chris@0: }//end if Chris@0: }//end if Chris@0: }//end if Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 2) { Chris@0: $type = $token['type']; Chris@0: $content = PHP_CodeSniffer::prepareForOutput($token['content']); Chris@0: echo "\t\tProcess token $stackPtr: $type => $content".PHP_EOL; Chris@0: } Chris@0: Chris@0: if ($token['code'] !== T_INLINE_HTML) { Chris@0: $foundCode = true; Chris@0: } Chris@0: Chris@0: if (isset($this->_listeners[$token['code']]) === false) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: foreach ($this->_listeners[$token['code']] as $listenerData) { Chris@0: if (isset($this->_ignoredListeners[$listenerData['class']]) === true Chris@0: || (isset($listenerIgnoreTo[$listenerData['class']]) === true Chris@0: && $listenerIgnoreTo[$listenerData['class']] > $stackPtr) Chris@0: ) { Chris@0: // This sniff is ignoring past this token, or the whole file. Chris@0: continue; Chris@0: } Chris@0: Chris@0: // Make sure this sniff supports the tokenizer Chris@0: // we are currently using. Chris@0: $class = $listenerData['class']; Chris@0: Chris@0: if (isset($listenerData['tokenizers'][$this->tokenizerType]) === false) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: // If the file path matches one of our ignore patterns, skip it. Chris@0: // While there is support for a type of each pattern Chris@0: // (absolute or relative) we don't actually support it here. Chris@0: foreach ($listenerData['ignore'] as $pattern) { Chris@0: // We assume a / directory separator, as do the exclude rules Chris@0: // most developers write, so we need a special case for any system Chris@0: // that is different. Chris@0: if (DIRECTORY_SEPARATOR === '\\') { Chris@0: $pattern = str_replace('/', '\\\\', $pattern); Chris@0: } Chris@0: Chris@0: $pattern = '`'.$pattern.'`i'; Chris@0: if (preg_match($pattern, $this->_file) === 1) { Chris@0: $this->_ignoredListeners[$class] = true; Chris@0: continue(2); Chris@0: } Chris@0: } Chris@0: Chris@0: $this->_activeListener = $class; Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 2) { Chris@0: $startTime = microtime(true); Chris@0: echo "\t\t\tProcessing ".$this->_activeListener.'... '; Chris@0: } Chris@0: Chris@0: $ignoreTo = $listeners[$class]->process($this, $stackPtr); Chris@0: if ($ignoreTo !== null) { Chris@0: $listenerIgnoreTo[$this->_activeListener] = $ignoreTo; Chris@0: } Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 2) { Chris@0: $timeTaken = (microtime(true) - $startTime); Chris@0: if (isset($this->_listenerTimes[$this->_activeListener]) === false) { Chris@0: $this->_listenerTimes[$this->_activeListener] = 0; Chris@0: } Chris@0: Chris@0: $this->_listenerTimes[$this->_activeListener] += $timeTaken; Chris@0: Chris@0: $timeTaken = round(($timeTaken), 4); Chris@0: echo "DONE in $timeTaken seconds".PHP_EOL; Chris@0: } Chris@0: Chris@0: $this->_activeListener = ''; Chris@0: }//end foreach Chris@0: }//end foreach Chris@0: Chris@0: if ($this->_recordErrors === false) { Chris@0: $this->_errors = array(); Chris@0: $this->_warnings = array(); Chris@0: } Chris@0: Chris@0: // If short open tags are off but the file being checked uses Chris@0: // short open tags, the whole content will be inline HTML Chris@0: // and nothing will be checked. So try and handle this case. Chris@0: // We don't show this error for STDIN because we can't be sure the content Chris@0: // actually came directly from the user. It could be something like Chris@0: // refs from a Git pre-push hook. Chris@0: if ($foundCode === false && $this->tokenizerType === 'PHP' && $this->_file !== 'STDIN') { Chris@0: $shortTags = (bool) ini_get('short_open_tag'); Chris@0: if ($shortTags === false) { Chris@0: $error = 'No PHP code was found in this file and short open tags are not allowed by this install of PHP. This file may be using short open tags but PHP does not allow them.'; Chris@0: $this->addWarning($error, null, 'Internal.NoCodeFound'); Chris@0: } Chris@0: } Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 2) { Chris@0: echo "\t*** END TOKEN PROCESSING ***".PHP_EOL; Chris@0: echo "\t*** START SNIFF PROCESSING REPORT ***".PHP_EOL; Chris@0: Chris@0: asort($this->_listenerTimes, SORT_NUMERIC); Chris@0: $this->_listenerTimes = array_reverse($this->_listenerTimes, true); Chris@0: foreach ($this->_listenerTimes as $listener => $timeTaken) { Chris@0: echo "\t$listener: ".round(($timeTaken), 4).' secs'.PHP_EOL; Chris@0: } Chris@0: Chris@0: echo "\t*** END SNIFF PROCESSING REPORT ***".PHP_EOL; Chris@0: } Chris@0: Chris@0: }//end start() Chris@0: Chris@0: Chris@0: /** Chris@0: * Remove vars stored in this file that are no longer required. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: public function cleanUp() Chris@0: { Chris@0: $this->_tokens = null; Chris@0: $this->_listeners = null; Chris@0: Chris@0: }//end cleanUp() Chris@0: Chris@0: Chris@0: /** Chris@0: * Tokenizes the file and prepares it for the test run. Chris@0: * Chris@0: * @param string $contents The contents to parse. If NULL, the content Chris@0: * is taken from the file system. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: private function _parse($contents=null) Chris@0: { Chris@0: if ($contents === null && empty($this->_tokens) === false) { Chris@0: // File has already been parsed. Chris@0: return; Chris@0: } Chris@0: Chris@0: $stdin = false; Chris@0: $cliValues = $this->phpcs->cli->getCommandLineValues(); Chris@0: if (empty($cliValues['files']) === true) { Chris@0: $stdin = true; Chris@0: } Chris@0: Chris@0: // Determine the tokenizer from the file extension. Chris@0: $fileParts = explode('.', $this->_file); Chris@0: $extension = array_pop($fileParts); Chris@0: if (isset($this->phpcs->allowedFileExtensions[$extension]) === true) { Chris@0: $tokenizerClass = 'PHP_CodeSniffer_Tokenizers_'.$this->phpcs->allowedFileExtensions[$extension]; Chris@0: $this->tokenizerType = $this->phpcs->allowedFileExtensions[$extension]; Chris@0: } else if (isset($this->phpcs->defaultFileExtensions[$extension]) === true) { Chris@0: $tokenizerClass = 'PHP_CodeSniffer_Tokenizers_'.$this->phpcs->defaultFileExtensions[$extension]; Chris@0: $this->tokenizerType = $this->phpcs->defaultFileExtensions[$extension]; Chris@0: } else { Chris@0: // Revert to default. Chris@0: $tokenizerClass = 'PHP_CodeSniffer_Tokenizers_'.$this->tokenizerType; Chris@0: } Chris@0: Chris@0: $tokenizer = new $tokenizerClass(); Chris@0: $this->tokenizer = $tokenizer; Chris@0: Chris@0: if ($contents === null) { Chris@0: $contents = file_get_contents($this->_file); Chris@0: } Chris@0: Chris@0: try { Chris@0: $tabWidth = null; Chris@0: $encoding = null; Chris@0: if (defined('PHP_CODESNIFFER_IN_TESTS') === true) { Chris@0: $cliValues = $this->phpcs->cli->getCommandLineValues(); Chris@0: if (isset($cliValues['tabWidth']) === true) { Chris@0: $tabWidth = $cliValues['tabWidth']; Chris@0: } Chris@0: Chris@0: if (isset($cliValues['encoding']) === true) { Chris@0: $encoding = $cliValues['encoding']; Chris@0: } Chris@0: } Chris@0: Chris@0: $this->_tokens = self::tokenizeString($contents, $tokenizer, $this->eolChar, $tabWidth, $encoding); Chris@0: } catch (PHP_CodeSniffer_Exception $e) { Chris@0: $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception'); Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 0 || (PHP_CODESNIFFER_CBF === true && $stdin === false)) { Chris@0: echo "[$this->tokenizerType => tokenizer error]... "; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo PHP_EOL; Chris@0: } Chris@0: } Chris@0: Chris@0: return; Chris@0: }//end try Chris@0: Chris@0: $this->numTokens = count($this->_tokens); Chris@0: Chris@0: // Check for mixed line endings as these can cause tokenizer errors and we Chris@0: // should let the user know that the results they get may be incorrect. Chris@0: // This is done by removing all backslashes, removing the newline char we Chris@0: // detected, then converting newlines chars into text. If any backslashes Chris@0: // are left at the end, we have additional newline chars in use. Chris@0: $contents = str_replace('\\', '', $contents); Chris@0: $contents = str_replace($this->eolChar, '', $contents); Chris@0: $contents = str_replace("\n", '\n', $contents); Chris@0: $contents = str_replace("\r", '\r', $contents); Chris@0: if (strpos($contents, '\\') !== false) { Chris@0: $error = 'File has mixed line endings; this may cause incorrect results'; Chris@0: $this->addWarning($error, 0, 'Internal.LineEndings.Mixed'); Chris@0: } Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 0 || (PHP_CODESNIFFER_CBF === true && $stdin === false)) { Chris@0: if ($this->numTokens === 0) { Chris@0: $numLines = 0; Chris@0: } else { Chris@0: $numLines = $this->_tokens[($this->numTokens - 1)]['line']; Chris@0: } Chris@0: Chris@0: echo "[$this->tokenizerType => $this->numTokens tokens in $numLines lines]... "; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo PHP_EOL; Chris@0: } Chris@0: } Chris@0: Chris@0: }//end _parse() Chris@0: Chris@0: Chris@0: /** Chris@0: * Opens a file and detects the EOL character being used. Chris@0: * Chris@0: * @param string $file The full path to the file. Chris@0: * @param string $contents The contents to parse. If NULL, the content Chris@0: * is taken from the file system. Chris@0: * Chris@0: * @return string Chris@0: * @throws PHP_CodeSniffer_Exception If $file could not be opened. Chris@0: */ Chris@0: public static function detectLineEndings($file, $contents=null) Chris@0: { Chris@0: if ($contents === null) { Chris@0: // Determine the newline character being used in this file. Chris@0: // Will be either \r, \r\n or \n. Chris@0: if (is_readable($file) === false) { Chris@0: $error = 'Error opening file; file no longer exists or you do not have access to read the file'; Chris@0: throw new PHP_CodeSniffer_Exception($error); Chris@0: } else { Chris@0: $handle = fopen($file, 'r'); Chris@0: if ($handle === false) { Chris@0: $error = 'Error opening file; could not auto-detect line endings'; Chris@0: throw new PHP_CodeSniffer_Exception($error); Chris@0: } Chris@0: } Chris@0: Chris@0: $firstLine = fgets($handle); Chris@0: fclose($handle); Chris@0: Chris@0: $eolChar = substr($firstLine, -1); Chris@0: if ($eolChar === "\n") { Chris@0: $secondLastChar = substr($firstLine, -2, 1); Chris@0: if ($secondLastChar === "\r") { Chris@0: $eolChar = "\r\n"; Chris@0: } Chris@0: } else if ($eolChar !== "\r") { Chris@0: // Must not be an EOL char at the end of the line. Chris@0: // Probably a one-line file, so assume \n as it really Chris@0: // doesn't matter considering there are no newlines. Chris@0: $eolChar = "\n"; Chris@0: } Chris@0: } else { Chris@0: if (preg_match("/\r\n?|\n/", $contents, $matches) !== 1) { Chris@0: // Assuming there are no newlines. Chris@0: $eolChar = "\n"; Chris@0: } else { Chris@0: $eolChar = $matches[0]; Chris@0: } Chris@0: }//end if Chris@0: Chris@0: return $eolChar; Chris@0: Chris@0: }//end detectLineEndings() Chris@0: Chris@0: Chris@0: /** Chris@0: * Records an error against a specific token in the file. Chris@0: * Chris@0: * @param string $error The error message. Chris@0: * @param int $stackPtr The stack position where the error occurred. Chris@0: * @param string $code A violation code unique to the sniff message. Chris@0: * @param array $data Replacements for the error message. Chris@0: * @param int $severity The severity level for this error. A value of 0 Chris@0: * will be converted into the default severity level. Chris@0: * @param boolean $fixable Can the error be fixed by the sniff? Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function addError( Chris@0: $error, Chris@0: $stackPtr, Chris@0: $code='', Chris@0: $data=array(), Chris@0: $severity=0, Chris@0: $fixable=false Chris@0: ) { Chris@0: if ($stackPtr === null) { Chris@0: $line = 1; Chris@0: $column = 1; Chris@0: } else { Chris@0: $line = $this->_tokens[$stackPtr]['line']; Chris@0: $column = $this->_tokens[$stackPtr]['column']; Chris@0: } Chris@0: Chris@0: return $this->_addError($error, $line, $column, $code, $data, $severity, $fixable); Chris@0: Chris@0: }//end addError() Chris@0: Chris@0: Chris@0: /** Chris@0: * Records a warning against a specific token in the file. Chris@0: * Chris@0: * @param string $warning The error message. Chris@0: * @param int $stackPtr The stack position where the error occurred. Chris@0: * @param string $code A violation code unique to the sniff message. Chris@0: * @param array $data Replacements for the warning message. Chris@0: * @param int $severity The severity level for this warning. A value of 0 Chris@0: * will be converted into the default severity level. Chris@0: * @param boolean $fixable Can the warning be fixed by the sniff? Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function addWarning( Chris@0: $warning, Chris@0: $stackPtr, Chris@0: $code='', Chris@0: $data=array(), Chris@0: $severity=0, Chris@0: $fixable=false Chris@0: ) { Chris@0: if ($stackPtr === null) { Chris@0: $line = 1; Chris@0: $column = 1; Chris@0: } else { Chris@0: $line = $this->_tokens[$stackPtr]['line']; Chris@0: $column = $this->_tokens[$stackPtr]['column']; Chris@0: } Chris@0: Chris@0: return $this->_addWarning($warning, $line, $column, $code, $data, $severity, $fixable); Chris@0: Chris@0: }//end addWarning() Chris@0: Chris@0: Chris@0: /** Chris@0: * Records an error against a specific line in the file. Chris@0: * Chris@0: * @param string $error The error message. Chris@0: * @param int $line The line on which the error occurred. Chris@0: * @param string $code A violation code unique to the sniff message. Chris@0: * @param array $data Replacements for the error message. Chris@0: * @param int $severity The severity level for this error. A value of 0 Chris@0: * will be converted into the default severity level. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function addErrorOnLine( Chris@0: $error, Chris@0: $line, Chris@0: $code='', Chris@0: $data=array(), Chris@0: $severity=0 Chris@0: ) { Chris@0: return $this->_addError($error, $line, 1, $code, $data, $severity, false); Chris@0: Chris@0: }//end addErrorOnLine() Chris@0: Chris@0: Chris@0: /** Chris@0: * Records a warning against a specific token in the file. Chris@0: * Chris@0: * @param string $warning The error message. Chris@0: * @param int $line The line on which the warning occurred. Chris@0: * @param string $code A violation code unique to the sniff message. Chris@0: * @param array $data Replacements for the warning message. Chris@0: * @param int $severity The severity level for this warning. A value of 0 Chris@0: * will be converted into the default severity level. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function addWarningOnLine( Chris@0: $warning, Chris@0: $line, Chris@0: $code='', Chris@0: $data=array(), Chris@0: $severity=0 Chris@0: ) { Chris@0: return $this->_addWarning($warning, $line, 1, $code, $data, $severity, false); Chris@0: Chris@0: }//end addWarningOnLine() Chris@0: Chris@0: Chris@0: /** Chris@0: * Records a fixable error against a specific token in the file. Chris@0: * Chris@0: * Returns true if the error was recorded and should be fixed. Chris@0: * Chris@0: * @param string $error The error message. Chris@0: * @param int $stackPtr The stack position where the error occurred. Chris@0: * @param string $code A violation code unique to the sniff message. Chris@0: * @param array $data Replacements for the error message. Chris@0: * @param int $severity The severity level for this error. A value of 0 Chris@0: * will be converted into the default severity level. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function addFixableError( Chris@0: $error, Chris@0: $stackPtr, Chris@0: $code='', Chris@0: $data=array(), Chris@0: $severity=0 Chris@0: ) { Chris@0: $recorded = $this->addError($error, $stackPtr, $code, $data, $severity, true); Chris@0: if ($recorded === true && $this->fixer->enabled === true) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end addFixableError() Chris@0: Chris@0: Chris@0: /** Chris@0: * Records a fixable warning against a specific token in the file. Chris@0: * Chris@0: * Returns true if the warning was recorded and should be fixed. Chris@0: * Chris@0: * @param string $warning The error message. Chris@0: * @param int $stackPtr The stack position where the error occurred. Chris@0: * @param string $code A violation code unique to the sniff message. Chris@0: * @param array $data Replacements for the warning message. Chris@0: * @param int $severity The severity level for this warning. A value of 0 Chris@0: * will be converted into the default severity level. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function addFixableWarning( Chris@0: $warning, Chris@0: $stackPtr, Chris@0: $code='', Chris@0: $data=array(), Chris@0: $severity=0 Chris@0: ) { Chris@0: $recorded = $this->addWarning($warning, $stackPtr, $code, $data, $severity, true); Chris@0: if ($recorded === true && $this->fixer->enabled === true) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end addFixableWarning() Chris@0: Chris@0: Chris@0: /** Chris@0: * Adds an error to the error stack. Chris@0: * Chris@0: * @param string $error The error message. Chris@0: * @param int $line The line on which the error occurred. Chris@0: * @param int $column The column at which the error occurred. Chris@0: * @param string $code A violation code unique to the sniff message. Chris@0: * @param array $data Replacements for the error message. Chris@0: * @param int $severity The severity level for this error. A value of 0 Chris@0: * will be converted into the default severity level. Chris@0: * @param boolean $fixable Can the error be fixed by the sniff? Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: private function _addError($error, $line, $column, $code, $data, $severity, $fixable) Chris@0: { Chris@0: if (isset(self::$_ignoredLines[$line]) === true) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Work out which sniff generated the error. Chris@0: if (substr($code, 0, 9) === 'Internal.') { Chris@0: // Any internal message. Chris@0: $sniffCode = $code; Chris@0: } else { Chris@0: $parts = explode('_', str_replace('\\', '_', $this->_activeListener)); Chris@0: if (isset($parts[3]) === true) { Chris@0: $sniff = $parts[0].'.'.$parts[2].'.'.$parts[3]; Chris@0: Chris@0: // Remove "Sniff" from the end. Chris@0: $sniff = substr($sniff, 0, -5); Chris@0: } else { Chris@0: $sniff = 'unknownSniff'; Chris@0: } Chris@0: Chris@0: $sniffCode = $sniff; Chris@0: if ($code !== '') { Chris@0: $sniffCode .= '.'.$code; Chris@0: } Chris@0: }//end if Chris@0: Chris@0: // If we know this sniff code is being ignored for this file, return early. Chris@0: if (isset($this->_ignoredCodes[$sniffCode]) === true) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Make sure this message type has not been set to "warning". Chris@0: if (isset($this->ruleset[$sniffCode]['type']) === true Chris@0: && $this->ruleset[$sniffCode]['type'] === 'warning' Chris@0: ) { Chris@0: // Pass this off to the warning handler. Chris@0: return $this->_addWarning($error, $line, $column, $code, $data, $severity, $fixable); Chris@0: } else if ($this->phpcs->cli->errorSeverity === 0) { Chris@0: // Don't bother doing any processing as errors are just going to Chris@0: // be hidden in the reports anyway. Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Make sure we are interested in this severity level. Chris@0: if (isset($this->ruleset[$sniffCode]['severity']) === true) { Chris@0: $severity = $this->ruleset[$sniffCode]['severity']; Chris@0: } else if ($severity === 0) { Chris@0: $severity = PHPCS_DEFAULT_ERROR_SEV; Chris@0: } Chris@0: Chris@0: if ($this->phpcs->cli->errorSeverity > $severity) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Make sure we are not ignoring this file. Chris@0: $patterns = $this->phpcs->getIgnorePatterns($sniffCode); Chris@0: foreach ($patterns as $pattern => $type) { Chris@0: // While there is support for a type of each pattern Chris@0: // (absolute or relative) we don't actually support it here. Chris@0: $replacements = array( Chris@0: '\\,' => ',', Chris@0: '*' => '.*', Chris@0: ); Chris@0: Chris@0: // We assume a / directory separator, as do the exclude rules Chris@0: // most developers write, so we need a special case for any system Chris@0: // that is different. Chris@0: if (DIRECTORY_SEPARATOR === '\\') { Chris@0: $replacements['/'] = '\\\\'; Chris@0: } Chris@0: Chris@0: $pattern = '`'.strtr($pattern, $replacements).'`i'; Chris@0: if (preg_match($pattern, $this->_file) === 1) { Chris@0: $this->_ignoredCodes[$sniffCode] = true; Chris@0: return false; Chris@0: } Chris@0: }//end foreach Chris@0: Chris@0: $this->_errorCount++; Chris@0: if ($fixable === true) { Chris@0: $this->_fixableCount++; Chris@0: } Chris@0: Chris@0: if ($this->_recordErrors === false) { Chris@0: if (isset($this->_errors[$line]) === false) { Chris@0: $this->_errors[$line] = 0; Chris@0: } Chris@0: Chris@0: $this->_errors[$line]++; Chris@0: return true; Chris@0: } Chris@0: Chris@0: // Work out the error message. Chris@0: if (isset($this->ruleset[$sniffCode]['message']) === true) { Chris@0: $error = $this->ruleset[$sniffCode]['message']; Chris@0: } Chris@0: Chris@0: if (empty($data) === true) { Chris@0: $message = $error; Chris@0: } else { Chris@0: $message = vsprintf($error, $data); Chris@0: } Chris@0: Chris@0: if (isset($this->_errors[$line]) === false) { Chris@0: $this->_errors[$line] = array(); Chris@0: } Chris@0: Chris@0: if (isset($this->_errors[$line][$column]) === false) { Chris@0: $this->_errors[$line][$column] = array(); Chris@0: } Chris@0: Chris@0: $this->_errors[$line][$column][] = array( Chris@0: 'message' => $message, Chris@0: 'source' => $sniffCode, Chris@0: 'severity' => $severity, Chris@0: 'fixable' => $fixable, Chris@0: ); Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1 Chris@0: && $this->fixer->enabled === true Chris@0: && $fixable === true Chris@0: ) { Chris@0: @ob_end_clean(); Chris@0: echo "\tE: [Line $line] $message ($sniffCode)".PHP_EOL; Chris@0: ob_start(); Chris@0: } Chris@0: Chris@0: return true; Chris@0: Chris@0: }//end _addError() Chris@0: Chris@0: Chris@0: /** Chris@0: * Adds an warning to the warning stack. Chris@0: * Chris@0: * @param string $warning The error message. Chris@0: * @param int $line The line on which the warning occurred. Chris@0: * @param int $column The column at which the warning occurred. Chris@0: * @param string $code A violation code unique to the sniff message. Chris@0: * @param array $data Replacements for the warning message. Chris@0: * @param int $severity The severity level for this warning. A value of 0 Chris@0: * will be converted into the default severity level. Chris@0: * @param boolean $fixable Can the warning be fixed by the sniff? Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: private function _addWarning($warning, $line, $column, $code, $data, $severity, $fixable) Chris@0: { Chris@0: if (isset(self::$_ignoredLines[$line]) === true) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Work out which sniff generated the warning. Chris@0: if (substr($code, 0, 9) === 'Internal.') { Chris@0: // Any internal message. Chris@0: $sniffCode = $code; Chris@0: } else { Chris@0: $parts = explode('_', str_replace('\\', '_', $this->_activeListener)); Chris@0: if (isset($parts[3]) === true) { Chris@0: $sniff = $parts[0].'.'.$parts[2].'.'.$parts[3]; Chris@0: Chris@0: // Remove "Sniff" from the end. Chris@0: $sniff = substr($sniff, 0, -5); Chris@0: } else { Chris@0: $sniff = 'unknownSniff'; Chris@0: } Chris@0: Chris@0: $sniffCode = $sniff; Chris@0: if ($code !== '') { Chris@0: $sniffCode .= '.'.$code; Chris@0: } Chris@0: }//end if Chris@0: Chris@0: // If we know this sniff code is being ignored for this file, return early. Chris@0: if (isset($this->_ignoredCodes[$sniffCode]) === true) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Make sure this message type has not been set to "error". Chris@0: if (isset($this->ruleset[$sniffCode]['type']) === true Chris@0: && $this->ruleset[$sniffCode]['type'] === 'error' Chris@0: ) { Chris@0: // Pass this off to the error handler. Chris@0: return $this->_addError($warning, $line, $column, $code, $data, $severity, $fixable); Chris@0: } else if ($this->phpcs->cli->warningSeverity === 0) { Chris@0: // Don't bother doing any processing as warnings are just going to Chris@0: // be hidden in the reports anyway. Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Make sure we are interested in this severity level. Chris@0: if (isset($this->ruleset[$sniffCode]['severity']) === true) { Chris@0: $severity = $this->ruleset[$sniffCode]['severity']; Chris@0: } else if ($severity === 0) { Chris@0: $severity = PHPCS_DEFAULT_WARN_SEV; Chris@0: } Chris@0: Chris@0: if ($this->phpcs->cli->warningSeverity > $severity) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Make sure we are not ignoring this file. Chris@0: $patterns = $this->phpcs->getIgnorePatterns($sniffCode); Chris@0: foreach ($patterns as $pattern => $type) { Chris@0: // While there is support for a type of each pattern Chris@0: // (absolute or relative) we don't actually support it here. Chris@0: $replacements = array( Chris@0: '\\,' => ',', Chris@0: '*' => '.*', Chris@0: ); Chris@0: Chris@0: // We assume a / directory separator, as do the exclude rules Chris@0: // most developers write, so we need a special case for any system Chris@0: // that is different. Chris@0: if (DIRECTORY_SEPARATOR === '\\') { Chris@0: $replacements['/'] = '\\\\'; Chris@0: } Chris@0: Chris@0: $pattern = '`'.strtr($pattern, $replacements).'`i'; Chris@0: if (preg_match($pattern, $this->_file) === 1) { Chris@0: $this->_ignoredCodes[$sniffCode] = true; Chris@0: return false; Chris@0: } Chris@0: }//end foreach Chris@0: Chris@0: $this->_warningCount++; Chris@0: if ($fixable === true) { Chris@0: $this->_fixableCount++; Chris@0: } Chris@0: Chris@0: if ($this->_recordErrors === false) { Chris@0: if (isset($this->_warnings[$line]) === false) { Chris@0: $this->_warnings[$line] = 0; Chris@0: } Chris@0: Chris@0: $this->_warnings[$line]++; Chris@0: return true; Chris@0: } Chris@0: Chris@0: // Work out the warning message. Chris@0: if (isset($this->ruleset[$sniffCode]['message']) === true) { Chris@0: $warning = $this->ruleset[$sniffCode]['message']; Chris@0: } Chris@0: Chris@0: if (empty($data) === true) { Chris@0: $message = $warning; Chris@0: } else { Chris@0: $message = vsprintf($warning, $data); Chris@0: } Chris@0: Chris@0: if (isset($this->_warnings[$line]) === false) { Chris@0: $this->_warnings[$line] = array(); Chris@0: } Chris@0: Chris@0: if (isset($this->_warnings[$line][$column]) === false) { Chris@0: $this->_warnings[$line][$column] = array(); Chris@0: } Chris@0: Chris@0: $this->_warnings[$line][$column][] = array( Chris@0: 'message' => $message, Chris@0: 'source' => $sniffCode, Chris@0: 'severity' => $severity, Chris@0: 'fixable' => $fixable, Chris@0: ); Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1 Chris@0: && $this->fixer->enabled === true Chris@0: && $fixable === true Chris@0: ) { Chris@0: @ob_end_clean(); Chris@0: echo "\tW: $message ($sniffCode)".PHP_EOL; Chris@0: ob_start(); Chris@0: } Chris@0: Chris@0: return true; Chris@0: Chris@0: }//end _addWarning() Chris@0: Chris@0: Chris@0: /** Chris@0: * Adds an warning to the warning stack. Chris@0: * Chris@0: * @param int $stackPtr The stack position where the metric was recorded. Chris@0: * @param string $metric The name of the metric being recorded. Chris@0: * @param string $value The value of the metric being recorded. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function recordMetric($stackPtr, $metric, $value) Chris@0: { Chris@0: if (isset($this->_metrics[$metric]) === false) { Chris@0: $this->_metrics[$metric] = array( Chris@0: 'values' => array( Chris@0: $value => array($stackPtr), Chris@0: ), Chris@0: ); Chris@0: } else { Chris@0: if (isset($this->_metrics[$metric]['values'][$value]) === false) { Chris@0: $this->_metrics[$metric]['values'][$value] = array($stackPtr); Chris@0: } else { Chris@0: $this->_metrics[$metric]['values'][$value][] = $stackPtr; Chris@0: } Chris@0: } Chris@0: Chris@0: return true; Chris@0: Chris@0: }//end recordMetric() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the number of errors raised. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function getErrorCount() Chris@0: { Chris@0: return $this->_errorCount; Chris@0: Chris@0: }//end getErrorCount() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the number of warnings raised. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function getWarningCount() Chris@0: { Chris@0: return $this->_warningCount; Chris@0: Chris@0: }//end getWarningCount() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the number of successes recorded. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function getSuccessCount() Chris@0: { Chris@0: return $this->_successCount; Chris@0: Chris@0: }//end getSuccessCount() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the number of fixable errors/warnings raised. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function getFixableCount() Chris@0: { Chris@0: return $this->_fixableCount; Chris@0: Chris@0: }//end getFixableCount() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the list of ignored lines. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: public function getIgnoredLines() Chris@0: { Chris@0: return self::$_ignoredLines; Chris@0: Chris@0: }//end getIgnoredLines() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the errors raised from processing this file. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: public function getErrors() Chris@0: { Chris@0: return $this->_errors; Chris@0: Chris@0: }//end getErrors() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the warnings raised from processing this file. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: public function getWarnings() Chris@0: { Chris@0: return $this->_warnings; Chris@0: Chris@0: }//end getWarnings() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the metrics found while processing this file. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: public function getMetrics() Chris@0: { Chris@0: return $this->_metrics; Chris@0: Chris@0: }//end getMetrics() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the absolute filename of this file. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function getFilename() Chris@0: { Chris@0: return $this->_file; Chris@0: Chris@0: }//end getFilename() Chris@0: Chris@0: Chris@0: /** Chris@0: * Creates an array of tokens when given some PHP code. Chris@0: * Chris@0: * Starts by using token_get_all() but does a lot of extra processing Chris@0: * to insert information about the context of the token. Chris@0: * Chris@0: * @param string $string The string to tokenize. Chris@0: * @param object $tokenizer A tokenizer class to use to tokenize the string. Chris@0: * @param string $eolChar The EOL character to use for splitting strings. Chris@0: * @param int $tabWidth The number of spaces each tab respresents. Chris@0: * @param string $encoding The charset of the sniffed file. Chris@0: * Chris@0: * @throws PHP_CodeSniffer_Exception If the file cannot be processed. Chris@0: * @return array Chris@0: */ Chris@0: public static function tokenizeString($string, $tokenizer, $eolChar='\n', $tabWidth=null, $encoding=null) Chris@0: { Chris@0: // Minified files often have a very large number of characters per line Chris@0: // and cause issues when tokenizing. Chris@0: if (property_exists($tokenizer, 'skipMinified') === true Chris@0: && $tokenizer->skipMinified === true Chris@0: ) { Chris@0: $numChars = strlen($string); Chris@0: $numLines = (substr_count($string, $eolChar) + 1); Chris@0: $average = ($numChars / $numLines); Chris@0: if ($average > 100) { Chris@0: throw new PHP_CodeSniffer_Exception('File appears to be minified and cannot be processed'); Chris@0: } Chris@0: } Chris@0: Chris@0: $tokens = $tokenizer->tokenizeString($string, $eolChar); Chris@0: Chris@0: if ($tabWidth === null) { Chris@0: $tabWidth = PHP_CODESNIFFER_TAB_WIDTH; Chris@0: } Chris@0: Chris@0: if ($encoding === null) { Chris@0: $encoding = PHP_CODESNIFFER_ENCODING; Chris@0: } Chris@0: Chris@0: self::_createPositionMap($tokens, $tokenizer, $eolChar, $encoding, $tabWidth); Chris@0: self::_createTokenMap($tokens, $tokenizer, $eolChar); Chris@0: self::_createParenthesisNestingMap($tokens, $tokenizer, $eolChar); Chris@0: self::_createScopeMap($tokens, $tokenizer, $eolChar); Chris@0: Chris@0: self::_createLevelMap($tokens, $tokenizer, $eolChar); Chris@0: Chris@0: // Allow the tokenizer to do additional processing if required. Chris@0: $tokenizer->processAdditional($tokens, $eolChar); Chris@0: Chris@0: return $tokens; Chris@0: Chris@0: }//end tokenizeString() Chris@0: Chris@0: Chris@0: /** Chris@0: * Sets token position information. Chris@0: * Chris@0: * Can also convert tabs into spaces. Each tab can represent between Chris@0: * 1 and $width spaces, so this cannot be a straight string replace. Chris@0: * Chris@0: * @param array $tokens The array of tokens to process. Chris@0: * @param object $tokenizer The tokenizer being used to process this file. Chris@0: * @param string $eolChar The EOL character to use for splitting strings. Chris@0: * @param string $encoding The charset of the sniffed file. Chris@0: * @param int $tabWidth The number of spaces that each tab represents. Chris@0: * Set to 0 to disable tab replacement. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: private static function _createPositionMap(&$tokens, $tokenizer, $eolChar, $encoding, $tabWidth) Chris@0: { Chris@0: $currColumn = 1; Chris@0: $lineNumber = 1; Chris@0: $eolLen = (strlen($eolChar) * -1); Chris@0: $tokenizerType = get_class($tokenizer); Chris@0: $ignoring = false; Chris@0: $inTests = defined('PHP_CODESNIFFER_IN_TESTS'); Chris@0: Chris@0: $checkEncoding = false; Chris@0: if ($encoding !== 'iso-8859-1' && function_exists('iconv_strlen') === true) { Chris@0: $checkEncoding = true; Chris@0: } Chris@0: Chris@0: $tokensWithTabs = array( Chris@0: T_WHITESPACE => true, Chris@0: T_COMMENT => true, Chris@0: T_DOC_COMMENT => true, Chris@0: T_DOC_COMMENT_WHITESPACE => true, Chris@0: T_DOC_COMMENT_STRING => true, Chris@0: T_CONSTANT_ENCAPSED_STRING => true, Chris@0: T_DOUBLE_QUOTED_STRING => true, Chris@0: T_HEREDOC => true, Chris@0: T_NOWDOC => true, Chris@0: T_INLINE_HTML => true, Chris@0: ); Chris@0: Chris@0: $numTokens = count($tokens); Chris@0: for ($i = 0; $i < $numTokens; $i++) { Chris@0: $tokens[$i]['line'] = $lineNumber; Chris@0: $tokens[$i]['column'] = $currColumn; Chris@0: Chris@0: if ($tokenizerType === 'PHP_CodeSniffer_Tokenizers_PHP' Chris@0: && isset(PHP_CodeSniffer_Tokens::$knownLengths[$tokens[$i]['code']]) === true Chris@0: ) { Chris@0: // There are no tabs in the tokens we know the length of. Chris@0: $length = PHP_CodeSniffer_Tokens::$knownLengths[$tokens[$i]['code']]; Chris@0: $currColumn += $length; Chris@0: } else if ($tabWidth === 0 Chris@0: || isset($tokensWithTabs[$tokens[$i]['code']]) === false Chris@0: || strpos($tokens[$i]['content'], "\t") === false Chris@0: ) { Chris@0: // There are no tabs in this content, or we aren't replacing them. Chris@0: if ($checkEncoding === true) { Chris@0: // Not using the default encoding, so take a bit more care. Chris@0: $length = @iconv_strlen($tokens[$i]['content'], $encoding); Chris@0: if ($length === false) { Chris@0: // String contained invalid characters, so revert to default. Chris@0: $length = strlen($tokens[$i]['content']); Chris@0: } Chris@0: } else { Chris@0: $length = strlen($tokens[$i]['content']); Chris@0: } Chris@0: Chris@0: $currColumn += $length; Chris@0: } else { Chris@0: if (str_replace("\t", '', $tokens[$i]['content']) === '') { Chris@0: // String only contains tabs, so we can shortcut the process. Chris@0: $numTabs = strlen($tokens[$i]['content']); Chris@0: Chris@0: $newContent = ''; Chris@0: $firstTabSize = ($tabWidth - ($currColumn % $tabWidth) + 1); Chris@0: $length = ($firstTabSize + ($tabWidth * ($numTabs - 1))); Chris@0: $currColumn += $length; Chris@0: $newContent = str_repeat(' ', $length); Chris@0: } else { Chris@0: // We need to determine the length of each tab. Chris@0: $tabs = explode("\t", $tokens[$i]['content']); Chris@0: Chris@0: $numTabs = (count($tabs) - 1); Chris@0: $tabNum = 0; Chris@0: $newContent = ''; Chris@0: $length = 0; Chris@0: Chris@0: foreach ($tabs as $content) { Chris@0: if ($content !== '') { Chris@0: $newContent .= $content; Chris@0: if ($checkEncoding === true) { Chris@0: // Not using the default encoding, so take a bit more care. Chris@0: $contentLength = @iconv_strlen($content, $encoding); Chris@0: if ($contentLength === false) { Chris@0: // String contained invalid characters, so revert to default. Chris@0: $contentLength = strlen($content); Chris@0: } Chris@0: } else { Chris@0: $contentLength = strlen($content); Chris@0: } Chris@0: Chris@0: $currColumn += $contentLength; Chris@0: $length += $contentLength; Chris@0: } Chris@0: Chris@0: // The last piece of content does not have a tab after it. Chris@0: if ($tabNum === $numTabs) { Chris@0: break; Chris@0: } Chris@0: Chris@0: // Process the tab that comes after the content. Chris@0: $lastCurrColumn = $currColumn; Chris@0: $tabNum++; Chris@0: Chris@0: // Move the pointer to the next tab stop. Chris@0: if (($currColumn % $tabWidth) === 0) { Chris@0: // This is the first tab, and we are already at a Chris@0: // tab stop, so this tab counts as a single space. Chris@0: $currColumn++; Chris@0: } else { Chris@0: $currColumn++; Chris@0: while (($currColumn % $tabWidth) !== 0) { Chris@0: $currColumn++; Chris@0: } Chris@0: Chris@0: $currColumn++; Chris@0: } Chris@0: Chris@0: $length += ($currColumn - $lastCurrColumn); Chris@0: $newContent .= str_repeat(' ', ($currColumn - $lastCurrColumn)); Chris@0: }//end foreach Chris@0: }//end if Chris@0: Chris@0: $tokens[$i]['orig_content'] = $tokens[$i]['content']; Chris@0: $tokens[$i]['content'] = $newContent; Chris@0: }//end if Chris@0: Chris@0: $tokens[$i]['length'] = $length; Chris@0: Chris@0: if (isset(PHP_CodeSniffer_Tokens::$knownLengths[$tokens[$i]['code']]) === false Chris@0: && strpos($tokens[$i]['content'], $eolChar) !== false Chris@0: ) { Chris@0: $lineNumber++; Chris@0: $currColumn = 1; Chris@0: Chris@0: // Newline chars are not counted in the token length. Chris@0: $tokens[$i]['length'] += $eolLen; Chris@0: } Chris@0: Chris@0: if ($tokens[$i]['code'] === T_COMMENT Chris@0: || $tokens[$i]['code'] === T_DOC_COMMENT_TAG Chris@0: || ($inTests === true && $tokens[$i]['code'] === T_INLINE_HTML) Chris@0: ) { Chris@0: if (strpos($tokens[$i]['content'], '@codingStandards') !== false) { Chris@0: if ($ignoring === false Chris@0: && strpos($tokens[$i]['content'], '@codingStandardsIgnoreStart') !== false Chris@0: ) { Chris@0: $ignoring = true; Chris@0: } else if ($ignoring === true Chris@0: && strpos($tokens[$i]['content'], '@codingStandardsIgnoreEnd') !== false Chris@0: ) { Chris@0: $ignoring = false; Chris@0: // Ignore this comment too. Chris@0: self::$_ignoredLines[$tokens[$i]['line']] = true; Chris@0: } else if ($ignoring === false Chris@0: && strpos($tokens[$i]['content'], '@codingStandardsIgnoreLine') !== false Chris@0: ) { Chris@0: self::$_ignoredLines[($tokens[$i]['line'] + 1)] = true; Chris@0: // Ignore this comment too. Chris@0: self::$_ignoredLines[$tokens[$i]['line']] = true; Chris@0: } Chris@0: } Chris@0: }//end if Chris@0: Chris@0: if ($ignoring === true) { Chris@0: self::$_ignoredLines[$tokens[$i]['line']] = true; Chris@0: } Chris@0: }//end for Chris@0: Chris@0: }//end _createPositionMap() Chris@0: Chris@0: Chris@0: /** Chris@0: * Creates a map of brackets positions. Chris@0: * Chris@0: * @param array $tokens The array of tokens to process. Chris@0: * @param object $tokenizer The tokenizer being used to process this file. Chris@0: * @param string $eolChar The EOL character to use for splitting strings. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: private static function _createTokenMap(&$tokens, $tokenizer, $eolChar) Chris@0: { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "\t*** START TOKEN MAP ***".PHP_EOL; Chris@0: } Chris@0: Chris@0: $squareOpeners = array(); Chris@0: $curlyOpeners = array(); Chris@0: $numTokens = count($tokens); Chris@0: Chris@0: $openers = array(); Chris@0: $openOwner = null; Chris@0: Chris@0: for ($i = 0; $i < $numTokens; $i++) { Chris@0: /* Chris@0: Parenthesis mapping. Chris@0: */ Chris@0: Chris@0: if (isset(PHP_CodeSniffer_Tokens::$parenthesisOpeners[$tokens[$i]['code']]) === true) { Chris@0: $tokens[$i]['parenthesis_opener'] = null; Chris@0: $tokens[$i]['parenthesis_closer'] = null; Chris@0: $tokens[$i]['parenthesis_owner'] = $i; Chris@0: $openOwner = $i; Chris@0: } else if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) { Chris@0: $openers[] = $i; Chris@0: $tokens[$i]['parenthesis_opener'] = $i; Chris@0: if ($openOwner !== null) { Chris@0: $tokens[$openOwner]['parenthesis_opener'] = $i; Chris@0: $tokens[$i]['parenthesis_owner'] = $openOwner; Chris@0: $openOwner = null; Chris@0: } Chris@0: } else if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS) { Chris@0: // Did we set an owner for this set of parenthesis? Chris@0: $numOpeners = count($openers); Chris@0: if ($numOpeners !== 0) { Chris@0: $opener = array_pop($openers); Chris@0: if (isset($tokens[$opener]['parenthesis_owner']) === true) { Chris@0: $owner = $tokens[$opener]['parenthesis_owner']; Chris@0: Chris@0: $tokens[$owner]['parenthesis_closer'] = $i; Chris@0: $tokens[$i]['parenthesis_owner'] = $owner; Chris@0: } Chris@0: Chris@0: $tokens[$i]['parenthesis_opener'] = $opener; Chris@0: $tokens[$i]['parenthesis_closer'] = $i; Chris@0: $tokens[$opener]['parenthesis_closer'] = $i; Chris@0: } Chris@0: }//end if Chris@0: Chris@0: /* Chris@0: Bracket mapping. Chris@0: */ Chris@0: Chris@0: switch ($tokens[$i]['code']) { Chris@0: case T_OPEN_SQUARE_BRACKET: Chris@0: $squareOpeners[] = $i; Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", count($squareOpeners)); Chris@0: echo str_repeat("\t", count($curlyOpeners)); Chris@0: echo "=> Found square bracket opener at $i".PHP_EOL; Chris@0: } Chris@0: break; Chris@0: case T_OPEN_CURLY_BRACKET: Chris@0: if (isset($tokens[$i]['scope_closer']) === false) { Chris@0: $curlyOpeners[] = $i; Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", count($squareOpeners)); Chris@0: echo str_repeat("\t", count($curlyOpeners)); Chris@0: echo "=> Found curly bracket opener at $i".PHP_EOL; Chris@0: } Chris@0: } Chris@0: break; Chris@0: case T_CLOSE_SQUARE_BRACKET: Chris@0: if (empty($squareOpeners) === false) { Chris@0: $opener = array_pop($squareOpeners); Chris@0: $tokens[$i]['bracket_opener'] = $opener; Chris@0: $tokens[$i]['bracket_closer'] = $i; Chris@0: $tokens[$opener]['bracket_opener'] = $opener; Chris@0: $tokens[$opener]['bracket_closer'] = $i; Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", count($squareOpeners)); Chris@0: echo str_repeat("\t", count($curlyOpeners)); Chris@0: echo "\t=> Found square bracket closer at $i for $opener".PHP_EOL; Chris@0: } Chris@0: } Chris@0: break; Chris@0: case T_CLOSE_CURLY_BRACKET: Chris@0: if (empty($curlyOpeners) === false Chris@0: && isset($tokens[$i]['scope_opener']) === false Chris@0: ) { Chris@0: $opener = array_pop($curlyOpeners); Chris@0: $tokens[$i]['bracket_opener'] = $opener; Chris@0: $tokens[$i]['bracket_closer'] = $i; Chris@0: $tokens[$opener]['bracket_opener'] = $opener; Chris@0: $tokens[$opener]['bracket_closer'] = $i; Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", count($squareOpeners)); Chris@0: echo str_repeat("\t", count($curlyOpeners)); Chris@0: echo "\t=> Found curly bracket closer at $i for $opener".PHP_EOL; Chris@0: } Chris@0: } Chris@0: break; Chris@0: default: Chris@0: continue; Chris@0: }//end switch Chris@0: }//end for Chris@0: Chris@0: // Cleanup for any openers that we didn't find closers for. Chris@0: // This typically means there was a syntax error breaking things. Chris@0: foreach ($openers as $opener) { Chris@0: unset($tokens[$opener]['parenthesis_opener']); Chris@0: unset($tokens[$opener]['parenthesis_owner']); Chris@0: } Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "\t*** END TOKEN MAP ***".PHP_EOL; Chris@0: } Chris@0: Chris@0: }//end _createTokenMap() Chris@0: Chris@0: Chris@0: /** Chris@0: * Creates a map for the parenthesis tokens that surround other tokens. Chris@0: * Chris@0: * @param array $tokens The array of tokens to process. Chris@0: * @param object $tokenizer The tokenizer being used to process this file. Chris@0: * @param string $eolChar The EOL character to use for splitting strings. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: private static function _createParenthesisNestingMap( Chris@0: &$tokens, Chris@0: $tokenizer, Chris@0: $eolChar Chris@0: ) { Chris@0: $numTokens = count($tokens); Chris@0: $map = array(); Chris@0: for ($i = 0; $i < $numTokens; $i++) { Chris@0: if (isset($tokens[$i]['parenthesis_opener']) === true Chris@0: && $i === $tokens[$i]['parenthesis_opener'] Chris@0: ) { Chris@0: if (empty($map) === false) { Chris@0: $tokens[$i]['nested_parenthesis'] = $map; Chris@0: } Chris@0: Chris@0: if (isset($tokens[$i]['parenthesis_closer']) === true) { Chris@0: $map[$tokens[$i]['parenthesis_opener']] Chris@0: = $tokens[$i]['parenthesis_closer']; Chris@0: } Chris@0: } else if (isset($tokens[$i]['parenthesis_closer']) === true Chris@0: && $i === $tokens[$i]['parenthesis_closer'] Chris@0: ) { Chris@0: array_pop($map); Chris@0: if (empty($map) === false) { Chris@0: $tokens[$i]['nested_parenthesis'] = $map; Chris@0: } Chris@0: } else { Chris@0: if (empty($map) === false) { Chris@0: $tokens[$i]['nested_parenthesis'] = $map; Chris@0: } Chris@0: }//end if Chris@0: }//end for Chris@0: Chris@0: }//end _createParenthesisNestingMap() Chris@0: Chris@0: Chris@0: /** Chris@0: * Creates a scope map of tokens that open scopes. Chris@0: * Chris@0: * @param array $tokens The array of tokens to process. Chris@0: * @param object $tokenizer The tokenizer being used to process this file. Chris@0: * @param string $eolChar The EOL character to use for splitting strings. Chris@0: * Chris@0: * @return void Chris@0: * @see _recurseScopeMap() Chris@0: */ Chris@0: private static function _createScopeMap(&$tokens, $tokenizer, $eolChar) Chris@0: { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "\t*** START SCOPE MAP ***".PHP_EOL; Chris@0: } Chris@0: Chris@0: $numTokens = count($tokens); Chris@0: for ($i = 0; $i < $numTokens; $i++) { Chris@0: // Check to see if the current token starts a new scope. Chris@0: if (isset($tokenizer->scopeOpeners[$tokens[$i]['code']]) === true) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$i]['type']; Chris@0: $content = PHP_CodeSniffer::prepareForOutput($tokens[$i]['content']); Chris@0: echo "\tStart scope map at $i:$type => $content".PHP_EOL; Chris@0: } Chris@0: Chris@0: if (isset($tokens[$i]['scope_condition']) === true) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "\t* already processed, skipping *".PHP_EOL; Chris@0: } Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@0: $i = self::_recurseScopeMap( Chris@0: $tokens, Chris@0: $numTokens, Chris@0: $tokenizer, Chris@0: $eolChar, Chris@0: $i Chris@0: ); Chris@0: }//end if Chris@0: }//end for Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "\t*** END SCOPE MAP ***".PHP_EOL; Chris@0: } Chris@0: Chris@0: }//end _createScopeMap() Chris@0: Chris@0: Chris@0: /** Chris@0: * Recurses though the scope openers to build a scope map. Chris@0: * Chris@0: * @param array $tokens The array of tokens to process. Chris@0: * @param int $numTokens The size of the tokens array. Chris@0: * @param object $tokenizer The tokenizer being used to process this file. Chris@0: * @param string $eolChar The EOL character to use for splitting strings. Chris@0: * @param int $stackPtr The position in the stack of the token that Chris@0: * opened the scope (eg. an IF token or FOR token). Chris@0: * @param int $depth How many scope levels down we are. Chris@0: * @param int $ignore How many curly braces we are ignoring. Chris@0: * Chris@0: * @return int The position in the stack that closed the scope. Chris@0: */ Chris@0: private static function _recurseScopeMap( Chris@0: &$tokens, Chris@0: $numTokens, Chris@0: $tokenizer, Chris@0: $eolChar, Chris@0: $stackPtr, Chris@0: $depth=1, Chris@0: &$ignore=0 Chris@0: ) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Begin scope map recursion at token $stackPtr with depth $depth".PHP_EOL; Chris@0: } Chris@0: Chris@0: $opener = null; Chris@0: $currType = $tokens[$stackPtr]['code']; Chris@0: $startLine = $tokens[$stackPtr]['line']; Chris@0: Chris@0: // We will need this to restore the value if we end up Chris@0: // returning a token ID that causes our calling function to go back Chris@0: // over already ignored braces. Chris@0: $originalIgnore = $ignore; Chris@0: Chris@0: // If the start token for this scope opener is the same as Chris@0: // the scope token, we have already found our opener. Chris@0: if (isset($tokenizer->scopeOpeners[$currType]['start'][$currType]) === true) { Chris@0: $opener = $stackPtr; Chris@0: } Chris@0: Chris@0: for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { Chris@0: $tokenType = $tokens[$i]['code']; Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$i]['type']; Chris@0: $line = $tokens[$i]['line']; Chris@0: $content = PHP_CodeSniffer::prepareForOutput($tokens[$i]['content']); Chris@0: Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "Process token $i on line $line ["; Chris@0: if ($opener !== null) { Chris@0: echo "opener:$opener;"; Chris@0: } Chris@0: Chris@0: if ($ignore > 0) { Chris@0: echo "ignore=$ignore;"; Chris@0: } Chris@0: Chris@0: echo "]: $type => $content".PHP_EOL; Chris@0: }//end if Chris@0: Chris@0: // Very special case for IF statements in PHP that can be defined without Chris@0: // scope tokens. E.g., if (1) 1; 1 ? (1 ? 1 : 1) : 1; Chris@0: // If an IF statement below this one has an opener but no Chris@0: // keyword, the opener will be incorrectly assigned to this IF statement. Chris@0: // The same case also applies to USE statements, which don't have to have Chris@0: // openers, so a following USE statement can cause an incorrect brace match. Chris@0: if (($currType === T_IF || $currType === T_ELSE || $currType === T_USE) Chris@0: && $opener === null Chris@0: && $tokens[$i]['code'] === T_SEMICOLON Chris@0: ) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Found semicolon before scope opener for $stackPtr:$type, bailing".PHP_EOL; Chris@0: } Chris@0: Chris@0: return $i; Chris@0: } Chris@0: Chris@0: if ($opener === null Chris@0: && $ignore === 0 Chris@0: && $tokenType === T_CLOSE_CURLY_BRACKET Chris@0: && isset($tokenizer->scopeOpeners[$currType]['end'][$tokenType]) === true Chris@0: ) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Found curly brace closer before scope opener for $stackPtr:$type, bailing".PHP_EOL; Chris@0: } Chris@0: Chris@0: return ($i - 1); Chris@0: } Chris@0: Chris@0: if ($opener !== null Chris@0: && (isset($tokens[$i]['scope_opener']) === false Chris@0: || $tokenizer->scopeOpeners[$tokens[$stackPtr]['code']]['shared'] === true) Chris@0: && isset($tokenizer->scopeOpeners[$currType]['end'][$tokenType]) === true Chris@0: ) { Chris@0: if ($ignore > 0 && $tokenType === T_CLOSE_CURLY_BRACKET) { Chris@0: // The last opening bracket must have been for a string Chris@0: // offset or alike, so let's ignore it. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* finished ignoring curly brace *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $ignore--; Chris@0: continue; Chris@0: } else if ($tokens[$opener]['code'] === T_OPEN_CURLY_BRACKET Chris@0: && $tokenType !== T_CLOSE_CURLY_BRACKET Chris@0: ) { Chris@0: // The opener is a curly bracket so the closer must be a curly bracket as well. Chris@0: // We ignore this closer to handle cases such as T_ELSE or T_ELSEIF being considered Chris@0: // a closer of T_IF when it should not. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Ignoring non-curly scope closer for $stackPtr:$type".PHP_EOL; Chris@0: } Chris@0: } else { Chris@0: $scopeCloser = $i; Chris@0: $todo = array( Chris@0: $stackPtr, Chris@0: $opener, Chris@0: ); Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: $closerType = $tokens[$scopeCloser]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Found scope closer ($scopeCloser:$closerType) for $stackPtr:$type".PHP_EOL; Chris@0: } Chris@0: Chris@0: $validCloser = true; Chris@0: if (($tokens[$stackPtr]['code'] === T_IF || $tokens[$stackPtr]['code'] === T_ELSEIF) Chris@0: && ($tokenType === T_ELSE || $tokenType === T_ELSEIF) Chris@0: ) { Chris@0: // To be a closer, this token must have an opener. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "* closer needs to be tested *".PHP_EOL; Chris@0: } Chris@0: Chris@0: $i = self::_recurseScopeMap( Chris@0: $tokens, Chris@0: $numTokens, Chris@0: $tokenizer, Chris@0: $eolChar, Chris@0: $i, Chris@0: ($depth + 1), Chris@0: $ignore Chris@0: ); Chris@0: Chris@0: if (isset($tokens[$scopeCloser]['scope_opener']) === false) { Chris@0: $validCloser = false; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "* closer is not valid (no opener found) *".PHP_EOL; Chris@0: } Chris@0: } else if ($tokens[$tokens[$scopeCloser]['scope_opener']]['code'] !== $tokens[$opener]['code']) { Chris@0: $validCloser = false; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: $type = $tokens[$tokens[$scopeCloser]['scope_opener']]['type']; Chris@0: $openerType = $tokens[$opener]['type']; Chris@0: echo "* closer is not valid (mismatched opener type; $type != $openerType) *".PHP_EOL; Chris@0: } Chris@0: } else if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "* closer was valid *".PHP_EOL; Chris@0: } Chris@0: } else { Chris@0: // The closer was not processed, so we need to Chris@0: // complete that token as well. Chris@0: $todo[] = $scopeCloser; Chris@0: }//end if Chris@0: Chris@0: if ($validCloser === true) { Chris@0: foreach ($todo as $token) { Chris@0: $tokens[$token]['scope_condition'] = $stackPtr; Chris@0: $tokens[$token]['scope_opener'] = $opener; Chris@0: $tokens[$token]['scope_closer'] = $scopeCloser; Chris@0: } Chris@0: Chris@0: if ($tokenizer->scopeOpeners[$tokens[$stackPtr]['code']]['shared'] === true) { Chris@0: // As we are going back to where we started originally, restore Chris@0: // the ignore value back to its original value. Chris@0: $ignore = $originalIgnore; Chris@0: return $opener; Chris@0: } else if ($scopeCloser === $i Chris@0: && isset($tokenizer->scopeOpeners[$tokenType]) === true Chris@0: ) { Chris@0: // Unset scope_condition here or else the token will appear to have Chris@0: // already been processed, and it will be skipped. Normally we want that, Chris@0: // but in this case, the token is both a closer and an opener, so Chris@0: // it needs to act like an opener. This is also why we return the Chris@0: // token before this one; so the closer has a chance to be processed Chris@0: // a second time, but as an opener. Chris@0: unset($tokens[$scopeCloser]['scope_condition']); Chris@0: return ($i - 1); Chris@0: } else { Chris@0: return $i; Chris@0: } Chris@0: } else { Chris@0: continue; Chris@0: }//end if Chris@0: }//end if Chris@0: }//end if Chris@0: Chris@0: // Is this an opening condition ? Chris@0: if (isset($tokenizer->scopeOpeners[$tokenType]) === true) { Chris@0: if ($opener === null) { Chris@0: if ($tokenType === T_USE) { Chris@0: // PHP use keywords are special because they can be Chris@0: // used as blocks but also inline in function definitions. Chris@0: // So if we find them nested inside another opener, just skip them. Chris@0: continue; Chris@0: } Chris@0: Chris@0: if ($tokenType === T_FUNCTION Chris@0: && $tokens[$stackPtr]['code'] !== T_FUNCTION Chris@0: ) { Chris@0: // Probably a closure, so process it manually. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Found function before scope opener for $stackPtr:$type, processing manually".PHP_EOL; Chris@0: } Chris@0: Chris@0: if (isset($tokens[$i]['scope_closer']) === true) { Chris@0: // We've already processed this closure. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* already processed, skipping *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $i = $tokens[$i]['scope_closer']; Chris@0: continue; Chris@0: } Chris@0: Chris@0: $i = self::_recurseScopeMap( Chris@0: $tokens, Chris@0: $numTokens, Chris@0: $tokenizer, Chris@0: $eolChar, Chris@0: $i, Chris@0: ($depth + 1), Chris@0: $ignore Chris@0: ); Chris@0: Chris@0: continue; Chris@0: }//end if Chris@0: Chris@0: // Found another opening condition but still haven't Chris@0: // found our opener, so we are never going to find one. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Found new opening condition before scope opener for $stackPtr:$type, "; Chris@0: } Chris@0: Chris@0: if (($tokens[$stackPtr]['code'] === T_IF Chris@0: || $tokens[$stackPtr]['code'] === T_ELSEIF Chris@0: || $tokens[$stackPtr]['code'] === T_ELSE) Chris@0: && ($tokens[$i]['code'] === T_ELSE Chris@0: || $tokens[$i]['code'] === T_ELSEIF) Chris@0: ) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "continuing".PHP_EOL; Chris@0: } Chris@0: Chris@0: return ($i - 1); Chris@0: } else { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "backtracking".PHP_EOL; Chris@0: } Chris@0: Chris@0: return $stackPtr; Chris@0: } Chris@0: }//end if Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* token is an opening condition *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $isShared = ($tokenizer->scopeOpeners[$tokenType]['shared'] === true); Chris@0: Chris@0: if (isset($tokens[$i]['scope_condition']) === true) { Chris@0: // We've been here before. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* already processed, skipping *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: if ($isShared === false Chris@0: && isset($tokens[$i]['scope_closer']) === true Chris@0: ) { Chris@0: $i = $tokens[$i]['scope_closer']; Chris@0: } Chris@0: Chris@0: continue; Chris@0: } else if ($currType === $tokenType Chris@0: && $isShared === false Chris@0: && $opener === null Chris@0: ) { Chris@0: // We haven't yet found our opener, but we have found another Chris@0: // scope opener which is the same type as us, and we don't Chris@0: // share openers, so we will never find one. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* it was another token\'s opener, bailing *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: return $stackPtr; Chris@0: } else { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* searching for opener *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: if (isset($tokenizer->scopeOpeners[$tokenType]['end'][T_CLOSE_CURLY_BRACKET]) === true) { Chris@0: $oldIgnore = $ignore; Chris@0: $ignore = 0; Chris@0: } Chris@0: Chris@0: // PHP has a max nesting level for functions. Stop before we hit that limit Chris@0: // because too many loops means we've run into trouble anyway. Chris@0: if ($depth > 50) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* reached maximum nesting level; aborting *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: throw new PHP_CodeSniffer_Exception('Maximum nesting level reached; file could not be processed'); Chris@0: } Chris@0: Chris@0: $oldDepth = $depth; Chris@0: if ($isShared === true Chris@0: && isset($tokenizer->scopeOpeners[$tokenType]['with'][$currType]) === true Chris@0: ) { Chris@0: // Don't allow the depth to increment because this is Chris@0: // possibly not a true nesting if we are sharing our closer. Chris@0: // This can happen, for example, when a SWITCH has a large Chris@0: // number of CASE statements with the same shared BREAK. Chris@0: $depth--; Chris@0: } Chris@0: Chris@0: $i = self::_recurseScopeMap( Chris@0: $tokens, Chris@0: $numTokens, Chris@0: $tokenizer, Chris@0: $eolChar, Chris@0: $i, Chris@0: ($depth + 1), Chris@0: $ignore Chris@0: ); Chris@0: Chris@0: $depth = $oldDepth; Chris@0: Chris@0: if (isset($tokenizer->scopeOpeners[$tokenType]['end'][T_CLOSE_CURLY_BRACKET]) === true) { Chris@0: $ignore = $oldIgnore; Chris@0: } Chris@0: }//end if Chris@0: }//end if Chris@0: Chris@0: if (isset($tokenizer->scopeOpeners[$currType]['start'][$tokenType]) === true Chris@0: && $opener === null Chris@0: ) { Chris@0: if ($tokenType === T_OPEN_CURLY_BRACKET) { Chris@0: if (isset($tokens[$stackPtr]['parenthesis_closer']) === true Chris@0: && $i < $tokens[$stackPtr]['parenthesis_closer'] Chris@0: ) { Chris@0: // We found a curly brace inside the condition of the Chris@0: // current scope opener, so it must be a string offset. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* ignoring curly brace *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $ignore++; Chris@0: } else { Chris@0: // Make sure this is actually an opener and not a Chris@0: // string offset (e.g., $var{0}). Chris@0: for ($x = ($i - 1); $x > 0; $x--) { Chris@0: if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$x]['code']]) === true) { Chris@0: continue; Chris@0: } else { Chris@0: // If the first non-whitespace/comment token is a Chris@0: // variable or object operator then this is an opener Chris@0: // for a string offset and not a scope. Chris@0: if ($tokens[$x]['code'] === T_VARIABLE Chris@0: || $tokens[$x]['code'] === T_OBJECT_OPERATOR Chris@0: ) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* ignoring curly brace *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $ignore++; Chris@0: }//end if Chris@0: Chris@0: break; Chris@0: }//end if Chris@0: }//end for Chris@0: }//end if Chris@0: }//end if Chris@0: Chris@0: if ($ignore === 0 || $tokenType !== T_OPEN_CURLY_BRACKET) { Chris@0: // We found the opening scope token for $currType. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Found scope opener for $stackPtr:$type".PHP_EOL; Chris@0: } Chris@0: Chris@0: $opener = $i; Chris@0: } Chris@0: } else if ($tokenType === T_OPEN_PARENTHESIS) { Chris@0: if (isset($tokens[$i]['parenthesis_owner']) === true) { Chris@0: $owner = $tokens[$i]['parenthesis_owner']; Chris@0: if (isset(PHP_CodeSniffer_Tokens::$scopeOpeners[$tokens[$owner]['code']]) === true Chris@0: && isset($tokens[$i]['parenthesis_closer']) === true Chris@0: ) { Chris@0: // If we get into here, then we opened a parenthesis for Chris@0: // a scope (eg. an if or else if) so we need to update the Chris@0: // start of the line so that when we check to see Chris@0: // if the closing parenthesis is more than 3 lines away from Chris@0: // the statement, we check from the closing parenthesis. Chris@0: $startLine = $tokens[$tokens[$i]['parenthesis_closer']]['line']; Chris@0: } Chris@0: } Chris@0: } else if ($tokenType === T_OPEN_CURLY_BRACKET && $opener !== null) { Chris@0: // We opened something that we don't have a scope opener for. Chris@0: // Examples of this are curly brackets for string offsets etc. Chris@0: // We want to ignore this so that we don't have an invalid scope Chris@0: // map. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* ignoring curly brace *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $ignore++; Chris@0: } else if ($tokenType === T_CLOSE_CURLY_BRACKET && $ignore > 0) { Chris@0: // We found the end token for the opener we were ignoring. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* finished ignoring curly brace *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $ignore--; Chris@0: } else if ($opener === null Chris@0: && isset($tokenizer->scopeOpeners[$currType]) === true Chris@0: ) { Chris@0: // If we still haven't found the opener after 3 lines, Chris@0: // we're not going to find it, unless we know it requires Chris@0: // an opener (in which case we better keep looking) or the last Chris@0: // token was empty (in which case we'll just confirm there is Chris@0: // more code in this file and not just a big comment). Chris@0: if ($tokens[$i]['line'] >= ($startLine + 3) Chris@0: && isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[($i - 1)]['code']]) === false Chris@0: ) { Chris@0: if ($tokenizer->scopeOpeners[$currType]['strict'] === true) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: $lines = ($tokens[$i]['line'] - $startLine); Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Still looking for $stackPtr:$type scope opener after $lines lines".PHP_EOL; Chris@0: } Chris@0: } else { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Couldn't find scope opener for $stackPtr:$type, bailing".PHP_EOL; Chris@0: } Chris@0: Chris@0: return $stackPtr; Chris@0: } Chris@0: } Chris@0: } else if ($opener !== null Chris@0: && $tokenType !== T_BREAK Chris@0: && isset($tokenizer->endScopeTokens[$tokenType]) === true Chris@0: ) { Chris@0: if (isset($tokens[$i]['scope_condition']) === false) { Chris@0: if ($ignore > 0) { Chris@0: // We found the end token for the opener we were ignoring. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", $depth); Chris@0: echo '* finished ignoring curly brace *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $ignore--; Chris@0: } else { Chris@0: // We found a token that closes the scope but it doesn't Chris@0: // have a condition, so it belongs to another token and Chris@0: // our token doesn't have a closer, so pretend this is Chris@0: // the closer. Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", $depth); Chris@0: echo "=> Found (unexpected) scope closer for $stackPtr:$type".PHP_EOL; Chris@0: } Chris@0: Chris@0: foreach (array($stackPtr, $opener) as $token) { Chris@0: $tokens[$token]['scope_condition'] = $stackPtr; Chris@0: $tokens[$token]['scope_opener'] = $opener; Chris@0: $tokens[$token]['scope_closer'] = $i; Chris@0: } Chris@0: Chris@0: return ($i - 1); Chris@0: }//end if Chris@0: }//end if Chris@0: }//end if Chris@0: }//end for Chris@0: Chris@0: return $stackPtr; Chris@0: Chris@0: }//end _recurseScopeMap() Chris@0: Chris@0: Chris@0: /** Chris@0: * Constructs the level map. Chris@0: * Chris@0: * The level map adds a 'level' index to each token which indicates the Chris@0: * depth that a token within a set of scope blocks. It also adds a Chris@0: * 'condition' index which is an array of the scope conditions that opened Chris@0: * each of the scopes - position 0 being the first scope opener. Chris@0: * Chris@0: * @param array $tokens The array of tokens to process. Chris@0: * @param object $tokenizer The tokenizer being used to process this file. Chris@0: * @param string $eolChar The EOL character to use for splitting strings. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: private static function _createLevelMap(&$tokens, $tokenizer, $eolChar) Chris@0: { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "\t*** START LEVEL MAP ***".PHP_EOL; Chris@0: } Chris@0: Chris@0: $numTokens = count($tokens); Chris@0: $level = 0; Chris@0: $conditions = array(); Chris@0: $lastOpener = null; Chris@0: $openers = array(); Chris@0: Chris@0: for ($i = 0; $i < $numTokens; $i++) { Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$i]['type']; Chris@0: $line = $tokens[$i]['line']; Chris@0: $len = $tokens[$i]['length']; Chris@0: $col = $tokens[$i]['column']; Chris@0: Chris@0: $content = PHP_CodeSniffer::prepareForOutput($tokens[$i]['content']); Chris@0: Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "Process token $i on line $line [col:$col;len:$len;lvl:$level;"; Chris@0: if (empty($conditions) !== true) { Chris@0: $condString = 'conds;'; Chris@0: foreach ($conditions as $condition) { Chris@0: $condString .= token_name($condition).','; Chris@0: } Chris@0: Chris@0: echo rtrim($condString, ',').';'; Chris@0: } Chris@0: Chris@0: echo "]: $type => $content".PHP_EOL; Chris@0: }//end if Chris@0: Chris@0: $tokens[$i]['level'] = $level; Chris@0: $tokens[$i]['conditions'] = $conditions; Chris@0: Chris@0: if (isset($tokens[$i]['scope_condition']) === true) { Chris@0: // Check to see if this token opened the scope. Chris@0: if ($tokens[$i]['scope_opener'] === $i) { Chris@0: $stackPtr = $tokens[$i]['scope_condition']; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "=> Found scope opener for $stackPtr:$type".PHP_EOL; Chris@0: } Chris@0: Chris@0: $stackPtr = $tokens[$i]['scope_condition']; Chris@0: Chris@0: // If we find a scope opener that has a shared closer, Chris@0: // then we need to go back over the condition map that we Chris@0: // just created and fix ourselves as we just added some Chris@0: // conditions where there was none. This happens for T_CASE Chris@0: // statements that are using the same break statement. Chris@0: if ($lastOpener !== null && $tokens[$lastOpener]['scope_closer'] === $tokens[$i]['scope_closer']) { Chris@0: // This opener shares its closer with the previous opener, Chris@0: // but we still need to check if the two openers share their Chris@0: // closer with each other directly (like CASE and DEFAULT) Chris@0: // or if they are just sharing because one doesn't have a Chris@0: // closer (like CASE with no BREAK using a SWITCHes closer). Chris@0: $thisType = $tokens[$tokens[$i]['scope_condition']]['code']; Chris@0: $opener = $tokens[$lastOpener]['scope_condition']; Chris@0: Chris@0: $isShared = isset($tokenizer->scopeOpeners[$thisType]['with'][$tokens[$opener]['code']]); Chris@0: Chris@0: reset($tokenizer->scopeOpeners[$thisType]['end']); Chris@0: reset($tokenizer->scopeOpeners[$tokens[$opener]['code']]['end']); Chris@0: $sameEnd = (current($tokenizer->scopeOpeners[$thisType]['end']) === current($tokenizer->scopeOpeners[$tokens[$opener]['code']]['end'])); Chris@0: Chris@0: if ($isShared === true && $sameEnd === true) { Chris@0: $badToken = $opener; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$badToken]['type']; Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "* shared closer, cleaning up $badToken:$type *".PHP_EOL; Chris@0: } Chris@0: Chris@0: for ($x = $tokens[$i]['scope_condition']; $x <= $i; $x++) { Chris@0: $oldConditions = $tokens[$x]['conditions']; Chris@0: $oldLevel = $tokens[$x]['level']; Chris@0: $tokens[$x]['level']--; Chris@0: unset($tokens[$x]['conditions'][$badToken]); Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$x]['type']; Chris@0: $oldConds = ''; Chris@0: foreach ($oldConditions as $condition) { Chris@0: $oldConds .= token_name($condition).','; Chris@0: } Chris@0: Chris@0: $oldConds = rtrim($oldConds, ','); Chris@0: Chris@0: $newConds = ''; Chris@0: foreach ($tokens[$x]['conditions'] as $condition) { Chris@0: $newConds .= token_name($condition).','; Chris@0: } Chris@0: Chris@0: $newConds = rtrim($newConds, ','); Chris@0: Chris@0: $newLevel = $tokens[$x]['level']; Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "* cleaned $x:$type *".PHP_EOL; Chris@0: echo str_repeat("\t", ($level + 2)); Chris@0: echo "=> level changed from $oldLevel to $newLevel".PHP_EOL; Chris@0: echo str_repeat("\t", ($level + 2)); Chris@0: echo "=> conditions changed from $oldConds to $newConds".PHP_EOL; Chris@0: }//end if Chris@0: }//end for Chris@0: Chris@0: unset($conditions[$badToken]); Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$badToken]['type']; Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "* token $badToken:$type removed from conditions array *".PHP_EOL; Chris@0: } Chris@0: Chris@0: unset($openers[$lastOpener]); Chris@0: Chris@0: $level--; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", ($level + 2)); Chris@0: echo '* level decreased *'.PHP_EOL; Chris@0: } Chris@0: }//end if Chris@0: }//end if Chris@0: Chris@0: $level++; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo '* level increased *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $conditions[$stackPtr] = $tokens[$stackPtr]['code']; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$stackPtr]['type']; Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "* token $stackPtr:$type added to conditions array *".PHP_EOL; Chris@0: } Chris@0: Chris@0: $lastOpener = $tokens[$i]['scope_opener']; Chris@0: if ($lastOpener !== null) { Chris@0: $openers[$lastOpener] = $lastOpener; Chris@0: } Chris@0: } else if ($lastOpener !== null && $tokens[$lastOpener]['scope_closer'] === $i) { Chris@0: foreach (array_reverse($openers) as $opener) { Chris@0: if ($tokens[$opener]['scope_closer'] === $i) { Chris@0: $oldOpener = array_pop($openers); Chris@0: if (empty($openers) === false) { Chris@0: $lastOpener = array_pop($openers); Chris@0: $openers[$lastOpener] = $lastOpener; Chris@0: } else { Chris@0: $lastOpener = null; Chris@0: } Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$oldOpener]['type']; Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "=> Found scope closer for $oldOpener:$type".PHP_EOL; Chris@0: } Chris@0: Chris@0: $oldCondition = array_pop($conditions); Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo '* token '.token_name($oldCondition).' removed from conditions array *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: // Make sure this closer actually belongs to us. Chris@0: // Either the condition also has to think this is the Chris@0: // closer, or it has to allow sharing with us. Chris@0: $condition = $tokens[$tokens[$i]['scope_condition']]['code']; Chris@0: if ($condition !== $oldCondition) { Chris@0: if (isset($tokenizer->scopeOpeners[$oldCondition]['with'][$condition]) === false) { Chris@0: $badToken = $tokens[$oldOpener]['scope_condition']; Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = token_name($oldCondition); Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "* scope closer was bad, cleaning up $badToken:$type *".PHP_EOL; Chris@0: } Chris@0: Chris@0: for ($x = ($oldOpener + 1); $x <= $i; $x++) { Chris@0: $oldConditions = $tokens[$x]['conditions']; Chris@0: $oldLevel = $tokens[$x]['level']; Chris@0: $tokens[$x]['level']--; Chris@0: unset($tokens[$x]['conditions'][$badToken]); Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: $type = $tokens[$x]['type']; Chris@0: $oldConds = ''; Chris@0: foreach ($oldConditions as $condition) { Chris@0: $oldConds .= token_name($condition).','; Chris@0: } Chris@0: Chris@0: $oldConds = rtrim($oldConds, ','); Chris@0: Chris@0: $newConds = ''; Chris@0: foreach ($tokens[$x]['conditions'] as $condition) { Chris@0: $newConds .= token_name($condition).','; Chris@0: } Chris@0: Chris@0: $newConds = rtrim($newConds, ','); Chris@0: Chris@0: $newLevel = $tokens[$x]['level']; Chris@0: echo str_repeat("\t", ($level + 1)); Chris@0: echo "* cleaned $x:$type *".PHP_EOL; Chris@0: echo str_repeat("\t", ($level + 2)); Chris@0: echo "=> level changed from $oldLevel to $newLevel".PHP_EOL; Chris@0: echo str_repeat("\t", ($level + 2)); Chris@0: echo "=> conditions changed from $oldConds to $newConds".PHP_EOL; Chris@0: }//end if Chris@0: }//end for Chris@0: }//end if Chris@0: }//end if Chris@0: Chris@0: $level--; Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo str_repeat("\t", ($level + 2)); Chris@0: echo '* level decreased *'.PHP_EOL; Chris@0: } Chris@0: Chris@0: $tokens[$i]['level'] = $level; Chris@0: $tokens[$i]['conditions'] = $conditions; Chris@0: }//end if Chris@0: }//end foreach Chris@0: }//end if Chris@0: }//end if Chris@0: }//end for Chris@0: Chris@0: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@0: echo "\t*** END LEVEL MAP ***".PHP_EOL; Chris@0: } Chris@0: Chris@0: }//end _createLevelMap() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the declaration names for classes, interfaces, and functions. Chris@0: * Chris@0: * @param int $stackPtr The position of the declaration token which Chris@0: * declared the class, interface or function. Chris@0: * Chris@0: * @return string|null The name of the class, interface or function. Chris@0: * or NULL if the function or class is anonymous. Chris@0: * @throws PHP_CodeSniffer_Exception If the specified token is not of type Chris@0: * T_FUNCTION, T_CLASS, T_ANON_CLASS, Chris@0: * or T_INTERFACE. Chris@0: */ Chris@0: public function getDeclarationName($stackPtr) Chris@0: { Chris@0: $tokenCode = $this->_tokens[$stackPtr]['code']; Chris@0: Chris@0: if ($tokenCode === T_ANON_CLASS) { Chris@0: return null; Chris@0: } Chris@0: Chris@0: if ($tokenCode === T_FUNCTION Chris@0: && $this->isAnonymousFunction($stackPtr) === true Chris@0: ) { Chris@0: return null; Chris@0: } Chris@0: Chris@0: if ($tokenCode !== T_FUNCTION Chris@0: && $tokenCode !== T_CLASS Chris@0: && $tokenCode !== T_INTERFACE Chris@0: && $tokenCode !== T_TRAIT Chris@0: ) { Chris@0: throw new PHP_CodeSniffer_Exception('Token type "'.$this->_tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT'); Chris@0: } Chris@0: Chris@0: $content = null; Chris@0: for ($i = $stackPtr; $i < $this->numTokens; $i++) { Chris@0: if ($this->_tokens[$i]['code'] === T_STRING) { Chris@0: $content = $this->_tokens[$i]['content']; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: return $content; Chris@0: Chris@0: }//end getDeclarationName() Chris@0: Chris@0: Chris@0: /** Chris@0: * Check if the token at the specified position is a anonymous function. Chris@0: * Chris@0: * @param int $stackPtr The position of the declaration token which Chris@0: * declared the class, interface or function. Chris@0: * Chris@0: * @return boolean Chris@0: * @throws PHP_CodeSniffer_Exception If the specified token is not of type Chris@0: * T_FUNCTION Chris@0: */ Chris@0: public function isAnonymousFunction($stackPtr) Chris@0: { Chris@0: $tokenCode = $this->_tokens[$stackPtr]['code']; Chris@0: if ($tokenCode !== T_FUNCTION) { Chris@0: throw new PHP_CodeSniffer_Exception('Token type is not T_FUNCTION'); Chris@0: } Chris@0: Chris@0: if (isset($this->_tokens[$stackPtr]['parenthesis_opener']) === false) { Chris@0: // Something is not right with this function. Chris@0: return false; Chris@0: } Chris@0: Chris@0: $name = false; Chris@0: for ($i = ($stackPtr + 1); $i < $this->numTokens; $i++) { Chris@0: if ($this->_tokens[$i]['code'] === T_STRING) { Chris@0: $name = $i; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($name === false) { Chris@0: // No name found. Chris@0: return true; Chris@0: } Chris@0: Chris@0: $open = $this->_tokens[$stackPtr]['parenthesis_opener']; Chris@0: if ($name > $open) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end isAnonymousFunction() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the method parameters for the specified function token. Chris@0: * Chris@0: * Each parameter is in the following format: Chris@0: * Chris@0: * Chris@0: * 0 => array( Chris@0: * 'token' => int, // The position of the var in the token stack. Chris@0: * 'name' => '$var', // The variable name. Chris@0: * 'content' => string, // The full content of the variable definition. Chris@0: * 'pass_by_reference' => boolean, // Is the variable passed by reference? Chris@0: * 'type_hint' => string, // The type hint for the variable. Chris@0: * 'nullable_type' => boolean, // Is the variable using a nullable type? Chris@0: * ) Chris@0: * Chris@0: * Chris@0: * Parameters with default values have an additional array index of Chris@0: * 'default' with the value of the default as a string. Chris@0: * Chris@0: * @param int $stackPtr The position in the stack of the function token Chris@0: * to acquire the parameters for. Chris@0: * Chris@0: * @return array Chris@0: * @throws PHP_CodeSniffer_Exception If the specified $stackPtr is not of Chris@0: * type T_FUNCTION or T_CLOSURE. Chris@0: */ Chris@0: public function getMethodParameters($stackPtr) Chris@0: { Chris@0: if ($this->_tokens[$stackPtr]['code'] !== T_FUNCTION Chris@0: && $this->_tokens[$stackPtr]['code'] !== T_CLOSURE Chris@0: ) { Chris@0: throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_FUNCTION or T_CLOSURE'); Chris@0: } Chris@0: Chris@0: $opener = $this->_tokens[$stackPtr]['parenthesis_opener']; Chris@0: $closer = $this->_tokens[$stackPtr]['parenthesis_closer']; Chris@0: Chris@0: $vars = array(); Chris@0: $currVar = null; Chris@0: $paramStart = ($opener + 1); Chris@0: $defaultStart = null; Chris@0: $paramCount = 0; Chris@0: $passByReference = false; Chris@0: $variableLength = false; Chris@0: $typeHint = ''; Chris@0: $nullableType = false; Chris@0: Chris@0: for ($i = $paramStart; $i <= $closer; $i++) { Chris@0: // Check to see if this token has a parenthesis or bracket opener. If it does Chris@0: // it's likely to be an array which might have arguments in it. This Chris@0: // could cause problems in our parsing below, so lets just skip to the Chris@0: // end of it. Chris@0: if (isset($this->_tokens[$i]['parenthesis_opener']) === true) { Chris@0: // Don't do this if it's the close parenthesis for the method. Chris@0: if ($i !== $this->_tokens[$i]['parenthesis_closer']) { Chris@0: $i = ($this->_tokens[$i]['parenthesis_closer'] + 1); Chris@0: } Chris@0: } Chris@0: Chris@0: if (isset($this->_tokens[$i]['bracket_opener']) === true) { Chris@0: // Don't do this if it's the close parenthesis for the method. Chris@0: if ($i !== $this->_tokens[$i]['bracket_closer']) { Chris@0: $i = ($this->_tokens[$i]['bracket_closer'] + 1); Chris@0: } Chris@0: } Chris@0: Chris@0: switch ($this->_tokens[$i]['code']) { Chris@0: case T_BITWISE_AND: Chris@0: $passByReference = true; Chris@0: break; Chris@0: case T_VARIABLE: Chris@0: $currVar = $i; Chris@0: break; Chris@0: case T_ELLIPSIS: Chris@0: $variableLength = true; Chris@0: break; Chris@0: case T_ARRAY_HINT: Chris@0: case T_CALLABLE: Chris@0: $typeHint .= $this->_tokens[$i]['content']; Chris@0: break; Chris@0: case T_SELF: Chris@0: case T_PARENT: Chris@0: case T_STATIC: Chris@0: // Self is valid, the others invalid, but were probably intended as type hints. Chris@0: if (isset($defaultStart) === false) { Chris@0: $typeHint .= $this->_tokens[$i]['content']; Chris@0: } Chris@0: break; Chris@0: case T_STRING: Chris@0: // This is a string, so it may be a type hint, but it could Chris@0: // also be a constant used as a default value. Chris@0: $prevComma = false; Chris@0: for ($t = $i; $t >= $opener; $t--) { Chris@0: if ($this->_tokens[$t]['code'] === T_COMMA) { Chris@0: $prevComma = $t; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($prevComma !== false) { Chris@0: $nextEquals = false; Chris@0: for ($t = $prevComma; $t < $i; $t++) { Chris@0: if ($this->_tokens[$t]['code'] === T_EQUAL) { Chris@0: $nextEquals = $t; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($nextEquals !== false) { Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($defaultStart === null) { Chris@0: $typeHint .= $this->_tokens[$i]['content']; Chris@0: } Chris@0: break; Chris@0: case T_NS_SEPARATOR: Chris@0: // Part of a type hint or default value. Chris@0: if ($defaultStart === null) { Chris@0: $typeHint .= $this->_tokens[$i]['content']; Chris@0: } Chris@0: break; Chris@0: case T_NULLABLE: Chris@0: if ($defaultStart === null) { Chris@0: $nullableType = true; Chris@0: $typeHint .= $this->_tokens[$i]['content']; Chris@0: } Chris@0: break; Chris@0: case T_CLOSE_PARENTHESIS: Chris@0: case T_COMMA: Chris@0: // If it's null, then there must be no parameters for this Chris@0: // method. Chris@0: if ($currVar === null) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $vars[$paramCount] = array(); Chris@0: $vars[$paramCount]['token'] = $currVar; Chris@0: $vars[$paramCount]['name'] = $this->_tokens[$currVar]['content']; Chris@0: $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart))); Chris@0: Chris@0: if ($defaultStart !== null) { Chris@0: $vars[$paramCount]['default'] = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart))); Chris@0: } Chris@0: Chris@0: $vars[$paramCount]['pass_by_reference'] = $passByReference; Chris@0: $vars[$paramCount]['variable_length'] = $variableLength; Chris@0: $vars[$paramCount]['type_hint'] = $typeHint; Chris@0: $vars[$paramCount]['nullable_type'] = $nullableType; Chris@0: Chris@0: // Reset the vars, as we are about to process the next parameter. Chris@0: $defaultStart = null; Chris@0: $paramStart = ($i + 1); Chris@0: $passByReference = false; Chris@0: $variableLength = false; Chris@0: $typeHint = ''; Chris@0: $nullableType = false; Chris@0: Chris@0: $paramCount++; Chris@0: break; Chris@0: case T_EQUAL: Chris@0: $defaultStart = ($i + 1); Chris@0: break; Chris@0: }//end switch Chris@0: }//end for Chris@0: Chris@0: return $vars; Chris@0: Chris@0: }//end getMethodParameters() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the visibility and implementation properties of a method. Chris@0: * Chris@0: * The format of the array is: Chris@0: * Chris@0: * array( Chris@0: * 'scope' => 'public', // public private or protected Chris@0: * 'scope_specified' => true, // true is scope keyword was found. Chris@0: * 'is_abstract' => false, // true if the abstract keyword was found. Chris@0: * 'is_final' => false, // true if the final keyword was found. Chris@0: * 'is_static' => false, // true if the static keyword was found. Chris@0: * 'is_closure' => false, // true if no name is found. Chris@0: * ); Chris@0: * Chris@0: * Chris@0: * @param int $stackPtr The position in the stack of the T_FUNCTION token to Chris@0: * acquire the properties for. Chris@0: * Chris@0: * @return array Chris@0: * @throws PHP_CodeSniffer_Exception If the specified position is not a Chris@0: * T_FUNCTION token. Chris@0: */ Chris@0: public function getMethodProperties($stackPtr) Chris@0: { Chris@0: if ($this->_tokens[$stackPtr]['code'] !== T_FUNCTION) { Chris@0: throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_FUNCTION'); Chris@0: } Chris@0: Chris@0: $valid = array( Chris@0: T_PUBLIC => T_PUBLIC, Chris@0: T_PRIVATE => T_PRIVATE, Chris@0: T_PROTECTED => T_PROTECTED, Chris@0: T_STATIC => T_STATIC, Chris@0: T_FINAL => T_FINAL, Chris@0: T_ABSTRACT => T_ABSTRACT, Chris@0: T_WHITESPACE => T_WHITESPACE, Chris@0: T_COMMENT => T_COMMENT, Chris@0: T_DOC_COMMENT => T_DOC_COMMENT, Chris@0: ); Chris@0: Chris@0: $scope = 'public'; Chris@0: $scopeSpecified = false; Chris@0: $isAbstract = false; Chris@0: $isFinal = false; Chris@0: $isStatic = false; Chris@0: $isClosure = $this->isAnonymousFunction($stackPtr); Chris@0: Chris@0: for ($i = ($stackPtr - 1); $i > 0; $i--) { Chris@0: if (isset($valid[$this->_tokens[$i]['code']]) === false) { Chris@0: break; Chris@0: } Chris@0: Chris@0: switch ($this->_tokens[$i]['code']) { Chris@0: case T_PUBLIC: Chris@0: $scope = 'public'; Chris@0: $scopeSpecified = true; Chris@0: break; Chris@0: case T_PRIVATE: Chris@0: $scope = 'private'; Chris@0: $scopeSpecified = true; Chris@0: break; Chris@0: case T_PROTECTED: Chris@0: $scope = 'protected'; Chris@0: $scopeSpecified = true; Chris@0: break; Chris@0: case T_ABSTRACT: Chris@0: $isAbstract = true; Chris@0: break; Chris@0: case T_FINAL: Chris@0: $isFinal = true; Chris@0: break; Chris@0: case T_STATIC: Chris@0: $isStatic = true; Chris@0: break; Chris@0: }//end switch Chris@0: }//end for Chris@0: Chris@0: return array( Chris@0: 'scope' => $scope, Chris@0: 'scope_specified' => $scopeSpecified, Chris@0: 'is_abstract' => $isAbstract, Chris@0: 'is_final' => $isFinal, Chris@0: 'is_static' => $isStatic, Chris@0: 'is_closure' => $isClosure, Chris@0: ); Chris@0: Chris@0: }//end getMethodProperties() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the visibility and implementation properties of the class member Chris@0: * variable found at the specified position in the stack. Chris@0: * Chris@0: * The format of the array is: Chris@0: * Chris@0: * Chris@0: * array( Chris@0: * 'scope' => 'public', // public private or protected Chris@0: * 'is_static' => false, // true if the static keyword was found. Chris@0: * ); Chris@0: * Chris@0: * Chris@0: * @param int $stackPtr The position in the stack of the T_VARIABLE token to Chris@0: * acquire the properties for. Chris@0: * Chris@0: * @return array Chris@0: * @throws PHP_CodeSniffer_Exception If the specified position is not a Chris@0: * T_VARIABLE token, or if the position is not Chris@0: * a class member variable. Chris@0: */ Chris@0: public function getMemberProperties($stackPtr) Chris@0: { Chris@0: if ($this->_tokens[$stackPtr]['code'] !== T_VARIABLE) { Chris@0: throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_VARIABLE'); Chris@0: } Chris@0: Chris@0: $conditions = array_keys($this->_tokens[$stackPtr]['conditions']); Chris@0: $ptr = array_pop($conditions); Chris@0: if (isset($this->_tokens[$ptr]) === false Chris@0: || ($this->_tokens[$ptr]['code'] !== T_CLASS Chris@0: && $this->_tokens[$ptr]['code'] !== T_ANON_CLASS Chris@0: && $this->_tokens[$ptr]['code'] !== T_TRAIT) Chris@0: ) { Chris@0: if (isset($this->_tokens[$ptr]) === true Chris@0: && $this->_tokens[$ptr]['code'] === T_INTERFACE Chris@0: ) { Chris@0: // T_VARIABLEs in interfaces can actually be method arguments Chris@0: // but they wont be seen as being inside the method because there Chris@0: // are no scope openers and closers for abstract methods. If it is in Chris@0: // parentheses, we can be pretty sure it is a method argument. Chris@0: if (isset($this->_tokens[$stackPtr]['nested_parenthesis']) === false Chris@0: || empty($this->_tokens[$stackPtr]['nested_parenthesis']) === true Chris@0: ) { Chris@0: $error = 'Possible parse error: interfaces may not include member vars'; Chris@0: $this->addWarning($error, $stackPtr, 'Internal.ParseError.InterfaceHasMemberVar'); Chris@0: return array(); Chris@0: } Chris@0: } else { Chris@0: throw new PHP_CodeSniffer_Exception('$stackPtr is not a class member var'); Chris@0: } Chris@0: } Chris@0: Chris@0: $valid = array( Chris@0: T_PUBLIC => T_PUBLIC, Chris@0: T_PRIVATE => T_PRIVATE, Chris@0: T_PROTECTED => T_PROTECTED, Chris@0: T_STATIC => T_STATIC, Chris@0: T_WHITESPACE => T_WHITESPACE, Chris@0: T_COMMENT => T_COMMENT, Chris@0: T_DOC_COMMENT => T_DOC_COMMENT, Chris@0: T_VARIABLE => T_VARIABLE, Chris@0: T_COMMA => T_COMMA, Chris@0: ); Chris@0: Chris@0: $scope = 'public'; Chris@0: $scopeSpecified = false; Chris@0: $isStatic = false; Chris@0: Chris@0: for ($i = ($stackPtr - 1); $i > 0; $i--) { Chris@0: if (isset($valid[$this->_tokens[$i]['code']]) === false) { Chris@0: break; Chris@0: } Chris@0: Chris@0: switch ($this->_tokens[$i]['code']) { Chris@0: case T_PUBLIC: Chris@0: $scope = 'public'; Chris@0: $scopeSpecified = true; Chris@0: break; Chris@0: case T_PRIVATE: Chris@0: $scope = 'private'; Chris@0: $scopeSpecified = true; Chris@0: break; Chris@0: case T_PROTECTED: Chris@0: $scope = 'protected'; Chris@0: $scopeSpecified = true; Chris@0: break; Chris@0: case T_STATIC: Chris@0: $isStatic = true; Chris@0: break; Chris@0: } Chris@0: }//end for Chris@0: Chris@0: return array( Chris@0: 'scope' => $scope, Chris@0: 'scope_specified' => $scopeSpecified, Chris@0: 'is_static' => $isStatic, Chris@0: ); Chris@0: Chris@0: }//end getMemberProperties() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the visibility and implementation properties of a class. Chris@0: * Chris@0: * The format of the array is: Chris@0: * Chris@0: * array( Chris@0: * 'is_abstract' => false, // true if the abstract keyword was found. Chris@0: * 'is_final' => false, // true if the final keyword was found. Chris@0: * ); Chris@0: * Chris@0: * Chris@0: * @param int $stackPtr The position in the stack of the T_CLASS token to Chris@0: * acquire the properties for. Chris@0: * Chris@0: * @return array Chris@0: * @throws PHP_CodeSniffer_Exception If the specified position is not a Chris@0: * T_CLASS token. Chris@0: */ Chris@0: public function getClassProperties($stackPtr) Chris@0: { Chris@0: if ($this->_tokens[$stackPtr]['code'] !== T_CLASS) { Chris@0: throw new PHP_CodeSniffer_Exception('$stackPtr must be of type T_CLASS'); Chris@0: } Chris@0: Chris@0: $valid = array( Chris@0: T_FINAL => T_FINAL, Chris@0: T_ABSTRACT => T_ABSTRACT, Chris@0: T_WHITESPACE => T_WHITESPACE, Chris@0: T_COMMENT => T_COMMENT, Chris@0: T_DOC_COMMENT => T_DOC_COMMENT, Chris@0: ); Chris@0: Chris@0: $isAbstract = false; Chris@0: $isFinal = false; Chris@0: Chris@0: for ($i = ($stackPtr - 1); $i > 0; $i--) { Chris@0: if (isset($valid[$this->_tokens[$i]['code']]) === false) { Chris@0: break; Chris@0: } Chris@0: Chris@0: switch ($this->_tokens[$i]['code']) { Chris@0: case T_ABSTRACT: Chris@0: $isAbstract = true; Chris@0: break; Chris@0: Chris@0: case T_FINAL: Chris@0: $isFinal = true; Chris@0: break; Chris@0: } Chris@0: }//end for Chris@0: Chris@0: return array( Chris@0: 'is_abstract' => $isAbstract, Chris@0: 'is_final' => $isFinal, Chris@0: ); Chris@0: Chris@0: }//end getClassProperties() Chris@0: Chris@0: Chris@0: /** Chris@0: * Determine if the passed token is a reference operator. Chris@0: * Chris@0: * Returns true if the specified token position represents a reference. Chris@0: * Returns false if the token represents a bitwise operator. Chris@0: * Chris@0: * @param int $stackPtr The position of the T_BITWISE_AND token. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function isReference($stackPtr) Chris@0: { Chris@0: if ($this->_tokens[$stackPtr]['code'] !== T_BITWISE_AND) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $tokenBefore = $this->findPrevious( Chris@0: PHP_CodeSniffer_Tokens::$emptyTokens, Chris@0: ($stackPtr - 1), Chris@0: null, Chris@0: true Chris@0: ); Chris@0: Chris@0: if ($this->_tokens[$tokenBefore]['code'] === T_FUNCTION) { Chris@0: // Function returns a reference. Chris@0: return true; Chris@0: } Chris@0: Chris@0: if ($this->_tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) { Chris@0: // Inside a foreach loop, this is a reference. Chris@0: return true; Chris@0: } Chris@0: Chris@0: if ($this->_tokens[$tokenBefore]['code'] === T_AS) { Chris@0: // Inside a foreach loop, this is a reference. Chris@0: return true; Chris@0: } Chris@0: Chris@0: if ($this->_tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY) { Chris@0: // Inside an array declaration, this is a reference. Chris@0: return true; Chris@0: } Chris@0: Chris@0: if (isset(PHP_CodeSniffer_Tokens::$assignmentTokens[$this->_tokens[$tokenBefore]['code']]) === true) { Chris@0: // This is directly after an assignment. It's a reference. Even if Chris@0: // it is part of an operation, the other tests will handle it. Chris@0: return true; Chris@0: } Chris@0: Chris@0: if (isset($this->_tokens[$stackPtr]['nested_parenthesis']) === true) { Chris@0: $brackets = $this->_tokens[$stackPtr]['nested_parenthesis']; Chris@0: $lastBracket = array_pop($brackets); Chris@0: if (isset($this->_tokens[$lastBracket]['parenthesis_owner']) === true) { Chris@0: $owner = $this->_tokens[$this->_tokens[$lastBracket]['parenthesis_owner']]; Chris@0: if ($owner['code'] === T_FUNCTION Chris@0: || $owner['code'] === T_CLOSURE Chris@0: || $owner['code'] === T_ARRAY Chris@0: ) { Chris@0: // Inside a function or array declaration, this is a reference. Chris@0: return true; Chris@0: } Chris@0: } else { Chris@0: $prev = false; Chris@0: for ($t = ($this->_tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) { Chris@0: if ($this->_tokens[$t]['code'] !== T_WHITESPACE) { Chris@0: $prev = $t; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($prev !== false && $this->_tokens[$prev]['code'] === T_USE) { Chris@0: return true; Chris@0: } Chris@0: }//end if Chris@0: }//end if Chris@0: Chris@0: $tokenAfter = $this->findNext( Chris@0: PHP_CodeSniffer_Tokens::$emptyTokens, Chris@0: ($stackPtr + 1), Chris@0: null, Chris@0: true Chris@0: ); Chris@0: Chris@0: if ($this->_tokens[$tokenAfter]['code'] === T_VARIABLE Chris@0: && ($this->_tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS Chris@0: || $this->_tokens[$tokenBefore]['code'] === T_COMMA) Chris@0: ) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end isReference() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the content of the tokens from the specified start position in Chris@0: * the token stack for the specified length. Chris@0: * Chris@0: * @param int $start The position to start from in the token stack. Chris@0: * @param int $length The length of tokens to traverse from the start pos. Chris@0: * Chris@0: * @return string The token contents. Chris@0: */ Chris@0: public function getTokensAsString($start, $length) Chris@0: { Chris@0: $str = ''; Chris@0: $end = ($start + $length); Chris@0: if ($end > $this->numTokens) { Chris@0: $end = $this->numTokens; Chris@0: } Chris@0: Chris@0: for ($i = $start; $i < $end; $i++) { Chris@0: $str .= $this->_tokens[$i]['content']; Chris@0: } Chris@0: Chris@0: return $str; Chris@0: Chris@0: }//end getTokensAsString() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the position of the previous specified token(s). Chris@0: * Chris@0: * If a value is specified, the previous token of the specified type(s) Chris@0: * containing the specified value will be returned. Chris@0: * Chris@0: * Returns false if no token can be found. Chris@0: * Chris@0: * @param int|array $types The type(s) of tokens to search for. Chris@0: * @param int $start The position to start searching from in the Chris@0: * token stack. Chris@0: * @param int $end The end position to fail if no token is found. Chris@0: * if not specified or null, end will default to Chris@0: * the start of the token stack. Chris@0: * @param bool $exclude If true, find the previous token that are NOT of Chris@0: * the types specified in $types. Chris@0: * @param string $value The value that the token(s) must be equal to. Chris@0: * If value is omitted, tokens with any value will Chris@0: * be returned. Chris@0: * @param bool $local If true, tokens outside the current statement Chris@0: * will not be checked. IE. checking will stop Chris@0: * at the previous semi-colon found. Chris@0: * Chris@0: * @return int|bool Chris@0: * @see findNext() Chris@0: */ Chris@0: public function findPrevious( Chris@0: $types, Chris@0: $start, Chris@0: $end=null, Chris@0: $exclude=false, Chris@0: $value=null, Chris@0: $local=false Chris@0: ) { Chris@0: $types = (array) $types; Chris@0: Chris@0: if ($end === null) { Chris@0: $end = 0; Chris@0: } Chris@0: Chris@0: for ($i = $start; $i >= $end; $i--) { Chris@0: $found = (bool) $exclude; Chris@0: foreach ($types as $type) { Chris@0: if ($this->_tokens[$i]['code'] === $type) { Chris@0: $found = !$exclude; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($found === true) { Chris@0: if ($value === null) { Chris@0: return $i; Chris@0: } else if ($this->_tokens[$i]['content'] === $value) { Chris@0: return $i; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($local === true) { Chris@0: if (isset($this->_tokens[$i]['scope_opener']) === true Chris@0: && $i === $this->_tokens[$i]['scope_closer'] Chris@0: ) { Chris@0: $i = $this->_tokens[$i]['scope_opener']; Chris@0: } else if (isset($this->_tokens[$i]['bracket_opener']) === true Chris@0: && $i === $this->_tokens[$i]['bracket_closer'] Chris@0: ) { Chris@0: $i = $this->_tokens[$i]['bracket_opener']; Chris@0: } else if (isset($this->_tokens[$i]['parenthesis_opener']) === true Chris@0: && $i === $this->_tokens[$i]['parenthesis_closer'] Chris@0: ) { Chris@0: $i = $this->_tokens[$i]['parenthesis_opener']; Chris@0: } else if ($this->_tokens[$i]['code'] === T_SEMICOLON) { Chris@0: break; Chris@0: } Chris@0: } Chris@0: }//end for Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end findPrevious() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the position of the next specified token(s). Chris@0: * Chris@0: * If a value is specified, the next token of the specified type(s) Chris@0: * containing the specified value will be returned. Chris@0: * Chris@0: * Returns false if no token can be found. Chris@0: * Chris@0: * @param int|array $types The type(s) of tokens to search for. Chris@0: * @param int $start The position to start searching from in the Chris@0: * token stack. Chris@0: * @param int $end The end position to fail if no token is found. Chris@0: * if not specified or null, end will default to Chris@0: * the end of the token stack. Chris@0: * @param bool $exclude If true, find the next token that is NOT of Chris@0: * a type specified in $types. Chris@0: * @param string $value The value that the token(s) must be equal to. Chris@0: * If value is omitted, tokens with any value will Chris@0: * be returned. Chris@0: * @param bool $local If true, tokens outside the current statement Chris@0: * will not be checked. i.e., checking will stop Chris@0: * at the next semi-colon found. Chris@0: * Chris@0: * @return int|bool Chris@0: * @see findPrevious() Chris@0: */ Chris@0: public function findNext( Chris@0: $types, Chris@0: $start, Chris@0: $end=null, Chris@0: $exclude=false, Chris@0: $value=null, Chris@0: $local=false Chris@0: ) { Chris@0: $types = (array) $types; Chris@0: Chris@0: if ($end === null || $end > $this->numTokens) { Chris@0: $end = $this->numTokens; Chris@0: } Chris@0: Chris@0: for ($i = $start; $i < $end; $i++) { Chris@0: $found = (bool) $exclude; Chris@0: foreach ($types as $type) { Chris@0: if ($this->_tokens[$i]['code'] === $type) { Chris@0: $found = !$exclude; Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($found === true) { Chris@0: if ($value === null) { Chris@0: return $i; Chris@0: } else if ($this->_tokens[$i]['content'] === $value) { Chris@0: return $i; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($local === true && $this->_tokens[$i]['code'] === T_SEMICOLON) { Chris@0: break; Chris@0: } Chris@0: }//end for Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end findNext() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the position of the first non-whitespace token in a statement. Chris@0: * Chris@0: * @param int $start The position to start searching from in the token stack. Chris@0: * @param int|array $ignore Token types that should not be considered stop points. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function findStartOfStatement($start, $ignore=null) Chris@0: { Chris@0: $endTokens = PHP_CodeSniffer_Tokens::$blockOpeners; Chris@0: Chris@0: $endTokens[T_COLON] = true; Chris@0: $endTokens[T_COMMA] = true; Chris@0: $endTokens[T_DOUBLE_ARROW] = true; Chris@0: $endTokens[T_SEMICOLON] = true; Chris@0: $endTokens[T_OPEN_TAG] = true; Chris@0: $endTokens[T_CLOSE_TAG] = true; Chris@0: $endTokens[T_OPEN_SHORT_ARRAY] = true; Chris@0: Chris@0: if ($ignore !== null) { Chris@0: $ignore = (array) $ignore; Chris@0: foreach ($ignore as $code) { Chris@0: if (isset($endTokens[$code]) === true) { Chris@0: unset($endTokens[$code]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $lastNotEmpty = $start; Chris@0: Chris@0: for ($i = $start; $i >= 0; $i--) { Chris@0: if (isset($endTokens[$this->_tokens[$i]['code']]) === true) { Chris@0: // Found the end of the previous statement. Chris@0: return $lastNotEmpty; Chris@0: } Chris@0: Chris@0: if (isset($this->_tokens[$i]['scope_opener']) === true Chris@0: && $i === $this->_tokens[$i]['scope_closer'] Chris@0: ) { Chris@0: // Found the end of the previous scope block. Chris@0: return $lastNotEmpty; Chris@0: } Chris@0: Chris@0: // Skip nested statements. Chris@0: if (isset($this->_tokens[$i]['bracket_opener']) === true Chris@0: && $i === $this->_tokens[$i]['bracket_closer'] Chris@0: ) { Chris@0: $i = $this->_tokens[$i]['bracket_opener']; Chris@0: } else if (isset($this->_tokens[$i]['parenthesis_opener']) === true Chris@0: && $i === $this->_tokens[$i]['parenthesis_closer'] Chris@0: ) { Chris@0: $i = $this->_tokens[$i]['parenthesis_opener']; Chris@0: } Chris@0: Chris@0: if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$this->_tokens[$i]['code']]) === false) { Chris@0: $lastNotEmpty = $i; Chris@0: } Chris@0: }//end for Chris@0: Chris@0: return 0; Chris@0: Chris@0: }//end findStartOfStatement() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the position of the last non-whitespace token in a statement. Chris@0: * Chris@0: * @param int $start The position to start searching from in the token stack. Chris@0: * @param int|array $ignore Token types that should not be considered stop points. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function findEndOfStatement($start, $ignore=null) Chris@0: { Chris@0: $endTokens = array( Chris@0: T_COLON => true, Chris@0: T_COMMA => true, Chris@0: T_DOUBLE_ARROW => true, Chris@0: T_SEMICOLON => true, Chris@0: T_CLOSE_PARENTHESIS => true, Chris@0: T_CLOSE_SQUARE_BRACKET => true, Chris@0: T_CLOSE_CURLY_BRACKET => true, Chris@0: T_CLOSE_SHORT_ARRAY => true, Chris@0: T_OPEN_TAG => true, Chris@0: T_CLOSE_TAG => true, Chris@0: ); Chris@0: Chris@0: if ($ignore !== null) { Chris@0: $ignore = (array) $ignore; Chris@0: foreach ($ignore as $code) { Chris@0: if (isset($endTokens[$code]) === true) { Chris@0: unset($endTokens[$code]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $lastNotEmpty = $start; Chris@0: Chris@0: for ($i = $start; $i < $this->numTokens; $i++) { Chris@0: if ($i !== $start && isset($endTokens[$this->_tokens[$i]['code']]) === true) { Chris@0: // Found the end of the statement. Chris@0: if ($this->_tokens[$i]['code'] === T_CLOSE_PARENTHESIS Chris@0: || $this->_tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET Chris@0: || $this->_tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET Chris@0: || $this->_tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY Chris@0: || $this->_tokens[$i]['code'] === T_OPEN_TAG Chris@0: || $this->_tokens[$i]['code'] === T_CLOSE_TAG Chris@0: ) { Chris@0: return $lastNotEmpty; Chris@0: } Chris@0: Chris@0: return $i; Chris@0: } Chris@0: Chris@0: // Skip nested statements. Chris@0: if (isset($this->_tokens[$i]['scope_closer']) === true Chris@0: && ($i === $this->_tokens[$i]['scope_opener'] Chris@0: || $i === $this->_tokens[$i]['scope_condition']) Chris@0: ) { Chris@0: $i = $this->_tokens[$i]['scope_closer']; Chris@0: } else if (isset($this->_tokens[$i]['bracket_closer']) === true Chris@0: && $i === $this->_tokens[$i]['bracket_opener'] Chris@0: ) { Chris@0: $i = $this->_tokens[$i]['bracket_closer']; Chris@0: } else if (isset($this->_tokens[$i]['parenthesis_closer']) === true Chris@0: && $i === $this->_tokens[$i]['parenthesis_opener'] Chris@0: ) { Chris@0: $i = $this->_tokens[$i]['parenthesis_closer']; Chris@0: } Chris@0: Chris@0: if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$this->_tokens[$i]['code']]) === false) { Chris@0: $lastNotEmpty = $i; Chris@0: } Chris@0: }//end for Chris@0: Chris@0: return ($this->numTokens - 1); Chris@0: Chris@0: }//end findEndOfStatement() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the position of the first token on a line, matching given type. Chris@0: * Chris@0: * Returns false if no token can be found. Chris@0: * Chris@0: * @param int|array $types The type(s) of tokens to search for. Chris@0: * @param int $start The position to start searching from in the Chris@0: * token stack. The first token matching on Chris@0: * this line before this token will be returned. Chris@0: * @param bool $exclude If true, find the token that is NOT of Chris@0: * the types specified in $types. Chris@0: * @param string $value The value that the token must be equal to. Chris@0: * If value is omitted, tokens with any value will Chris@0: * be returned. Chris@0: * Chris@0: * @return int | bool Chris@0: */ Chris@0: public function findFirstOnLine($types, $start, $exclude=false, $value=null) Chris@0: { Chris@0: if (is_array($types) === false) { Chris@0: $types = array($types); Chris@0: } Chris@0: Chris@0: $foundToken = false; Chris@0: Chris@0: for ($i = $start; $i >= 0; $i--) { Chris@0: if ($this->_tokens[$i]['line'] < $this->_tokens[$start]['line']) { Chris@0: break; Chris@0: } Chris@0: Chris@0: $found = $exclude; Chris@0: foreach ($types as $type) { Chris@0: if ($exclude === false) { Chris@0: if ($this->_tokens[$i]['code'] === $type) { Chris@0: $found = true; Chris@0: break; Chris@0: } Chris@0: } else { Chris@0: if ($this->_tokens[$i]['code'] === $type) { Chris@0: $found = false; Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: if ($found === true) { Chris@0: if ($value === null) { Chris@0: $foundToken = $i; Chris@0: } else if ($this->_tokens[$i]['content'] === $value) { Chris@0: $foundToken = $i; Chris@0: } Chris@0: } Chris@0: }//end for Chris@0: Chris@0: return $foundToken; Chris@0: Chris@0: }//end findFirstOnLine() Chris@0: Chris@0: Chris@0: /** Chris@0: * Determine if the passed token has a condition of one of the passed types. Chris@0: * Chris@0: * @param int $stackPtr The position of the token we are checking. Chris@0: * @param int|array $types The type(s) of tokens to search for. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function hasCondition($stackPtr, $types) Chris@0: { Chris@0: // Check for the existence of the token. Chris@0: if (isset($this->_tokens[$stackPtr]) === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Make sure the token has conditions. Chris@0: if (isset($this->_tokens[$stackPtr]['conditions']) === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $types = (array) $types; Chris@0: $conditions = $this->_tokens[$stackPtr]['conditions']; Chris@0: Chris@0: foreach ($types as $type) { Chris@0: if (in_array($type, $conditions) === true) { Chris@0: // We found a token with the required type. Chris@0: return true; Chris@0: } Chris@0: } Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end hasCondition() Chris@0: Chris@0: Chris@0: /** Chris@0: * Return the position of the condition for the passed token. Chris@0: * Chris@0: * Returns FALSE if the token does not have the condition. Chris@0: * Chris@0: * @param int $stackPtr The position of the token we are checking. Chris@0: * @param int $type The type of token to search for. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function getCondition($stackPtr, $type) Chris@0: { Chris@0: // Check for the existence of the token. Chris@0: if (isset($this->_tokens[$stackPtr]) === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Make sure the token has conditions. Chris@0: if (isset($this->_tokens[$stackPtr]['conditions']) === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $conditions = $this->_tokens[$stackPtr]['conditions']; Chris@0: foreach ($conditions as $token => $condition) { Chris@0: if ($condition === $type) { Chris@0: return $token; Chris@0: } Chris@0: } Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end getCondition() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the name of the class that the specified class extends. Chris@0: * Chris@0: * Returns FALSE on error or if there is no extended class name. Chris@0: * Chris@0: * @param int $stackPtr The stack position of the class. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function findExtendedClassName($stackPtr) Chris@0: { Chris@0: // Check for the existence of the token. Chris@0: if (isset($this->_tokens[$stackPtr]) === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: if ($this->_tokens[$stackPtr]['code'] !== T_CLASS Chris@0: && $this->_tokens[$stackPtr]['code'] !== T_ANON_CLASS Chris@0: ) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: if (isset($this->_tokens[$stackPtr]['scope_closer']) === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $classCloserIndex = $this->_tokens[$stackPtr]['scope_closer']; Chris@0: $extendsIndex = $this->findNext(T_EXTENDS, $stackPtr, $classCloserIndex); Chris@0: if (false === $extendsIndex) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $find = array( Chris@0: T_NS_SEPARATOR, Chris@0: T_STRING, Chris@0: T_WHITESPACE, Chris@0: ); Chris@0: Chris@0: $end = $this->findNext($find, ($extendsIndex + 1), $classCloserIndex, true); Chris@0: $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1)); Chris@0: $name = trim($name); Chris@0: Chris@0: if ($name === '') { Chris@0: return false; Chris@0: } Chris@0: Chris@0: return $name; Chris@0: Chris@0: }//end findExtendedClassName() Chris@0: Chris@0: Chris@0: /** Chris@0: * Returns the name(s) of the interface(s) that the specified class implements. Chris@0: * Chris@0: * Returns FALSE on error or if there are no implemented interface names. Chris@0: * Chris@0: * @param int $stackPtr The stack position of the class. Chris@0: * Chris@0: * @return array|false Chris@0: */ Chris@0: public function findImplementedInterfaceNames($stackPtr) Chris@0: { Chris@0: // Check for the existence of the token. Chris@0: if (isset($this->_tokens[$stackPtr]) === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: if ($this->_tokens[$stackPtr]['code'] !== T_CLASS Chris@0: && $this->_tokens[$stackPtr]['code'] !== T_ANON_CLASS Chris@0: ) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: if (isset($this->_tokens[$stackPtr]['scope_closer']) === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $classOpenerIndex = $this->_tokens[$stackPtr]['scope_opener']; Chris@0: $implementsIndex = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex); Chris@0: if ($implementsIndex === false) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $find = array( Chris@0: T_NS_SEPARATOR, Chris@0: T_STRING, Chris@0: T_WHITESPACE, Chris@0: T_COMMA, Chris@0: ); Chris@0: Chris@0: $end = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true); Chris@0: $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1)); Chris@0: $name = trim($name); Chris@0: Chris@0: if ($name === '') { Chris@0: return false; Chris@0: } else { Chris@0: $names = explode(',', $name); Chris@0: $names = array_map('trim', $names); Chris@0: return $names; Chris@0: } Chris@0: Chris@0: }//end findImplementedInterfaceNames() Chris@0: Chris@0: Chris@0: }//end class