Stream.php (17525B)
1 <?php 2 /** 3 * php-token-stream 4 * 5 * Copyright (c) 2009-2013, Sebastian Bergmann <sebastian@phpunit.de>. 6 * All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * * Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 15 * * Redistributions in binary form must reproduce the above copyright 16 * notice, this list of conditions and the following disclaimer in 17 * the documentation and/or other materials provided with the 18 * distribution. 19 * 20 * * Neither the name of Sebastian Bergmann nor the names of his 21 * contributors may be used to endorse or promote products derived 22 * from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 27 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 28 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 29 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 30 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 34 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 * POSSIBILITY OF SUCH DAMAGE. 36 * 37 * @package PHP_TokenStream 38 * @author Sebastian Bergmann <sebastian@phpunit.de> 39 * @copyright 2009-2013 Sebastian Bergmann <sebastian@phpunit.de> 40 * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License 41 * @since File available since Release 1.0.0 42 */ 43 44 /** 45 * A stream of PHP tokens. 46 * 47 * @author Sebastian Bergmann <sebastian@phpunit.de> 48 * @copyright 2009-2013 Sebastian Bergmann <sebastian@phpunit.de> 49 * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License 50 * @version Release: @package_version@ 51 * @link http://github.com/sebastianbergmann/php-token-stream/tree 52 * @since Class available since Release 1.0.0 53 */ 54 class PHP_Token_Stream implements ArrayAccess, Countable, SeekableIterator 55 { 56 /** 57 * @var array 58 */ 59 protected static $customTokens = array( 60 '(' => 'PHP_Token_OPEN_BRACKET', 61 ')' => 'PHP_Token_CLOSE_BRACKET', 62 '[' => 'PHP_Token_OPEN_SQUARE', 63 ']' => 'PHP_Token_CLOSE_SQUARE', 64 '{' => 'PHP_Token_OPEN_CURLY', 65 '}' => 'PHP_Token_CLOSE_CURLY', 66 ';' => 'PHP_Token_SEMICOLON', 67 '.' => 'PHP_Token_DOT', 68 ',' => 'PHP_Token_COMMA', 69 '=' => 'PHP_Token_EQUAL', 70 '<' => 'PHP_Token_LT', 71 '>' => 'PHP_Token_GT', 72 '+' => 'PHP_Token_PLUS', 73 '-' => 'PHP_Token_MINUS', 74 '*' => 'PHP_Token_MULT', 75 '/' => 'PHP_Token_DIV', 76 '?' => 'PHP_Token_QUESTION_MARK', 77 '!' => 'PHP_Token_EXCLAMATION_MARK', 78 ':' => 'PHP_Token_COLON', 79 '"' => 'PHP_Token_DOUBLE_QUOTES', 80 '@' => 'PHP_Token_AT', 81 '&' => 'PHP_Token_AMPERSAND', 82 '%' => 'PHP_Token_PERCENT', 83 '|' => 'PHP_Token_PIPE', 84 '$' => 'PHP_Token_DOLLAR', 85 '^' => 'PHP_Token_CARET', 86 '~' => 'PHP_Token_TILDE', 87 '`' => 'PHP_Token_BACKTICK' 88 ); 89 90 /** 91 * @var string 92 */ 93 protected $filename; 94 95 /** 96 * @var array 97 */ 98 protected $tokens = array(); 99 100 /** 101 * @var integer 102 */ 103 protected $position = 0; 104 105 /** 106 * @var array 107 */ 108 protected $linesOfCode = array('loc' => 0, 'cloc' => 0, 'ncloc' => 0); 109 110 /** 111 * @var array 112 */ 113 protected $classes; 114 115 /** 116 * @var array 117 */ 118 protected $functions; 119 120 /** 121 * @var array 122 */ 123 protected $includes; 124 125 /** 126 * @var array 127 */ 128 protected $interfaces; 129 130 /** 131 * @var array 132 */ 133 protected $traits; 134 135 /** 136 * @var array 137 */ 138 protected $lineToFunctionMap = array(); 139 140 /** 141 * Constructor. 142 * 143 * @param string $sourceCode 144 */ 145 public function __construct($sourceCode) 146 { 147 if (is_file($sourceCode)) { 148 $this->filename = $sourceCode; 149 $sourceCode = file_get_contents($sourceCode); 150 } 151 152 $this->scan($sourceCode); 153 } 154 155 /** 156 * Destructor. 157 */ 158 public function __destruct() 159 { 160 $this->tokens = array(); 161 } 162 163 /** 164 * @return string 165 */ 166 public function __toString() 167 { 168 $buffer = ''; 169 170 foreach ($this as $token) { 171 $buffer .= $token; 172 } 173 174 return $buffer; 175 } 176 177 /** 178 * @return string 179 * @since Method available since Release 1.1.0 180 */ 181 public function getFilename() 182 { 183 return $this->filename; 184 } 185 186 /** 187 * Scans the source for sequences of characters and converts them into a 188 * stream of tokens. 189 * 190 * @param string $sourceCode 191 */ 192 protected function scan($sourceCode) 193 { 194 $line = 1; 195 $tokens = token_get_all($sourceCode); 196 $numTokens = count($tokens); 197 198 $lastNonWhitespaceTokenWasDoubleColon = FALSE; 199 200 for ($i = 0; $i < $numTokens; ++$i) { 201 $token = $tokens[$i]; 202 unset($tokens[$i]); 203 204 if (is_array($token)) { 205 $name = substr(token_name($token[0]), 2); 206 $text = $token[1]; 207 208 if ($lastNonWhitespaceTokenWasDoubleColon && $name == 'CLASS') { 209 $name = 'CLASS_NAME_CONSTANT'; 210 } 211 212 $tokenClass = 'PHP_Token_' . $name; 213 } else { 214 $text = $token; 215 $tokenClass = self::$customTokens[$token]; 216 } 217 218 $this->tokens[] = new $tokenClass($text, $line, $this, $i); 219 $lines = substr_count($text, "\n"); 220 $line += $lines; 221 222 if ($tokenClass == 'PHP_Token_HALT_COMPILER') { 223 break; 224 } 225 226 else if ($tokenClass == 'PHP_Token_COMMENT' || 227 $tokenClass == 'PHP_Token_DOC_COMMENT') { 228 $this->linesOfCode['cloc'] += $lines + 1; 229 } 230 231 if ($name == 'DOUBLE_COLON') { 232 $lastNonWhitespaceTokenWasDoubleColon = TRUE; 233 } 234 235 else if ($name != 'WHITESPACE') { 236 $lastNonWhitespaceTokenWasDoubleColon = FALSE; 237 } 238 } 239 240 $this->linesOfCode['loc'] = substr_count($sourceCode, "\n"); 241 $this->linesOfCode['ncloc'] = $this->linesOfCode['loc'] - 242 $this->linesOfCode['cloc']; 243 } 244 245 /** 246 * @return integer 247 */ 248 public function count() 249 { 250 return count($this->tokens); 251 } 252 253 /** 254 * @return PHP_Token[] 255 */ 256 public function tokens() 257 { 258 return $this->tokens; 259 } 260 261 /** 262 * @return array 263 */ 264 public function getClasses() 265 { 266 if ($this->classes !== NULL) { 267 return $this->classes; 268 } 269 270 $this->parse(); 271 272 return $this->classes; 273 } 274 275 /** 276 * @return array 277 */ 278 public function getFunctions() 279 { 280 if ($this->functions !== NULL) { 281 return $this->functions; 282 } 283 284 $this->parse(); 285 286 return $this->functions; 287 } 288 289 /** 290 * @return array 291 */ 292 public function getInterfaces() 293 { 294 if ($this->interfaces !== NULL) { 295 return $this->interfaces; 296 } 297 298 $this->parse(); 299 300 return $this->interfaces; 301 } 302 303 /** 304 * @return array 305 * @since Method available since Release 1.1.0 306 */ 307 public function getTraits() 308 { 309 if ($this->traits !== NULL) { 310 return $this->traits; 311 } 312 313 $this->parse(); 314 315 return $this->traits; 316 } 317 318 /** 319 * Gets the names of all files that have been included 320 * using include(), include_once(), require() or require_once(). 321 * 322 * Parameter $categorize set to TRUE causing this function to return a 323 * multi-dimensional array with categories in the keys of the first dimension 324 * and constants and their values in the second dimension. 325 * 326 * Parameter $category allow to filter following specific inclusion type 327 * 328 * @param bool $categorize OPTIONAL 329 * @param string $category OPTIONAL Either 'require_once', 'require', 330 * 'include_once', 'include'. 331 * @return array 332 * @since Method available since Release 1.1.0 333 */ 334 public function getIncludes($categorize = FALSE, $category = NULL) 335 { 336 if ($this->includes === NULL) { 337 $this->includes = array( 338 'require_once' => array(), 339 'require' => array(), 340 'include_once' => array(), 341 'include' => array() 342 ); 343 344 foreach ($this->tokens as $token) { 345 switch (get_class($token)) { 346 case 'PHP_Token_REQUIRE_ONCE': 347 case 'PHP_Token_REQUIRE': 348 case 'PHP_Token_INCLUDE_ONCE': 349 case 'PHP_Token_INCLUDE': { 350 $this->includes[$token->getType()][] = $token->getName(); 351 } 352 break; 353 } 354 } 355 } 356 357 if (isset($this->includes[$category])) { 358 $includes = $this->includes[$category]; 359 } 360 361 else if ($categorize === FALSE) { 362 $includes = array_merge( 363 $this->includes['require_once'], 364 $this->includes['require'], 365 $this->includes['include_once'], 366 $this->includes['include'] 367 ); 368 } else { 369 $includes = $this->includes; 370 } 371 372 return $includes; 373 } 374 375 /** 376 * Returns the name of the function or method a line belongs to. 377 * 378 * @return string or null if the line is not in a function or method 379 * @since Method available since Release 1.2.0 380 */ 381 public function getFunctionForLine($line) 382 { 383 $this->parse(); 384 385 if (isset($this->lineToFunctionMap[$line])) { 386 return $this->lineToFunctionMap[$line]; 387 } 388 } 389 390 protected function parse() 391 { 392 $this->interfaces = array(); 393 $this->classes = array(); 394 $this->traits = array(); 395 $this->functions = array(); 396 $class = FALSE; 397 $classEndLine = FALSE; 398 $trait = FALSE; 399 $traitEndLine = FALSE; 400 $interface = FALSE; 401 $interfaceEndLine = FALSE; 402 403 foreach ($this->tokens as $token) { 404 switch (get_class($token)) { 405 case 'PHP_Token_HALT_COMPILER': { 406 return; 407 } 408 break; 409 410 case 'PHP_Token_INTERFACE': { 411 $interface = $token->getName(); 412 $interfaceEndLine = $token->getEndLine(); 413 414 $this->interfaces[$interface] = array( 415 'methods' => array(), 416 'parent' => $token->getParent(), 417 'keywords' => $token->getKeywords(), 418 'docblock' => $token->getDocblock(), 419 'startLine' => $token->getLine(), 420 'endLine' => $interfaceEndLine, 421 'package' => $token->getPackage(), 422 'file' => $this->filename 423 ); 424 } 425 break; 426 427 case 'PHP_Token_CLASS': 428 case 'PHP_Token_TRAIT': { 429 $tmp = array( 430 'methods' => array(), 431 'parent' => $token->getParent(), 432 'interfaces'=> $token->getInterfaces(), 433 'keywords' => $token->getKeywords(), 434 'docblock' => $token->getDocblock(), 435 'startLine' => $token->getLine(), 436 'endLine' => $token->getEndLine(), 437 'package' => $token->getPackage(), 438 'file' => $this->filename 439 ); 440 441 if ($token instanceof PHP_Token_CLASS) { 442 $class = $token->getName(); 443 $classEndLine = $token->getEndLine(); 444 $this->classes[$class] = $tmp; 445 } else { 446 $trait = $token->getName(); 447 $traitEndLine = $token->getEndLine(); 448 $this->traits[$trait] = $tmp; 449 } 450 } 451 break; 452 453 case 'PHP_Token_FUNCTION': { 454 $name = $token->getName(); 455 $tmp = array( 456 'docblock' => $token->getDocblock(), 457 'keywords' => $token->getKeywords(), 458 'visibility'=> $token->getVisibility(), 459 'signature' => $token->getSignature(), 460 'startLine' => $token->getLine(), 461 'endLine' => $token->getEndLine(), 462 'ccn' => $token->getCCN(), 463 'file' => $this->filename 464 ); 465 466 if ($class === FALSE && 467 $trait === FALSE && 468 $interface === FALSE) { 469 $this->functions[$name] = $tmp; 470 471 $this->addFunctionToMap( 472 $name, $tmp['startLine'], $tmp['endLine'] 473 ); 474 } 475 476 else if ($class !== FALSE) { 477 $this->classes[$class]['methods'][$name] = $tmp; 478 479 $this->addFunctionToMap( 480 $class . '::' . $name, 481 $tmp['startLine'], 482 $tmp['endLine'] 483 ); 484 } 485 486 else if ($trait !== FALSE) { 487 $this->traits[$trait]['methods'][$name] = $tmp; 488 489 $this->addFunctionToMap( 490 $trait . '::' . $name, 491 $tmp['startLine'], 492 $tmp['endLine'] 493 ); 494 } 495 496 else { 497 $this->interfaces[$interface]['methods'][$name] = $tmp; 498 } 499 } 500 break; 501 502 case 'PHP_Token_CLOSE_CURLY': { 503 if ($classEndLine !== FALSE && 504 $classEndLine == $token->getLine()) { 505 $class = FALSE; 506 $classEndLine = FALSE; 507 } 508 509 else if ($traitEndLine !== FALSE && 510 $traitEndLine == $token->getLine()) { 511 $trait = FALSE; 512 $traitEndLine = FALSE; 513 } 514 515 else if ($interfaceEndLine !== FALSE && 516 $interfaceEndLine == $token->getLine()) { 517 $interface = FALSE; 518 $interfaceEndLine = FALSE; 519 } 520 } 521 break; 522 } 523 } 524 } 525 526 /** 527 * @return array 528 */ 529 public function getLinesOfCode() 530 { 531 return $this->linesOfCode; 532 } 533 534 /** 535 */ 536 public function rewind() 537 { 538 $this->position = 0; 539 } 540 541 /** 542 * @return boolean 543 */ 544 public function valid() 545 { 546 return isset($this->tokens[$this->position]); 547 } 548 549 /** 550 * @return integer 551 */ 552 public function key() 553 { 554 return $this->position; 555 } 556 557 /** 558 * @return PHP_Token 559 */ 560 public function current() 561 { 562 return $this->tokens[$this->position]; 563 } 564 565 /** 566 */ 567 public function next() 568 { 569 $this->position++; 570 } 571 572 /** 573 * @param mixed $offset 574 */ 575 public function offsetExists($offset) 576 { 577 return isset($this->tokens[$offset]); 578 } 579 580 /** 581 * @param mixed $offset 582 * @return mixed 583 */ 584 public function offsetGet($offset) 585 { 586 return $this->tokens[$offset]; 587 } 588 589 /** 590 * @param mixed $offset 591 * @param mixed $value 592 */ 593 public function offsetSet($offset, $value) 594 { 595 $this->tokens[$offset] = $value; 596 } 597 598 /** 599 * @param mixed $offset 600 */ 601 public function offsetUnset($offset) 602 { 603 unset($this->tokens[$offset]); 604 } 605 606 /** 607 * Seek to an absolute position. 608 * 609 * @param integer $position 610 * @throws OutOfBoundsException 611 */ 612 public function seek($position) 613 { 614 $this->position = $position; 615 616 if (!$this->valid()) { 617 throw new OutOfBoundsException('Invalid seek position'); 618 } 619 } 620 621 private function addFunctionToMap($name, $startLine, $endLine) 622 { 623 for ($line = $startLine; $line <= $endLine; $line++) { 624 $this->lineToFunctionMap[$line] = $name; 625 } 626 } 627 }