Stream.php (16638B)
1 <?php 2 /* 3 * This file is part of the PHP_TokenStream package. 4 * 5 * (c) Sebastian Bergmann <sebastian@phpunit.de> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11 /** 12 * A stream of PHP tokens. 13 * 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 19 */ 20 class PHP_Token_Stream implements ArrayAccess, Countable, SeekableIterator 21 { 22 /** 23 * @var array 24 */ 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' 54 ); 55 56 /** 57 * @var string 58 */ 59 protected $filename; 60 61 /** 62 * @var array 63 */ 64 protected $tokens = array(); 65 66 /** 67 * @var integer 68 */ 69 protected $position = 0; 70 71 /** 72 * @var array 73 */ 74 protected $linesOfCode = array('loc' => 0, 'cloc' => 0, 'ncloc' => 0); 75 76 /** 77 * @var array 78 */ 79 protected $classes; 80 81 /** 82 * @var array 83 */ 84 protected $functions; 85 86 /** 87 * @var array 88 */ 89 protected $includes; 90 91 /** 92 * @var array 93 */ 94 protected $interfaces; 95 96 /** 97 * @var array 98 */ 99 protected $traits; 100 101 /** 102 * @var array 103 */ 104 protected $lineToFunctionMap = array(); 105 106 /** 107 * Constructor. 108 * 109 * @param string $sourceCode 110 */ 111 public function __construct($sourceCode) 112 { 113 if (is_file($sourceCode)) { 114 $this->filename = $sourceCode; 115 $sourceCode = file_get_contents($sourceCode); 116 } 117 118 $this->scan($sourceCode); 119 } 120 121 /** 122 * Destructor. 123 */ 124 public function __destruct() 125 { 126 $this->tokens = array(); 127 } 128 129 /** 130 * @return string 131 */ 132 public function __toString() 133 { 134 $buffer = ''; 135 136 foreach ($this as $token) { 137 $buffer .= $token; 138 } 139 140 return $buffer; 141 } 142 143 /** 144 * @return string 145 * @since Method available since Release 1.1.0 146 */ 147 public function getFilename() 148 { 149 return $this->filename; 150 } 151 152 /** 153 * Scans the source for sequences of characters and converts them into a 154 * stream of tokens. 155 * 156 * @param string $sourceCode 157 */ 158 protected function scan($sourceCode) 159 { 160 $line = 1; 161 $tokens = token_get_all($sourceCode); 162 $numTokens = count($tokens); 163 164 $lastNonWhitespaceTokenWasDoubleColon = false; 165 166 for ($i = 0; $i < $numTokens; ++$i) { 167 $token = $tokens[$i]; 168 unset($tokens[$i]); 169 170 if (is_array($token)) { 171 $name = substr(token_name($token[0]), 2); 172 $text = $token[1]; 173 174 if ($lastNonWhitespaceTokenWasDoubleColon && $name == 'CLASS') { 175 $name = 'CLASS_NAME_CONSTANT'; 176 } 177 178 $tokenClass = 'PHP_Token_' . $name; 179 } else { 180 $text = $token; 181 $tokenClass = self::$customTokens[$token]; 182 } 183 184 $this->tokens[] = new $tokenClass($text, $line, $this, $i); 185 $lines = substr_count($text, "\n"); 186 $line += $lines; 187 188 if ($tokenClass == 'PHP_Token_HALT_COMPILER') { 189 break; 190 } elseif ($tokenClass == 'PHP_Token_COMMENT' || 191 $tokenClass == 'PHP_Token_DOC_COMMENT') { 192 $this->linesOfCode['cloc'] += $lines + 1; 193 } 194 195 if ($name == 'DOUBLE_COLON') { 196 $lastNonWhitespaceTokenWasDoubleColon = true; 197 } elseif ($name != 'WHITESPACE') { 198 $lastNonWhitespaceTokenWasDoubleColon = false; 199 } 200 } 201 202 $this->linesOfCode['loc'] = substr_count($sourceCode, "\n"); 203 $this->linesOfCode['ncloc'] = $this->linesOfCode['loc'] - 204 $this->linesOfCode['cloc']; 205 } 206 207 /** 208 * @return integer 209 */ 210 public function count() 211 { 212 return count($this->tokens); 213 } 214 215 /** 216 * @return PHP_Token[] 217 */ 218 public function tokens() 219 { 220 return $this->tokens; 221 } 222 223 /** 224 * @return array 225 */ 226 public function getClasses() 227 { 228 if ($this->classes !== null) { 229 return $this->classes; 230 } 231 232 $this->parse(); 233 234 return $this->classes; 235 } 236 237 /** 238 * @return array 239 */ 240 public function getFunctions() 241 { 242 if ($this->functions !== null) { 243 return $this->functions; 244 } 245 246 $this->parse(); 247 248 return $this->functions; 249 } 250 251 /** 252 * @return array 253 */ 254 public function getInterfaces() 255 { 256 if ($this->interfaces !== null) { 257 return $this->interfaces; 258 } 259 260 $this->parse(); 261 262 return $this->interfaces; 263 } 264 265 /** 266 * @return array 267 * @since Method available since Release 1.1.0 268 */ 269 public function getTraits() 270 { 271 if ($this->traits !== null) { 272 return $this->traits; 273 } 274 275 $this->parse(); 276 277 return $this->traits; 278 } 279 280 /** 281 * Gets the names of all files that have been included 282 * using include(), include_once(), require() or require_once(). 283 * 284 * Parameter $categorize set to TRUE causing this function to return a 285 * multi-dimensional array with categories in the keys of the first dimension 286 * and constants and their values in the second dimension. 287 * 288 * Parameter $category allow to filter following specific inclusion type 289 * 290 * @param bool $categorize OPTIONAL 291 * @param string $category OPTIONAL Either 'require_once', 'require', 292 * 'include_once', 'include'. 293 * @return array 294 * @since Method available since Release 1.1.0 295 */ 296 public function getIncludes($categorize = false, $category = null) 297 { 298 if ($this->includes === null) { 299 $this->includes = array( 300 'require_once' => array(), 301 'require' => array(), 302 'include_once' => array(), 303 'include' => array() 304 ); 305 306 foreach ($this->tokens as $token) { 307 switch (get_class($token)) { 308 case 'PHP_Token_REQUIRE_ONCE': 309 case 'PHP_Token_REQUIRE': 310 case 'PHP_Token_INCLUDE_ONCE': 311 case 'PHP_Token_INCLUDE': 312 $this->includes[$token->getType()][] = $token->getName(); 313 break; 314 } 315 } 316 } 317 318 if (isset($this->includes[$category])) { 319 $includes = $this->includes[$category]; 320 } elseif ($categorize === false) { 321 $includes = array_merge( 322 $this->includes['require_once'], 323 $this->includes['require'], 324 $this->includes['include_once'], 325 $this->includes['include'] 326 ); 327 } else { 328 $includes = $this->includes; 329 } 330 331 return $includes; 332 } 333 334 /** 335 * Returns the name of the function or method a line belongs to. 336 * 337 * @return string or null if the line is not in a function or method 338 * @since Method available since Release 1.2.0 339 */ 340 public function getFunctionForLine($line) 341 { 342 $this->parse(); 343 344 if (isset($this->lineToFunctionMap[$line])) { 345 return $this->lineToFunctionMap[$line]; 346 } 347 } 348 349 protected function parse() 350 { 351 $this->interfaces = array(); 352 $this->classes = array(); 353 $this->traits = array(); 354 $this->functions = array(); 355 $class = array(); 356 $classEndLine = array(); 357 $trait = false; 358 $traitEndLine = false; 359 $interface = false; 360 $interfaceEndLine = false; 361 362 foreach ($this->tokens as $token) { 363 switch (get_class($token)) { 364 case 'PHP_Token_HALT_COMPILER': 365 return; 366 367 case 'PHP_Token_INTERFACE': 368 $interface = $token->getName(); 369 $interfaceEndLine = $token->getEndLine(); 370 371 $this->interfaces[$interface] = array( 372 'methods' => array(), 373 'parent' => $token->getParent(), 374 'keywords' => $token->getKeywords(), 375 'docblock' => $token->getDocblock(), 376 'startLine' => $token->getLine(), 377 'endLine' => $interfaceEndLine, 378 'package' => $token->getPackage(), 379 'file' => $this->filename 380 ); 381 break; 382 383 case 'PHP_Token_CLASS': 384 case 'PHP_Token_TRAIT': 385 $tmp = array( 386 'methods' => array(), 387 'parent' => $token->getParent(), 388 'interfaces'=> $token->getInterfaces(), 389 'keywords' => $token->getKeywords(), 390 'docblock' => $token->getDocblock(), 391 'startLine' => $token->getLine(), 392 'endLine' => $token->getEndLine(), 393 'package' => $token->getPackage(), 394 'file' => $this->filename 395 ); 396 397 if ($token instanceof PHP_Token_CLASS) { 398 $class[] = $token->getName(); 399 $classEndLine[] = $token->getEndLine(); 400 401 if ($class[count($class)-1] != 'anonymous class') { 402 $this->classes[$class[count($class)-1]] = $tmp; 403 } 404 } else { 405 $trait = $token->getName(); 406 $traitEndLine = $token->getEndLine(); 407 $this->traits[$trait] = $tmp; 408 } 409 break; 410 411 case 'PHP_Token_FUNCTION': 412 $name = $token->getName(); 413 $tmp = array( 414 'docblock' => $token->getDocblock(), 415 'keywords' => $token->getKeywords(), 416 'visibility'=> $token->getVisibility(), 417 'signature' => $token->getSignature(), 418 'startLine' => $token->getLine(), 419 'endLine' => $token->getEndLine(), 420 'ccn' => $token->getCCN(), 421 'file' => $this->filename 422 ); 423 424 if (empty($class) && 425 $trait === false && 426 $interface === false) { 427 $this->functions[$name] = $tmp; 428 429 $this->addFunctionToMap( 430 $name, 431 $tmp['startLine'], 432 $tmp['endLine'] 433 ); 434 } elseif (!empty($class) && $class[count($class)-1] != 'anonymous class') { 435 $this->classes[$class[count($class)-1]]['methods'][$name] = $tmp; 436 437 $this->addFunctionToMap( 438 $class[count($class)-1] . '::' . $name, 439 $tmp['startLine'], 440 $tmp['endLine'] 441 ); 442 } elseif ($trait !== false) { 443 $this->traits[$trait]['methods'][$name] = $tmp; 444 445 $this->addFunctionToMap( 446 $trait . '::' . $name, 447 $tmp['startLine'], 448 $tmp['endLine'] 449 ); 450 } else { 451 $this->interfaces[$interface]['methods'][$name] = $tmp; 452 } 453 break; 454 455 case 'PHP_Token_CLOSE_CURLY': 456 if (!empty($classEndLine) && 457 $classEndLine[count($classEndLine)-1] == $token->getLine()) { 458 array_pop($classEndLine); 459 array_pop($class); 460 } elseif ($traitEndLine !== false && 461 $traitEndLine == $token->getLine()) { 462 $trait = false; 463 $traitEndLine = false; 464 } elseif ($interfaceEndLine !== false && 465 $interfaceEndLine == $token->getLine()) { 466 $interface = false; 467 $interfaceEndLine = false; 468 } 469 break; 470 } 471 } 472 } 473 474 /** 475 * @return array 476 */ 477 public function getLinesOfCode() 478 { 479 return $this->linesOfCode; 480 } 481 482 /** 483 */ 484 public function rewind() 485 { 486 $this->position = 0; 487 } 488 489 /** 490 * @return boolean 491 */ 492 public function valid() 493 { 494 return isset($this->tokens[$this->position]); 495 } 496 497 /** 498 * @return integer 499 */ 500 public function key() 501 { 502 return $this->position; 503 } 504 505 /** 506 * @return PHP_Token 507 */ 508 public function current() 509 { 510 return $this->tokens[$this->position]; 511 } 512 513 /** 514 */ 515 public function next() 516 { 517 $this->position++; 518 } 519 520 /** 521 * @param integer $offset 522 * @return boolean 523 */ 524 public function offsetExists($offset) 525 { 526 return isset($this->tokens[$offset]); 527 } 528 529 /** 530 * @param integer $offset 531 * @return mixed 532 * @throws OutOfBoundsException 533 */ 534 public function offsetGet($offset) 535 { 536 if (!$this->offsetExists($offset)) { 537 throw new OutOfBoundsException( 538 sprintf( 539 'No token at position "%s"', 540 $offset 541 ) 542 ); 543 } 544 545 return $this->tokens[$offset]; 546 } 547 548 /** 549 * @param integer $offset 550 * @param mixed $value 551 */ 552 public function offsetSet($offset, $value) 553 { 554 $this->tokens[$offset] = $value; 555 } 556 557 /** 558 * @param integer $offset 559 * @throws OutOfBoundsException 560 */ 561 public function offsetUnset($offset) 562 { 563 if (!$this->offsetExists($offset)) { 564 throw new OutOfBoundsException( 565 sprintf( 566 'No token at position "%s"', 567 $offset 568 ) 569 ); 570 } 571 572 unset($this->tokens[$offset]); 573 } 574 575 /** 576 * Seek to an absolute position. 577 * 578 * @param integer $position 579 * @throws OutOfBoundsException 580 */ 581 public function seek($position) 582 { 583 $this->position = $position; 584 585 if (!$this->valid()) { 586 throw new OutOfBoundsException( 587 sprintf( 588 'No token at position "%s"', 589 $this->position 590 ) 591 ); 592 } 593 } 594 595 /** 596 * @param string $name 597 * @param integer $startLine 598 * @param integer $endLine 599 */ 600 private function addFunctionToMap($name, $startLine, $endLine) 601 { 602 for ($line = $startLine; $line <= $endLine; $line++) { 603 $this->lineToFunctionMap[$line] = $name; 604 } 605 } 606 }