1<?php 2/* 3 * This file is part of php-token-stream. 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 */ 14class PHP_Token_Stream implements ArrayAccess, Countable, SeekableIterator 15{ 16 /** 17 * @var array 18 */ 19 protected static $customTokens = [ 20 '(' => 'PHP_Token_OPEN_BRACKET', 21 ')' => 'PHP_Token_CLOSE_BRACKET', 22 '[' => 'PHP_Token_OPEN_SQUARE', 23 ']' => 'PHP_Token_CLOSE_SQUARE', 24 '{' => 'PHP_Token_OPEN_CURLY', 25 '}' => 'PHP_Token_CLOSE_CURLY', 26 ';' => 'PHP_Token_SEMICOLON', 27 '.' => 'PHP_Token_DOT', 28 ',' => 'PHP_Token_COMMA', 29 '=' => 'PHP_Token_EQUAL', 30 '<' => 'PHP_Token_LT', 31 '>' => 'PHP_Token_GT', 32 '+' => 'PHP_Token_PLUS', 33 '-' => 'PHP_Token_MINUS', 34 '*' => 'PHP_Token_MULT', 35 '/' => 'PHP_Token_DIV', 36 '?' => 'PHP_Token_QUESTION_MARK', 37 '!' => 'PHP_Token_EXCLAMATION_MARK', 38 ':' => 'PHP_Token_COLON', 39 '"' => 'PHP_Token_DOUBLE_QUOTES', 40 '@' => 'PHP_Token_AT', 41 '&' => 'PHP_Token_AMPERSAND', 42 '%' => 'PHP_Token_PERCENT', 43 '|' => 'PHP_Token_PIPE', 44 '$' => 'PHP_Token_DOLLAR', 45 '^' => 'PHP_Token_CARET', 46 '~' => 'PHP_Token_TILDE', 47 '`' => 'PHP_Token_BACKTICK' 48 ]; 49 50 /** 51 * @var string 52 */ 53 protected $filename; 54 55 /** 56 * @var array 57 */ 58 protected $tokens = []; 59 60 /** 61 * @var int 62 */ 63 protected $position = 0; 64 65 /** 66 * @var array 67 */ 68 protected $linesOfCode = ['loc' => 0, 'cloc' => 0, 'ncloc' => 0]; 69 70 /** 71 * @var array 72 */ 73 protected $classes; 74 75 /** 76 * @var array 77 */ 78 protected $functions; 79 80 /** 81 * @var array 82 */ 83 protected $includes; 84 85 /** 86 * @var array 87 */ 88 protected $interfaces; 89 90 /** 91 * @var array 92 */ 93 protected $traits; 94 95 /** 96 * @var array 97 */ 98 protected $lineToFunctionMap = []; 99 100 /** 101 * Constructor. 102 * 103 * @param string $sourceCode 104 */ 105 public function __construct($sourceCode) 106 { 107 if (is_file($sourceCode)) { 108 $this->filename = $sourceCode; 109 $sourceCode = file_get_contents($sourceCode); 110 } 111 112 $this->scan($sourceCode); 113 } 114 115 /** 116 * Destructor. 117 */ 118 public function __destruct() 119 { 120 $this->tokens = []; 121 } 122 123 /** 124 * @return string 125 */ 126 public function __toString() 127 { 128 $buffer = ''; 129 130 foreach ($this as $token) { 131 $buffer .= $token; 132 } 133 134 return $buffer; 135 } 136 137 /** 138 * @return string 139 */ 140 public function getFilename() 141 { 142 return $this->filename; 143 } 144 145 /** 146 * Scans the source for sequences of characters and converts them into a 147 * stream of tokens. 148 * 149 * @param string $sourceCode 150 */ 151 protected function scan($sourceCode) 152 { 153 $id = 0; 154 $line = 1; 155 $tokens = token_get_all($sourceCode); 156 $numTokens = count($tokens); 157 158 $lastNonWhitespaceTokenWasDoubleColon = false; 159 160 for ($i = 0; $i < $numTokens; ++$i) { 161 $token = $tokens[$i]; 162 $skip = 0; 163 164 if (is_array($token)) { 165 $name = substr(token_name($token[0]), 2); 166 $text = $token[1]; 167 168 if ($lastNonWhitespaceTokenWasDoubleColon && $name == 'CLASS') { 169 $name = 'CLASS_NAME_CONSTANT'; 170 } elseif ($name == 'USE' && isset($tokens[$i + 2][0]) && $tokens[$i + 2][0] == T_FUNCTION) { 171 $name = 'USE_FUNCTION'; 172 $text .= $tokens[$i + 1][1] . $tokens[$i + 2][1]; 173 $skip = 2; 174 } 175 176 $tokenClass = 'PHP_Token_' . $name; 177 } else { 178 $text = $token; 179 $tokenClass = self::$customTokens[$token]; 180 } 181 182 $this->tokens[] = new $tokenClass($text, $line, $this, $id++); 183 $lines = substr_count($text, "\n"); 184 $line += $lines; 185 186 if ($tokenClass == 'PHP_Token_HALT_COMPILER') { 187 break; 188 } elseif ($tokenClass == 'PHP_Token_COMMENT' || 189 $tokenClass == 'PHP_Token_DOC_COMMENT') { 190 $this->linesOfCode['cloc'] += $lines + 1; 191 } 192 193 if ($name == 'DOUBLE_COLON') { 194 $lastNonWhitespaceTokenWasDoubleColon = true; 195 } elseif ($name != 'WHITESPACE') { 196 $lastNonWhitespaceTokenWasDoubleColon = false; 197 } 198 199 $i += $skip; 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 int 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 */ 268 public function getTraits() 269 { 270 if ($this->traits !== null) { 271 return $this->traits; 272 } 273 274 $this->parse(); 275 276 return $this->traits; 277 } 278 279 /** 280 * Gets the names of all files that have been included 281 * using include(), include_once(), require() or require_once(). 282 * 283 * Parameter $categorize set to TRUE causing this function to return a 284 * multi-dimensional array with categories in the keys of the first dimension 285 * and constants and their values in the second dimension. 286 * 287 * Parameter $category allow to filter following specific inclusion type 288 * 289 * @param bool $categorize OPTIONAL 290 * @param string $category OPTIONAL Either 'require_once', 'require', 291 * 'include_once', 'include'. 292 * 293 * @return array 294 */ 295 public function getIncludes($categorize = false, $category = null) 296 { 297 if ($this->includes === null) { 298 $this->includes = [ 299 'require_once' => [], 300 'require' => [], 301 'include_once' => [], 302 'include' => [] 303 ]; 304 305 foreach ($this->tokens as $token) { 306 switch (get_class($token)) { 307 case 'PHP_Token_REQUIRE_ONCE': 308 case 'PHP_Token_REQUIRE': 309 case 'PHP_Token_INCLUDE_ONCE': 310 case 'PHP_Token_INCLUDE': 311 $this->includes[$token->getType()][] = $token->getName(); 312 break; 313 } 314 } 315 } 316 317 if (isset($this->includes[$category])) { 318 $includes = $this->includes[$category]; 319 } elseif ($categorize === false) { 320 $includes = array_merge( 321 $this->includes['require_once'], 322 $this->includes['require'], 323 $this->includes['include_once'], 324 $this->includes['include'] 325 ); 326 } else { 327 $includes = $this->includes; 328 } 329 330 return $includes; 331 } 332 333 /** 334 * Returns the name of the function or method a line belongs to. 335 * 336 * @return string or null if the line is not in a function or method 337 */ 338 public function getFunctionForLine($line) 339 { 340 $this->parse(); 341 342 if (isset($this->lineToFunctionMap[$line])) { 343 return $this->lineToFunctionMap[$line]; 344 } 345 } 346 347 protected function parse() 348 { 349 $this->interfaces = []; 350 $this->classes = []; 351 $this->traits = []; 352 $this->functions = []; 353 $class = []; 354 $classEndLine = []; 355 $trait = false; 356 $traitEndLine = false; 357 $interface = false; 358 $interfaceEndLine = false; 359 360 foreach ($this->tokens as $token) { 361 switch (get_class($token)) { 362 case 'PHP_Token_HALT_COMPILER': 363 return; 364 365 case 'PHP_Token_INTERFACE': 366 $interface = $token->getName(); 367 $interfaceEndLine = $token->getEndLine(); 368 369 $this->interfaces[$interface] = [ 370 'methods' => [], 371 'parent' => $token->getParent(), 372 'keywords' => $token->getKeywords(), 373 'docblock' => $token->getDocblock(), 374 'startLine' => $token->getLine(), 375 'endLine' => $interfaceEndLine, 376 'package' => $token->getPackage(), 377 'file' => $this->filename 378 ]; 379 break; 380 381 case 'PHP_Token_CLASS': 382 case 'PHP_Token_TRAIT': 383 $tmp = [ 384 'methods' => [], 385 'parent' => $token->getParent(), 386 'interfaces'=> $token->getInterfaces(), 387 'keywords' => $token->getKeywords(), 388 'docblock' => $token->getDocblock(), 389 'startLine' => $token->getLine(), 390 'endLine' => $token->getEndLine(), 391 'package' => $token->getPackage(), 392 'file' => $this->filename 393 ]; 394 395 if ($token instanceof PHP_Token_CLASS) { 396 $class[] = $token->getName(); 397 $classEndLine[] = $token->getEndLine(); 398 399 $this->classes[$class[count($class) - 1]] = $tmp; 400 } else { 401 $trait = $token->getName(); 402 $traitEndLine = $token->getEndLine(); 403 $this->traits[$trait] = $tmp; 404 } 405 break; 406 407 case 'PHP_Token_FUNCTION': 408 $name = $token->getName(); 409 $tmp = [ 410 'docblock' => $token->getDocblock(), 411 'keywords' => $token->getKeywords(), 412 'visibility'=> $token->getVisibility(), 413 'signature' => $token->getSignature(), 414 'startLine' => $token->getLine(), 415 'endLine' => $token->getEndLine(), 416 'ccn' => $token->getCCN(), 417 'file' => $this->filename 418 ]; 419 420 if (empty($class) && 421 $trait === false && 422 $interface === false) { 423 $this->functions[$name] = $tmp; 424 425 $this->addFunctionToMap( 426 $name, 427 $tmp['startLine'], 428 $tmp['endLine'] 429 ); 430 } elseif (!empty($class)) { 431 $this->classes[$class[count($class) - 1]]['methods'][$name] = $tmp; 432 433 $this->addFunctionToMap( 434 $class[count($class) - 1] . '::' . $name, 435 $tmp['startLine'], 436 $tmp['endLine'] 437 ); 438 } elseif ($trait !== false) { 439 $this->traits[$trait]['methods'][$name] = $tmp; 440 441 $this->addFunctionToMap( 442 $trait . '::' . $name, 443 $tmp['startLine'], 444 $tmp['endLine'] 445 ); 446 } else { 447 $this->interfaces[$interface]['methods'][$name] = $tmp; 448 } 449 break; 450 451 case 'PHP_Token_CLOSE_CURLY': 452 if (!empty($classEndLine) && 453 $classEndLine[count($classEndLine) - 1] == $token->getLine()) { 454 array_pop($classEndLine); 455 array_pop($class); 456 } elseif ($traitEndLine !== false && 457 $traitEndLine == $token->getLine()) { 458 $trait = false; 459 $traitEndLine = false; 460 } elseif ($interfaceEndLine !== false && 461 $interfaceEndLine == $token->getLine()) { 462 $interface = false; 463 $interfaceEndLine = false; 464 } 465 break; 466 } 467 } 468 } 469 470 /** 471 * @return array 472 */ 473 public function getLinesOfCode() 474 { 475 return $this->linesOfCode; 476 } 477 478 /** 479 */ 480 public function rewind() 481 { 482 $this->position = 0; 483 } 484 485 /** 486 * @return bool 487 */ 488 public function valid() 489 { 490 return isset($this->tokens[$this->position]); 491 } 492 493 /** 494 * @return int 495 */ 496 public function key() 497 { 498 return $this->position; 499 } 500 501 /** 502 * @return PHP_Token 503 */ 504 public function current() 505 { 506 return $this->tokens[$this->position]; 507 } 508 509 /** 510 */ 511 public function next() 512 { 513 $this->position++; 514 } 515 516 /** 517 * @param int $offset 518 * 519 * @return bool 520 */ 521 public function offsetExists($offset) 522 { 523 return isset($this->tokens[$offset]); 524 } 525 526 /** 527 * @param int $offset 528 * 529 * @return mixed 530 * 531 * @throws OutOfBoundsException 532 */ 533 public function offsetGet($offset) 534 { 535 if (!$this->offsetExists($offset)) { 536 throw new OutOfBoundsException( 537 sprintf( 538 'No token at position "%s"', 539 $offset 540 ) 541 ); 542 } 543 544 return $this->tokens[$offset]; 545 } 546 547 /** 548 * @param int $offset 549 * @param mixed $value 550 */ 551 public function offsetSet($offset, $value) 552 { 553 $this->tokens[$offset] = $value; 554 } 555 556 /** 557 * @param int $offset 558 * 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 int $position 579 * 580 * @throws OutOfBoundsException 581 */ 582 public function seek($position) 583 { 584 $this->position = $position; 585 586 if (!$this->valid()) { 587 throw new OutOfBoundsException( 588 sprintf( 589 'No token at position "%s"', 590 $this->position 591 ) 592 ); 593 } 594 } 595 596 /** 597 * @param string $name 598 * @param int $startLine 599 * @param int $endLine 600 */ 601 private function addFunctionToMap($name, $startLine, $endLine) 602 { 603 for ($line = $startLine; $line <= $endLine; $line++) { 604 $this->lineToFunctionMap[$line] = $name; 605 } 606 } 607} 608