Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: /** Chris@0: * A stream of PHP tokens. Chris@0: */ Chris@0: class PHP_Token_Stream implements ArrayAccess, Countable, SeekableIterator Chris@0: { Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@14: protected static $customTokens = [ Chris@0: '(' => 'PHP_Token_OPEN_BRACKET', Chris@0: ')' => 'PHP_Token_CLOSE_BRACKET', Chris@0: '[' => 'PHP_Token_OPEN_SQUARE', Chris@0: ']' => 'PHP_Token_CLOSE_SQUARE', Chris@0: '{' => 'PHP_Token_OPEN_CURLY', Chris@0: '}' => 'PHP_Token_CLOSE_CURLY', Chris@0: ';' => 'PHP_Token_SEMICOLON', Chris@0: '.' => 'PHP_Token_DOT', Chris@0: ',' => 'PHP_Token_COMMA', Chris@0: '=' => 'PHP_Token_EQUAL', Chris@0: '<' => 'PHP_Token_LT', Chris@0: '>' => 'PHP_Token_GT', Chris@0: '+' => 'PHP_Token_PLUS', Chris@0: '-' => 'PHP_Token_MINUS', Chris@0: '*' => 'PHP_Token_MULT', Chris@0: '/' => 'PHP_Token_DIV', Chris@0: '?' => 'PHP_Token_QUESTION_MARK', Chris@0: '!' => 'PHP_Token_EXCLAMATION_MARK', Chris@0: ':' => 'PHP_Token_COLON', Chris@0: '"' => 'PHP_Token_DOUBLE_QUOTES', Chris@0: '@' => 'PHP_Token_AT', Chris@0: '&' => 'PHP_Token_AMPERSAND', Chris@0: '%' => 'PHP_Token_PERCENT', Chris@0: '|' => 'PHP_Token_PIPE', Chris@0: '$' => 'PHP_Token_DOLLAR', Chris@0: '^' => 'PHP_Token_CARET', Chris@0: '~' => 'PHP_Token_TILDE', Chris@0: '`' => 'PHP_Token_BACKTICK' Chris@14: ]; Chris@0: Chris@0: /** Chris@0: * @var string Chris@0: */ Chris@0: protected $filename; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@14: protected $tokens = []; Chris@0: Chris@0: /** Chris@14: * @var int Chris@0: */ Chris@0: protected $position = 0; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@14: protected $linesOfCode = ['loc' => 0, 'cloc' => 0, 'ncloc' => 0]; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: protected $classes; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: protected $functions; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: protected $includes; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: protected $interfaces; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: protected $traits; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@14: protected $lineToFunctionMap = []; Chris@0: Chris@0: /** Chris@0: * Constructor. Chris@0: * Chris@0: * @param string $sourceCode Chris@0: */ Chris@0: public function __construct($sourceCode) Chris@0: { Chris@0: if (is_file($sourceCode)) { Chris@0: $this->filename = $sourceCode; Chris@0: $sourceCode = file_get_contents($sourceCode); Chris@0: } Chris@0: Chris@0: $this->scan($sourceCode); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Destructor. Chris@0: */ Chris@0: public function __destruct() Chris@0: { Chris@14: $this->tokens = []; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return string Chris@0: */ Chris@0: public function __toString() Chris@0: { Chris@0: $buffer = ''; Chris@0: Chris@0: foreach ($this as $token) { Chris@0: $buffer .= $token; Chris@0: } Chris@0: Chris@0: return $buffer; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return string Chris@0: */ Chris@0: public function getFilename() Chris@0: { Chris@0: return $this->filename; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Scans the source for sequences of characters and converts them into a Chris@0: * stream of tokens. Chris@0: * Chris@0: * @param string $sourceCode Chris@0: */ Chris@0: protected function scan($sourceCode) Chris@0: { Chris@0: $id = 0; Chris@0: $line = 1; Chris@0: $tokens = token_get_all($sourceCode); Chris@0: $numTokens = count($tokens); Chris@0: Chris@0: $lastNonWhitespaceTokenWasDoubleColon = false; Chris@0: Chris@0: for ($i = 0; $i < $numTokens; ++$i) { Chris@0: $token = $tokens[$i]; Chris@0: $skip = 0; Chris@0: Chris@0: if (is_array($token)) { Chris@0: $name = substr(token_name($token[0]), 2); Chris@0: $text = $token[1]; Chris@0: Chris@0: if ($lastNonWhitespaceTokenWasDoubleColon && $name == 'CLASS') { Chris@0: $name = 'CLASS_NAME_CONSTANT'; Chris@14: } elseif ($name == 'USE' && isset($tokens[$i + 2][0]) && $tokens[$i + 2][0] == T_FUNCTION) { Chris@0: $name = 'USE_FUNCTION'; Chris@14: $text .= $tokens[$i + 1][1] . $tokens[$i + 2][1]; Chris@0: $skip = 2; Chris@0: } Chris@0: Chris@0: $tokenClass = 'PHP_Token_' . $name; Chris@0: } else { Chris@0: $text = $token; Chris@0: $tokenClass = self::$customTokens[$token]; Chris@0: } Chris@0: Chris@0: $this->tokens[] = new $tokenClass($text, $line, $this, $id++); Chris@0: $lines = substr_count($text, "\n"); Chris@14: $line += $lines; Chris@0: Chris@0: if ($tokenClass == 'PHP_Token_HALT_COMPILER') { Chris@0: break; Chris@0: } elseif ($tokenClass == 'PHP_Token_COMMENT' || Chris@0: $tokenClass == 'PHP_Token_DOC_COMMENT') { Chris@0: $this->linesOfCode['cloc'] += $lines + 1; Chris@0: } Chris@0: Chris@0: if ($name == 'DOUBLE_COLON') { Chris@0: $lastNonWhitespaceTokenWasDoubleColon = true; Chris@0: } elseif ($name != 'WHITESPACE') { Chris@0: $lastNonWhitespaceTokenWasDoubleColon = false; Chris@0: } Chris@0: Chris@0: $i += $skip; Chris@0: } Chris@0: Chris@0: $this->linesOfCode['loc'] = substr_count($sourceCode, "\n"); Chris@0: $this->linesOfCode['ncloc'] = $this->linesOfCode['loc'] - Chris@0: $this->linesOfCode['cloc']; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @return int Chris@0: */ Chris@0: public function count() Chris@0: { Chris@0: return count($this->tokens); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return PHP_Token[] Chris@0: */ Chris@0: public function tokens() Chris@0: { Chris@0: return $this->tokens; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return array Chris@0: */ Chris@0: public function getClasses() Chris@0: { Chris@0: if ($this->classes !== null) { Chris@0: return $this->classes; Chris@0: } Chris@0: Chris@0: $this->parse(); Chris@0: Chris@0: return $this->classes; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return array Chris@0: */ Chris@0: public function getFunctions() Chris@0: { Chris@0: if ($this->functions !== null) { Chris@0: return $this->functions; Chris@0: } Chris@0: Chris@0: $this->parse(); Chris@0: Chris@0: return $this->functions; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return array Chris@0: */ Chris@0: public function getInterfaces() Chris@0: { Chris@0: if ($this->interfaces !== null) { Chris@0: return $this->interfaces; Chris@0: } Chris@0: Chris@0: $this->parse(); Chris@0: Chris@0: return $this->interfaces; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return array Chris@0: */ Chris@0: public function getTraits() Chris@0: { Chris@0: if ($this->traits !== null) { Chris@0: return $this->traits; Chris@0: } Chris@0: Chris@0: $this->parse(); Chris@0: Chris@0: return $this->traits; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the names of all files that have been included Chris@0: * using include(), include_once(), require() or require_once(). Chris@0: * Chris@0: * Parameter $categorize set to TRUE causing this function to return a Chris@0: * multi-dimensional array with categories in the keys of the first dimension Chris@0: * and constants and their values in the second dimension. Chris@0: * Chris@0: * Parameter $category allow to filter following specific inclusion type Chris@0: * Chris@0: * @param bool $categorize OPTIONAL Chris@0: * @param string $category OPTIONAL Either 'require_once', 'require', Chris@14: * 'include_once', 'include'. Chris@14: * Chris@0: * @return array Chris@0: */ Chris@0: public function getIncludes($categorize = false, $category = null) Chris@0: { Chris@0: if ($this->includes === null) { Chris@14: $this->includes = [ Chris@14: 'require_once' => [], Chris@14: 'require' => [], Chris@14: 'include_once' => [], Chris@14: 'include' => [] Chris@14: ]; Chris@0: Chris@0: foreach ($this->tokens as $token) { Chris@0: switch (get_class($token)) { Chris@0: case 'PHP_Token_REQUIRE_ONCE': Chris@0: case 'PHP_Token_REQUIRE': Chris@0: case 'PHP_Token_INCLUDE_ONCE': Chris@0: case 'PHP_Token_INCLUDE': Chris@0: $this->includes[$token->getType()][] = $token->getName(); Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: if (isset($this->includes[$category])) { Chris@0: $includes = $this->includes[$category]; Chris@0: } elseif ($categorize === false) { Chris@0: $includes = array_merge( Chris@0: $this->includes['require_once'], Chris@0: $this->includes['require'], Chris@0: $this->includes['include_once'], Chris@0: $this->includes['include'] Chris@0: ); Chris@0: } else { Chris@0: $includes = $this->includes; Chris@0: } Chris@0: Chris@0: return $includes; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the name of the function or method a line belongs to. Chris@0: * Chris@0: * @return string or null if the line is not in a function or method Chris@0: */ Chris@0: public function getFunctionForLine($line) Chris@0: { Chris@0: $this->parse(); Chris@0: Chris@0: if (isset($this->lineToFunctionMap[$line])) { Chris@0: return $this->lineToFunctionMap[$line]; Chris@0: } Chris@0: } Chris@0: Chris@0: protected function parse() Chris@0: { Chris@14: $this->interfaces = []; Chris@14: $this->classes = []; Chris@14: $this->traits = []; Chris@14: $this->functions = []; Chris@14: $class = []; Chris@14: $classEndLine = []; Chris@0: $trait = false; Chris@0: $traitEndLine = false; Chris@0: $interface = false; Chris@0: $interfaceEndLine = false; Chris@0: Chris@0: foreach ($this->tokens as $token) { Chris@0: switch (get_class($token)) { Chris@0: case 'PHP_Token_HALT_COMPILER': Chris@0: return; Chris@0: Chris@0: case 'PHP_Token_INTERFACE': Chris@0: $interface = $token->getName(); Chris@0: $interfaceEndLine = $token->getEndLine(); Chris@0: Chris@14: $this->interfaces[$interface] = [ Chris@14: 'methods' => [], Chris@0: 'parent' => $token->getParent(), Chris@0: 'keywords' => $token->getKeywords(), Chris@0: 'docblock' => $token->getDocblock(), Chris@0: 'startLine' => $token->getLine(), Chris@0: 'endLine' => $interfaceEndLine, Chris@0: 'package' => $token->getPackage(), Chris@0: 'file' => $this->filename Chris@14: ]; Chris@0: break; Chris@0: Chris@0: case 'PHP_Token_CLASS': Chris@0: case 'PHP_Token_TRAIT': Chris@14: $tmp = [ Chris@14: 'methods' => [], Chris@0: 'parent' => $token->getParent(), Chris@0: 'interfaces'=> $token->getInterfaces(), Chris@0: 'keywords' => $token->getKeywords(), Chris@0: 'docblock' => $token->getDocblock(), Chris@0: 'startLine' => $token->getLine(), Chris@0: 'endLine' => $token->getEndLine(), Chris@0: 'package' => $token->getPackage(), Chris@0: 'file' => $this->filename Chris@14: ]; Chris@0: Chris@0: if ($token instanceof PHP_Token_CLASS) { Chris@0: $class[] = $token->getName(); Chris@0: $classEndLine[] = $token->getEndLine(); Chris@0: Chris@14: $this->classes[$class[count($class) - 1]] = $tmp; Chris@0: } else { Chris@0: $trait = $token->getName(); Chris@0: $traitEndLine = $token->getEndLine(); Chris@0: $this->traits[$trait] = $tmp; Chris@0: } Chris@0: break; Chris@0: Chris@0: case 'PHP_Token_FUNCTION': Chris@0: $name = $token->getName(); Chris@14: $tmp = [ Chris@0: 'docblock' => $token->getDocblock(), Chris@0: 'keywords' => $token->getKeywords(), Chris@0: 'visibility'=> $token->getVisibility(), Chris@0: 'signature' => $token->getSignature(), Chris@0: 'startLine' => $token->getLine(), Chris@0: 'endLine' => $token->getEndLine(), Chris@0: 'ccn' => $token->getCCN(), Chris@0: 'file' => $this->filename Chris@14: ]; Chris@0: Chris@0: if (empty($class) && Chris@0: $trait === false && Chris@0: $interface === false) { Chris@0: $this->functions[$name] = $tmp; Chris@0: Chris@0: $this->addFunctionToMap( Chris@0: $name, Chris@0: $tmp['startLine'], Chris@0: $tmp['endLine'] Chris@0: ); Chris@14: } elseif (!empty($class)) { Chris@14: $this->classes[$class[count($class) - 1]]['methods'][$name] = $tmp; Chris@0: Chris@0: $this->addFunctionToMap( Chris@14: $class[count($class) - 1] . '::' . $name, Chris@0: $tmp['startLine'], Chris@0: $tmp['endLine'] Chris@0: ); Chris@0: } elseif ($trait !== false) { Chris@0: $this->traits[$trait]['methods'][$name] = $tmp; Chris@0: Chris@0: $this->addFunctionToMap( Chris@0: $trait . '::' . $name, Chris@0: $tmp['startLine'], Chris@0: $tmp['endLine'] Chris@0: ); Chris@0: } else { Chris@0: $this->interfaces[$interface]['methods'][$name] = $tmp; Chris@0: } Chris@0: break; Chris@0: Chris@0: case 'PHP_Token_CLOSE_CURLY': Chris@0: if (!empty($classEndLine) && Chris@14: $classEndLine[count($classEndLine) - 1] == $token->getLine()) { Chris@0: array_pop($classEndLine); Chris@0: array_pop($class); Chris@0: } elseif ($traitEndLine !== false && Chris@0: $traitEndLine == $token->getLine()) { Chris@0: $trait = false; Chris@0: $traitEndLine = false; Chris@0: } elseif ($interfaceEndLine !== false && Chris@0: $interfaceEndLine == $token->getLine()) { Chris@0: $interface = false; Chris@0: $interfaceEndLine = false; Chris@0: } Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return array Chris@0: */ Chris@0: public function getLinesOfCode() Chris@0: { Chris@0: return $this->linesOfCode; Chris@0: } Chris@0: Chris@0: /** Chris@0: */ Chris@0: public function rewind() Chris@0: { Chris@0: $this->position = 0; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @return bool Chris@0: */ Chris@0: public function valid() Chris@0: { Chris@0: return isset($this->tokens[$this->position]); Chris@0: } Chris@0: Chris@0: /** Chris@14: * @return int Chris@0: */ Chris@0: public function key() Chris@0: { Chris@0: return $this->position; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return PHP_Token Chris@0: */ Chris@0: public function current() Chris@0: { Chris@0: return $this->tokens[$this->position]; Chris@0: } Chris@0: Chris@0: /** Chris@0: */ Chris@0: public function next() Chris@0: { Chris@0: $this->position++; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param int $offset Chris@14: * Chris@14: * @return bool Chris@0: */ Chris@0: public function offsetExists($offset) Chris@0: { Chris@0: return isset($this->tokens[$offset]); Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param int $offset Chris@14: * Chris@0: * @return mixed Chris@14: * Chris@0: * @throws OutOfBoundsException Chris@0: */ Chris@0: public function offsetGet($offset) Chris@0: { Chris@0: if (!$this->offsetExists($offset)) { Chris@0: throw new OutOfBoundsException( Chris@0: sprintf( Chris@0: 'No token at position "%s"', Chris@0: $offset Chris@0: ) Chris@0: ); Chris@0: } Chris@0: Chris@0: return $this->tokens[$offset]; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param int $offset Chris@14: * @param mixed $value Chris@0: */ Chris@0: public function offsetSet($offset, $value) Chris@0: { Chris@0: $this->tokens[$offset] = $value; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param int $offset Chris@14: * Chris@0: * @throws OutOfBoundsException Chris@0: */ Chris@0: public function offsetUnset($offset) Chris@0: { Chris@0: if (!$this->offsetExists($offset)) { Chris@0: throw new OutOfBoundsException( Chris@0: sprintf( Chris@0: 'No token at position "%s"', Chris@0: $offset Chris@0: ) Chris@0: ); Chris@0: } Chris@0: Chris@0: unset($this->tokens[$offset]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Seek to an absolute position. Chris@0: * Chris@14: * @param int $position Chris@14: * Chris@0: * @throws OutOfBoundsException Chris@0: */ Chris@0: public function seek($position) Chris@0: { Chris@0: $this->position = $position; Chris@0: Chris@0: if (!$this->valid()) { Chris@0: throw new OutOfBoundsException( Chris@0: sprintf( Chris@0: 'No token at position "%s"', Chris@0: $this->position Chris@0: ) Chris@0: ); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param string $name Chris@14: * @param int $startLine Chris@14: * @param int $endLine Chris@0: */ Chris@0: private function addFunctionToMap($name, $startLine, $endLine) Chris@0: { Chris@0: for ($line = $startLine; $line <= $endLine; $line++) { Chris@0: $this->lineToFunctionMap[$line] = $name; Chris@0: } Chris@0: } Chris@0: }