3 * This file is part of the PHP_TokenStream package.
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
12 * A stream of PHP tokens.
14 * @author Sebastian Bergmann <sebastian@phpunit.de>
15 * @copyright Sebastian Bergmann <sebastian@phpunit.de>
16 * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License
17 * @link http://github.com/sebastianbergmann/php-token-stream/tree
18 * @since Class available since Release 1.0.0
20 class PHP_Token_Stream implements ArrayAccess, Countable, SeekableIterator
25 protected static $customTokens = array(
26 '(' => 'PHP_Token_OPEN_BRACKET',
27 ')' => 'PHP_Token_CLOSE_BRACKET',
28 '[' => 'PHP_Token_OPEN_SQUARE',
29 ']' => 'PHP_Token_CLOSE_SQUARE',
30 '{' => 'PHP_Token_OPEN_CURLY',
31 '}' => 'PHP_Token_CLOSE_CURLY',
32 ';' => 'PHP_Token_SEMICOLON',
33 '.' => 'PHP_Token_DOT',
34 ',' => 'PHP_Token_COMMA',
35 '=' => 'PHP_Token_EQUAL',
36 '<' => 'PHP_Token_LT',
37 '>' => 'PHP_Token_GT',
38 '+' => 'PHP_Token_PLUS',
39 '-' => 'PHP_Token_MINUS',
40 '*' => 'PHP_Token_MULT',
41 '/' => 'PHP_Token_DIV',
42 '?' => 'PHP_Token_QUESTION_MARK',
43 '!' => 'PHP_Token_EXCLAMATION_MARK',
44 ':' => 'PHP_Token_COLON',
45 '"' => 'PHP_Token_DOUBLE_QUOTES',
46 '@' => 'PHP_Token_AT',
47 '&' => 'PHP_Token_AMPERSAND',
48 '%' => 'PHP_Token_PERCENT',
49 '|' => 'PHP_Token_PIPE',
50 '$' => 'PHP_Token_DOLLAR',
51 '^' => 'PHP_Token_CARET',
52 '~' => 'PHP_Token_TILDE',
53 '`' => 'PHP_Token_BACKTICK'
64 protected $tokens = array();
69 protected $position = 0;
74 protected $linesOfCode = array('loc' => 0, 'cloc' => 0, 'ncloc' => 0);
94 protected $interfaces;
104 protected $lineToFunctionMap = array();
109 * @param string $sourceCode
111 public function __construct($sourceCode)
113 if (is_file($sourceCode)) {
114 $this->filename = $sourceCode;
115 $sourceCode = file_get_contents($sourceCode);
118 $this->scan($sourceCode);
124 public function __destruct()
126 $this->tokens = array();
132 public function __toString()
136 foreach ($this as $token) {
145 * @since Method available since Release 1.1.0
147 public function getFilename()
149 return $this->filename;
153 * Scans the source for sequences of characters and converts them into a
156 * @param string $sourceCode
158 protected function scan($sourceCode)
162 $tokens = token_get_all($sourceCode);
163 $numTokens = count($tokens);
165 $lastNonWhitespaceTokenWasDoubleColon = false;
167 for ($i = 0; $i < $numTokens; ++$i) {
168 $token = $tokens[$i];
171 if (is_array($token)) {
172 $name = substr(token_name($token[0]), 2);
175 if ($lastNonWhitespaceTokenWasDoubleColon && $name == 'CLASS') {
176 $name = 'CLASS_NAME_CONSTANT';
177 } elseif ($name == 'USE' && isset($tokens[$i+2][0]) && $tokens[$i+2][0] == T_FUNCTION) {
178 $name = 'USE_FUNCTION';
179 $text .= $tokens[$i+1][1] . $tokens[$i+2][1];
183 $tokenClass = 'PHP_Token_' . $name;
186 $tokenClass = self::$customTokens[$token];
189 $this->tokens[] = new $tokenClass($text, $line, $this, $id++);
190 $lines = substr_count($text, "\n");
193 if ($tokenClass == 'PHP_Token_HALT_COMPILER') {
195 } elseif ($tokenClass == 'PHP_Token_COMMENT' ||
196 $tokenClass == 'PHP_Token_DOC_COMMENT') {
197 $this->linesOfCode['cloc'] += $lines + 1;
200 if ($name == 'DOUBLE_COLON') {
201 $lastNonWhitespaceTokenWasDoubleColon = true;
202 } elseif ($name != 'WHITESPACE') {
203 $lastNonWhitespaceTokenWasDoubleColon = false;
209 $this->linesOfCode['loc'] = substr_count($sourceCode, "\n");
210 $this->linesOfCode['ncloc'] = $this->linesOfCode['loc'] -
211 $this->linesOfCode['cloc'];
217 public function count()
219 return count($this->tokens);
223 * @return PHP_Token[]
225 public function tokens()
227 return $this->tokens;
233 public function getClasses()
235 if ($this->classes !== null) {
236 return $this->classes;
241 return $this->classes;
247 public function getFunctions()
249 if ($this->functions !== null) {
250 return $this->functions;
255 return $this->functions;
261 public function getInterfaces()
263 if ($this->interfaces !== null) {
264 return $this->interfaces;
269 return $this->interfaces;
274 * @since Method available since Release 1.1.0
276 public function getTraits()
278 if ($this->traits !== null) {
279 return $this->traits;
284 return $this->traits;
288 * Gets the names of all files that have been included
289 * using include(), include_once(), require() or require_once().
291 * Parameter $categorize set to TRUE causing this function to return a
292 * multi-dimensional array with categories in the keys of the first dimension
293 * and constants and their values in the second dimension.
295 * Parameter $category allow to filter following specific inclusion type
297 * @param bool $categorize OPTIONAL
298 * @param string $category OPTIONAL Either 'require_once', 'require',
299 * 'include_once', 'include'.
301 * @since Method available since Release 1.1.0
303 public function getIncludes($categorize = false, $category = null)
305 if ($this->includes === null) {
306 $this->includes = array(
307 'require_once' => array(),
308 'require' => array(),
309 'include_once' => array(),
313 foreach ($this->tokens as $token) {
314 switch (get_class($token)) {
315 case 'PHP_Token_REQUIRE_ONCE':
316 case 'PHP_Token_REQUIRE':
317 case 'PHP_Token_INCLUDE_ONCE':
318 case 'PHP_Token_INCLUDE':
319 $this->includes[$token->getType()][] = $token->getName();
325 if (isset($this->includes[$category])) {
326 $includes = $this->includes[$category];
327 } elseif ($categorize === false) {
328 $includes = array_merge(
329 $this->includes['require_once'],
330 $this->includes['require'],
331 $this->includes['include_once'],
332 $this->includes['include']
335 $includes = $this->includes;
342 * Returns the name of the function or method a line belongs to.
344 * @return string or null if the line is not in a function or method
345 * @since Method available since Release 1.2.0
347 public function getFunctionForLine($line)
351 if (isset($this->lineToFunctionMap[$line])) {
352 return $this->lineToFunctionMap[$line];
356 protected function parse()
358 $this->interfaces = array();
359 $this->classes = array();
360 $this->traits = array();
361 $this->functions = array();
363 $classEndLine = array();
365 $traitEndLine = false;
367 $interfaceEndLine = false;
369 foreach ($this->tokens as $token) {
370 switch (get_class($token)) {
371 case 'PHP_Token_HALT_COMPILER':
374 case 'PHP_Token_INTERFACE':
375 $interface = $token->getName();
376 $interfaceEndLine = $token->getEndLine();
378 $this->interfaces[$interface] = array(
379 'methods' => array(),
380 'parent' => $token->getParent(),
381 'keywords' => $token->getKeywords(),
382 'docblock' => $token->getDocblock(),
383 'startLine' => $token->getLine(),
384 'endLine' => $interfaceEndLine,
385 'package' => $token->getPackage(),
386 'file' => $this->filename
390 case 'PHP_Token_CLASS':
391 case 'PHP_Token_TRAIT':
393 'methods' => array(),
394 'parent' => $token->getParent(),
395 'interfaces'=> $token->getInterfaces(),
396 'keywords' => $token->getKeywords(),
397 'docblock' => $token->getDocblock(),
398 'startLine' => $token->getLine(),
399 'endLine' => $token->getEndLine(),
400 'package' => $token->getPackage(),
401 'file' => $this->filename
404 if ($token instanceof PHP_Token_CLASS) {
405 $class[] = $token->getName();
406 $classEndLine[] = $token->getEndLine();
408 if ($class[count($class)-1] != 'anonymous class') {
409 $this->classes[$class[count($class)-1]] = $tmp;
412 $trait = $token->getName();
413 $traitEndLine = $token->getEndLine();
414 $this->traits[$trait] = $tmp;
418 case 'PHP_Token_FUNCTION':
419 $name = $token->getName();
421 'docblock' => $token->getDocblock(),
422 'keywords' => $token->getKeywords(),
423 'visibility'=> $token->getVisibility(),
424 'signature' => $token->getSignature(),
425 'startLine' => $token->getLine(),
426 'endLine' => $token->getEndLine(),
427 'ccn' => $token->getCCN(),
428 'file' => $this->filename
433 $interface === false) {
434 $this->functions[$name] = $tmp;
436 $this->addFunctionToMap(
441 } elseif (!empty($class) && $class[count($class)-1] != 'anonymous class') {
442 $this->classes[$class[count($class)-1]]['methods'][$name] = $tmp;
444 $this->addFunctionToMap(
445 $class[count($class)-1] . '::' . $name,
449 } elseif ($trait !== false) {
450 $this->traits[$trait]['methods'][$name] = $tmp;
452 $this->addFunctionToMap(
453 $trait . '::' . $name,
458 $this->interfaces[$interface]['methods'][$name] = $tmp;
462 case 'PHP_Token_CLOSE_CURLY':
463 if (!empty($classEndLine) &&
464 $classEndLine[count($classEndLine)-1] == $token->getLine()) {
465 array_pop($classEndLine);
467 } elseif ($traitEndLine !== false &&
468 $traitEndLine == $token->getLine()) {
470 $traitEndLine = false;
471 } elseif ($interfaceEndLine !== false &&
472 $interfaceEndLine == $token->getLine()) {
474 $interfaceEndLine = false;
484 public function getLinesOfCode()
486 return $this->linesOfCode;
491 public function rewind()
499 public function valid()
501 return isset($this->tokens[$this->position]);
507 public function key()
509 return $this->position;
515 public function current()
517 return $this->tokens[$this->position];
522 public function next()
528 * @param integer $offset
531 public function offsetExists($offset)
533 return isset($this->tokens[$offset]);
537 * @param integer $offset
539 * @throws OutOfBoundsException
541 public function offsetGet($offset)
543 if (!$this->offsetExists($offset)) {
544 throw new OutOfBoundsException(
546 'No token at position "%s"',
552 return $this->tokens[$offset];
556 * @param integer $offset
557 * @param mixed $value
559 public function offsetSet($offset, $value)
561 $this->tokens[$offset] = $value;
565 * @param integer $offset
566 * @throws OutOfBoundsException
568 public function offsetUnset($offset)
570 if (!$this->offsetExists($offset)) {
571 throw new OutOfBoundsException(
573 'No token at position "%s"',
579 unset($this->tokens[$offset]);
583 * Seek to an absolute position.
585 * @param integer $position
586 * @throws OutOfBoundsException
588 public function seek($position)
590 $this->position = $position;
592 if (!$this->valid()) {
593 throw new OutOfBoundsException(
595 'No token at position "%s"',
603 * @param string $name
604 * @param integer $startLine
605 * @param integer $endLine
607 private function addFunctionToMap($name, $startLine, $endLine)
609 for ($line = $startLine; $line <= $endLine; $line++) {
610 $this->lineToFunctionMap[$line] = $name;