1<?php declare(strict_types=1); 2 3namespace PhpParser; 4 5/* 6 * This parser is based on a skeleton written by Moriyoshi Koizumi, which in 7 * turn is based on work by Masato Bito. 8 */ 9use PhpParser\Node\Expr; 10use PhpParser\Node\Expr\Cast\Double; 11use PhpParser\Node\Name; 12use PhpParser\Node\Param; 13use PhpParser\Node\Scalar\Encapsed; 14use PhpParser\Node\Scalar\LNumber; 15use PhpParser\Node\Scalar\String_; 16use PhpParser\Node\Stmt\Class_; 17use PhpParser\Node\Stmt\ClassConst; 18use PhpParser\Node\Stmt\ClassMethod; 19use PhpParser\Node\Stmt\Enum_; 20use PhpParser\Node\Stmt\Interface_; 21use PhpParser\Node\Stmt\Namespace_; 22use PhpParser\Node\Stmt\Property; 23use PhpParser\Node\Stmt\TryCatch; 24use PhpParser\Node\Stmt\UseUse; 25use PhpParser\Node\VarLikeIdentifier; 26 27abstract class ParserAbstract implements Parser 28{ 29 const SYMBOL_NONE = -1; 30 31 /* 32 * The following members will be filled with generated parsing data: 33 */ 34 35 /** @var int Size of $tokenToSymbol map */ 36 protected $tokenToSymbolMapSize; 37 /** @var int Size of $action table */ 38 protected $actionTableSize; 39 /** @var int Size of $goto table */ 40 protected $gotoTableSize; 41 42 /** @var int Symbol number signifying an invalid token */ 43 protected $invalidSymbol; 44 /** @var int Symbol number of error recovery token */ 45 protected $errorSymbol; 46 /** @var int Action number signifying default action */ 47 protected $defaultAction; 48 /** @var int Rule number signifying that an unexpected token was encountered */ 49 protected $unexpectedTokenRule; 50 51 protected $YY2TBLSTATE; 52 /** @var int Number of non-leaf states */ 53 protected $numNonLeafStates; 54 55 /** @var int[] Map of lexer tokens to internal symbols */ 56 protected $tokenToSymbol; 57 /** @var string[] Map of symbols to their names */ 58 protected $symbolToName; 59 /** @var array Names of the production rules (only necessary for debugging) */ 60 protected $productions; 61 62 /** @var int[] Map of states to a displacement into the $action table. The corresponding action for this 63 * state/symbol pair is $action[$actionBase[$state] + $symbol]. If $actionBase[$state] is 0, the 64 * action is defaulted, i.e. $actionDefault[$state] should be used instead. */ 65 protected $actionBase; 66 /** @var int[] Table of actions. Indexed according to $actionBase comment. */ 67 protected $action; 68 /** @var int[] Table indexed analogously to $action. If $actionCheck[$actionBase[$state] + $symbol] != $symbol 69 * then the action is defaulted, i.e. $actionDefault[$state] should be used instead. */ 70 protected $actionCheck; 71 /** @var int[] Map of states to their default action */ 72 protected $actionDefault; 73 /** @var callable[] Semantic action callbacks */ 74 protected $reduceCallbacks; 75 76 /** @var int[] Map of non-terminals to a displacement into the $goto table. The corresponding goto state for this 77 * non-terminal/state pair is $goto[$gotoBase[$nonTerminal] + $state] (unless defaulted) */ 78 protected $gotoBase; 79 /** @var int[] Table of states to goto after reduction. Indexed according to $gotoBase comment. */ 80 protected $goto; 81 /** @var int[] Table indexed analogously to $goto. If $gotoCheck[$gotoBase[$nonTerminal] + $state] != $nonTerminal 82 * then the goto state is defaulted, i.e. $gotoDefault[$nonTerminal] should be used. */ 83 protected $gotoCheck; 84 /** @var int[] Map of non-terminals to the default state to goto after their reduction */ 85 protected $gotoDefault; 86 87 /** @var int[] Map of rules to the non-terminal on their left-hand side, i.e. the non-terminal to use for 88 * determining the state to goto after reduction. */ 89 protected $ruleToNonTerminal; 90 /** @var int[] Map of rules to the length of their right-hand side, which is the number of elements that have to 91 * be popped from the stack(s) on reduction. */ 92 protected $ruleToLength; 93 94 /* 95 * The following members are part of the parser state: 96 */ 97 98 /** @var Lexer Lexer that is used when parsing */ 99 protected $lexer; 100 /** @var mixed Temporary value containing the result of last semantic action (reduction) */ 101 protected $semValue; 102 /** @var array Semantic value stack (contains values of tokens and semantic action results) */ 103 protected $semStack; 104 /** @var array[] Start attribute stack */ 105 protected $startAttributeStack; 106 /** @var array[] End attribute stack */ 107 protected $endAttributeStack; 108 /** @var array End attributes of last *shifted* token */ 109 protected $endAttributes; 110 /** @var array Start attributes of last *read* token */ 111 protected $lookaheadStartAttributes; 112 113 /** @var ErrorHandler Error handler */ 114 protected $errorHandler; 115 /** @var int Error state, used to avoid error floods */ 116 protected $errorState; 117 118 /** 119 * Initialize $reduceCallbacks map. 120 */ 121 abstract protected function initReduceCallbacks(); 122 123 /** 124 * Creates a parser instance. 125 * 126 * Options: Currently none. 127 * 128 * @param Lexer $lexer A lexer 129 * @param array $options Options array. 130 */ 131 public function __construct(Lexer $lexer, array $options = []) { 132 $this->lexer = $lexer; 133 134 if (isset($options['throwOnError'])) { 135 throw new \LogicException( 136 '"throwOnError" is no longer supported, use "errorHandler" instead'); 137 } 138 139 $this->initReduceCallbacks(); 140 } 141 142 /** 143 * Parses PHP code into a node tree. 144 * 145 * If a non-throwing error handler is used, the parser will continue parsing after an error 146 * occurred and attempt to build a partial AST. 147 * 148 * @param string $code The source code to parse 149 * @param ErrorHandler|null $errorHandler Error handler to use for lexer/parser errors, defaults 150 * to ErrorHandler\Throwing. 151 * 152 * @return Node\Stmt[]|null Array of statements (or null non-throwing error handler is used and 153 * the parser was unable to recover from an error). 154 */ 155 public function parse(string $code, ErrorHandler $errorHandler = null) { 156 $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing; 157 158 $this->lexer->startLexing($code, $this->errorHandler); 159 $result = $this->doParse(); 160 161 // Clear out some of the interior state, so we don't hold onto unnecessary 162 // memory between uses of the parser 163 $this->startAttributeStack = []; 164 $this->endAttributeStack = []; 165 $this->semStack = []; 166 $this->semValue = null; 167 168 return $result; 169 } 170 171 protected function doParse() { 172 // We start off with no lookahead-token 173 $symbol = self::SYMBOL_NONE; 174 175 // The attributes for a node are taken from the first and last token of the node. 176 // From the first token only the startAttributes are taken and from the last only 177 // the endAttributes. Both are merged using the array union operator (+). 178 $startAttributes = []; 179 $endAttributes = []; 180 $this->endAttributes = $endAttributes; 181 182 // Keep stack of start and end attributes 183 $this->startAttributeStack = []; 184 $this->endAttributeStack = [$endAttributes]; 185 186 // Start off in the initial state and keep a stack of previous states 187 $state = 0; 188 $stateStack = [$state]; 189 190 // Semantic value stack (contains values of tokens and semantic action results) 191 $this->semStack = []; 192 193 // Current position in the stack(s) 194 $stackPos = 0; 195 196 $this->errorState = 0; 197 198 for (;;) { 199 //$this->traceNewState($state, $symbol); 200 201 if ($this->actionBase[$state] === 0) { 202 $rule = $this->actionDefault[$state]; 203 } else { 204 if ($symbol === self::SYMBOL_NONE) { 205 // Fetch the next token id from the lexer and fetch additional info by-ref. 206 // The end attributes are fetched into a temporary variable and only set once the token is really 207 // shifted (not during read). Otherwise you would sometimes get off-by-one errors, when a rule is 208 // reduced after a token was read but not yet shifted. 209 $tokenId = $this->lexer->getNextToken($tokenValue, $startAttributes, $endAttributes); 210 211 // map the lexer token id to the internally used symbols 212 $symbol = $tokenId >= 0 && $tokenId < $this->tokenToSymbolMapSize 213 ? $this->tokenToSymbol[$tokenId] 214 : $this->invalidSymbol; 215 216 if ($symbol === $this->invalidSymbol) { 217 throw new \RangeException(sprintf( 218 'The lexer returned an invalid token (id=%d, value=%s)', 219 $tokenId, $tokenValue 220 )); 221 } 222 223 // Allow productions to access the start attributes of the lookahead token. 224 $this->lookaheadStartAttributes = $startAttributes; 225 226 //$this->traceRead($symbol); 227 } 228 229 $idx = $this->actionBase[$state] + $symbol; 230 if ((($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol) 231 || ($state < $this->YY2TBLSTATE 232 && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0 233 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol)) 234 && ($action = $this->action[$idx]) !== $this->defaultAction) { 235 /* 236 * >= numNonLeafStates: shift and reduce 237 * > 0: shift 238 * = 0: accept 239 * < 0: reduce 240 * = -YYUNEXPECTED: error 241 */ 242 if ($action > 0) { 243 /* shift */ 244 //$this->traceShift($symbol); 245 246 ++$stackPos; 247 $stateStack[$stackPos] = $state = $action; 248 $this->semStack[$stackPos] = $tokenValue; 249 $this->startAttributeStack[$stackPos] = $startAttributes; 250 $this->endAttributeStack[$stackPos] = $endAttributes; 251 $this->endAttributes = $endAttributes; 252 $symbol = self::SYMBOL_NONE; 253 254 if ($this->errorState) { 255 --$this->errorState; 256 } 257 258 if ($action < $this->numNonLeafStates) { 259 continue; 260 } 261 262 /* $yyn >= numNonLeafStates means shift-and-reduce */ 263 $rule = $action - $this->numNonLeafStates; 264 } else { 265 $rule = -$action; 266 } 267 } else { 268 $rule = $this->actionDefault[$state]; 269 } 270 } 271 272 for (;;) { 273 if ($rule === 0) { 274 /* accept */ 275 //$this->traceAccept(); 276 return $this->semValue; 277 } elseif ($rule !== $this->unexpectedTokenRule) { 278 /* reduce */ 279 //$this->traceReduce($rule); 280 281 try { 282 $this->reduceCallbacks[$rule]($stackPos); 283 } catch (Error $e) { 284 if (-1 === $e->getStartLine() && isset($startAttributes['startLine'])) { 285 $e->setStartLine($startAttributes['startLine']); 286 } 287 288 $this->emitError($e); 289 // Can't recover from this type of error 290 return null; 291 } 292 293 /* Goto - shift nonterminal */ 294 $lastEndAttributes = $this->endAttributeStack[$stackPos]; 295 $ruleLength = $this->ruleToLength[$rule]; 296 $stackPos -= $ruleLength; 297 $nonTerminal = $this->ruleToNonTerminal[$rule]; 298 $idx = $this->gotoBase[$nonTerminal] + $stateStack[$stackPos]; 299 if ($idx >= 0 && $idx < $this->gotoTableSize && $this->gotoCheck[$idx] === $nonTerminal) { 300 $state = $this->goto[$idx]; 301 } else { 302 $state = $this->gotoDefault[$nonTerminal]; 303 } 304 305 ++$stackPos; 306 $stateStack[$stackPos] = $state; 307 $this->semStack[$stackPos] = $this->semValue; 308 $this->endAttributeStack[$stackPos] = $lastEndAttributes; 309 if ($ruleLength === 0) { 310 // Empty productions use the start attributes of the lookahead token. 311 $this->startAttributeStack[$stackPos] = $this->lookaheadStartAttributes; 312 } 313 } else { 314 /* error */ 315 switch ($this->errorState) { 316 case 0: 317 $msg = $this->getErrorMessage($symbol, $state); 318 $this->emitError(new Error($msg, $startAttributes + $endAttributes)); 319 // Break missing intentionally 320 case 1: 321 case 2: 322 $this->errorState = 3; 323 324 // Pop until error-expecting state uncovered 325 while (!( 326 (($idx = $this->actionBase[$state] + $this->errorSymbol) >= 0 327 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol) 328 || ($state < $this->YY2TBLSTATE 329 && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $this->errorSymbol) >= 0 330 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol) 331 ) || ($action = $this->action[$idx]) === $this->defaultAction) { // Not totally sure about this 332 if ($stackPos <= 0) { 333 // Could not recover from error 334 return null; 335 } 336 $state = $stateStack[--$stackPos]; 337 //$this->tracePop($state); 338 } 339 340 //$this->traceShift($this->errorSymbol); 341 ++$stackPos; 342 $stateStack[$stackPos] = $state = $action; 343 344 // We treat the error symbol as being empty, so we reset the end attributes 345 // to the end attributes of the last non-error symbol 346 $this->startAttributeStack[$stackPos] = $this->lookaheadStartAttributes; 347 $this->endAttributeStack[$stackPos] = $this->endAttributeStack[$stackPos - 1]; 348 $this->endAttributes = $this->endAttributeStack[$stackPos - 1]; 349 break; 350 351 case 3: 352 if ($symbol === 0) { 353 // Reached EOF without recovering from error 354 return null; 355 } 356 357 //$this->traceDiscard($symbol); 358 $symbol = self::SYMBOL_NONE; 359 break 2; 360 } 361 } 362 363 if ($state < $this->numNonLeafStates) { 364 break; 365 } 366 367 /* >= numNonLeafStates means shift-and-reduce */ 368 $rule = $state - $this->numNonLeafStates; 369 } 370 } 371 372 throw new \RuntimeException('Reached end of parser loop'); 373 } 374 375 protected function emitError(Error $error) { 376 $this->errorHandler->handleError($error); 377 } 378 379 /** 380 * Format error message including expected tokens. 381 * 382 * @param int $symbol Unexpected symbol 383 * @param int $state State at time of error 384 * 385 * @return string Formatted error message 386 */ 387 protected function getErrorMessage(int $symbol, int $state) : string { 388 $expectedString = ''; 389 if ($expected = $this->getExpectedTokens($state)) { 390 $expectedString = ', expecting ' . implode(' or ', $expected); 391 } 392 393 return 'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString; 394 } 395 396 /** 397 * Get limited number of expected tokens in given state. 398 * 399 * @param int $state State 400 * 401 * @return string[] Expected tokens. If too many, an empty array is returned. 402 */ 403 protected function getExpectedTokens(int $state) : array { 404 $expected = []; 405 406 $base = $this->actionBase[$state]; 407 foreach ($this->symbolToName as $symbol => $name) { 408 $idx = $base + $symbol; 409 if ($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol 410 || $state < $this->YY2TBLSTATE 411 && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0 412 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol 413 ) { 414 if ($this->action[$idx] !== $this->unexpectedTokenRule 415 && $this->action[$idx] !== $this->defaultAction 416 && $symbol !== $this->errorSymbol 417 ) { 418 if (count($expected) === 4) { 419 /* Too many expected tokens */ 420 return []; 421 } 422 423 $expected[] = $name; 424 } 425 } 426 } 427 428 return $expected; 429 } 430 431 /* 432 * Tracing functions used for debugging the parser. 433 */ 434 435 /* 436 protected function traceNewState($state, $symbol) { 437 echo '% State ' . $state 438 . ', Lookahead ' . ($symbol == self::SYMBOL_NONE ? '--none--' : $this->symbolToName[$symbol]) . "\n"; 439 } 440 441 protected function traceRead($symbol) { 442 echo '% Reading ' . $this->symbolToName[$symbol] . "\n"; 443 } 444 445 protected function traceShift($symbol) { 446 echo '% Shift ' . $this->symbolToName[$symbol] . "\n"; 447 } 448 449 protected function traceAccept() { 450 echo "% Accepted.\n"; 451 } 452 453 protected function traceReduce($n) { 454 echo '% Reduce by (' . $n . ') ' . $this->productions[$n] . "\n"; 455 } 456 457 protected function tracePop($state) { 458 echo '% Recovering, uncovered state ' . $state . "\n"; 459 } 460 461 protected function traceDiscard($symbol) { 462 echo '% Discard ' . $this->symbolToName[$symbol] . "\n"; 463 } 464 */ 465 466 /* 467 * Helper functions invoked by semantic actions 468 */ 469 470 /** 471 * Moves statements of semicolon-style namespaces into $ns->stmts and checks various error conditions. 472 * 473 * @param Node\Stmt[] $stmts 474 * @return Node\Stmt[] 475 */ 476 protected function handleNamespaces(array $stmts) : array { 477 $hasErrored = false; 478 $style = $this->getNamespacingStyle($stmts); 479 if (null === $style) { 480 // not namespaced, nothing to do 481 return $stmts; 482 } elseif ('brace' === $style) { 483 // For braced namespaces we only have to check that there are no invalid statements between the namespaces 484 $afterFirstNamespace = false; 485 foreach ($stmts as $stmt) { 486 if ($stmt instanceof Node\Stmt\Namespace_) { 487 $afterFirstNamespace = true; 488 } elseif (!$stmt instanceof Node\Stmt\HaltCompiler 489 && !$stmt instanceof Node\Stmt\Nop 490 && $afterFirstNamespace && !$hasErrored) { 491 $this->emitError(new Error( 492 'No code may exist outside of namespace {}', $stmt->getAttributes())); 493 $hasErrored = true; // Avoid one error for every statement 494 } 495 } 496 return $stmts; 497 } else { 498 // For semicolon namespaces we have to move the statements after a namespace declaration into ->stmts 499 $resultStmts = []; 500 $targetStmts =& $resultStmts; 501 $lastNs = null; 502 foreach ($stmts as $stmt) { 503 if ($stmt instanceof Node\Stmt\Namespace_) { 504 if ($lastNs !== null) { 505 $this->fixupNamespaceAttributes($lastNs); 506 } 507 if ($stmt->stmts === null) { 508 $stmt->stmts = []; 509 $targetStmts =& $stmt->stmts; 510 $resultStmts[] = $stmt; 511 } else { 512 // This handles the invalid case of mixed style namespaces 513 $resultStmts[] = $stmt; 514 $targetStmts =& $resultStmts; 515 } 516 $lastNs = $stmt; 517 } elseif ($stmt instanceof Node\Stmt\HaltCompiler) { 518 // __halt_compiler() is not moved into the namespace 519 $resultStmts[] = $stmt; 520 } else { 521 $targetStmts[] = $stmt; 522 } 523 } 524 if ($lastNs !== null) { 525 $this->fixupNamespaceAttributes($lastNs); 526 } 527 return $resultStmts; 528 } 529 } 530 531 private function fixupNamespaceAttributes(Node\Stmt\Namespace_ $stmt) { 532 // We moved the statements into the namespace node, as such the end of the namespace node 533 // needs to be extended to the end of the statements. 534 if (empty($stmt->stmts)) { 535 return; 536 } 537 538 // We only move the builtin end attributes here. This is the best we can do with the 539 // knowledge we have. 540 $endAttributes = ['endLine', 'endFilePos', 'endTokenPos']; 541 $lastStmt = $stmt->stmts[count($stmt->stmts) - 1]; 542 foreach ($endAttributes as $endAttribute) { 543 if ($lastStmt->hasAttribute($endAttribute)) { 544 $stmt->setAttribute($endAttribute, $lastStmt->getAttribute($endAttribute)); 545 } 546 } 547 } 548 549 /** 550 * Determine namespacing style (semicolon or brace) 551 * 552 * @param Node[] $stmts Top-level statements. 553 * 554 * @return null|string One of "semicolon", "brace" or null (no namespaces) 555 */ 556 private function getNamespacingStyle(array $stmts) { 557 $style = null; 558 $hasNotAllowedStmts = false; 559 foreach ($stmts as $i => $stmt) { 560 if ($stmt instanceof Node\Stmt\Namespace_) { 561 $currentStyle = null === $stmt->stmts ? 'semicolon' : 'brace'; 562 if (null === $style) { 563 $style = $currentStyle; 564 if ($hasNotAllowedStmts) { 565 $this->emitError(new Error( 566 'Namespace declaration statement has to be the very first statement in the script', 567 $stmt->getLine() // Avoid marking the entire namespace as an error 568 )); 569 } 570 } elseif ($style !== $currentStyle) { 571 $this->emitError(new Error( 572 'Cannot mix bracketed namespace declarations with unbracketed namespace declarations', 573 $stmt->getLine() // Avoid marking the entire namespace as an error 574 )); 575 // Treat like semicolon style for namespace normalization 576 return 'semicolon'; 577 } 578 continue; 579 } 580 581 /* declare(), __halt_compiler() and nops can be used before a namespace declaration */ 582 if ($stmt instanceof Node\Stmt\Declare_ 583 || $stmt instanceof Node\Stmt\HaltCompiler 584 || $stmt instanceof Node\Stmt\Nop) { 585 continue; 586 } 587 588 /* There may be a hashbang line at the very start of the file */ 589 if ($i === 0 && $stmt instanceof Node\Stmt\InlineHTML && preg_match('/\A#!.*\r?\n\z/', $stmt->value)) { 590 continue; 591 } 592 593 /* Everything else if forbidden before namespace declarations */ 594 $hasNotAllowedStmts = true; 595 } 596 return $style; 597 } 598 599 /** 600 * Fix up parsing of static property calls in PHP 5. 601 * 602 * In PHP 5 A::$b[c][d] and A::$b[c][d]() have very different interpretation. The former is 603 * interpreted as (A::$b)[c][d], while the latter is the same as A::{$b[c][d]}(). We parse the 604 * latter as the former initially and this method fixes the AST into the correct form when we 605 * encounter the "()". 606 * 607 * @param Node\Expr\StaticPropertyFetch|Node\Expr\ArrayDimFetch $prop 608 * @param Node\Arg[] $args 609 * @param array $attributes 610 * 611 * @return Expr\StaticCall 612 */ 613 protected function fixupPhp5StaticPropCall($prop, array $args, array $attributes) : Expr\StaticCall { 614 if ($prop instanceof Node\Expr\StaticPropertyFetch) { 615 $name = $prop->name instanceof VarLikeIdentifier 616 ? $prop->name->toString() : $prop->name; 617 $var = new Expr\Variable($name, $prop->name->getAttributes()); 618 return new Expr\StaticCall($prop->class, $var, $args, $attributes); 619 } elseif ($prop instanceof Node\Expr\ArrayDimFetch) { 620 $tmp = $prop; 621 while ($tmp->var instanceof Node\Expr\ArrayDimFetch) { 622 $tmp = $tmp->var; 623 } 624 625 /** @var Expr\StaticPropertyFetch $staticProp */ 626 $staticProp = $tmp->var; 627 628 // Set start attributes to attributes of innermost node 629 $tmp = $prop; 630 $this->fixupStartAttributes($tmp, $staticProp->name); 631 while ($tmp->var instanceof Node\Expr\ArrayDimFetch) { 632 $tmp = $tmp->var; 633 $this->fixupStartAttributes($tmp, $staticProp->name); 634 } 635 636 $name = $staticProp->name instanceof VarLikeIdentifier 637 ? $staticProp->name->toString() : $staticProp->name; 638 $tmp->var = new Expr\Variable($name, $staticProp->name->getAttributes()); 639 return new Expr\StaticCall($staticProp->class, $prop, $args, $attributes); 640 } else { 641 throw new \Exception; 642 } 643 } 644 645 protected function fixupStartAttributes(Node $to, Node $from) { 646 $startAttributes = ['startLine', 'startFilePos', 'startTokenPos']; 647 foreach ($startAttributes as $startAttribute) { 648 if ($from->hasAttribute($startAttribute)) { 649 $to->setAttribute($startAttribute, $from->getAttribute($startAttribute)); 650 } 651 } 652 } 653 654 protected function handleBuiltinTypes(Name $name) { 655 $builtinTypes = [ 656 'bool' => true, 657 'int' => true, 658 'float' => true, 659 'string' => true, 660 'iterable' => true, 661 'void' => true, 662 'object' => true, 663 'null' => true, 664 'false' => true, 665 'mixed' => true, 666 'never' => true, 667 ]; 668 669 if (!$name->isUnqualified()) { 670 return $name; 671 } 672 673 $lowerName = $name->toLowerString(); 674 if (!isset($builtinTypes[$lowerName])) { 675 return $name; 676 } 677 678 return new Node\Identifier($lowerName, $name->getAttributes()); 679 } 680 681 /** 682 * Get combined start and end attributes at a stack location 683 * 684 * @param int $pos Stack location 685 * 686 * @return array Combined start and end attributes 687 */ 688 protected function getAttributesAt(int $pos) : array { 689 return $this->startAttributeStack[$pos] + $this->endAttributeStack[$pos]; 690 } 691 692 protected function getFloatCastKind(string $cast): int 693 { 694 $cast = strtolower($cast); 695 if (strpos($cast, 'float') !== false) { 696 return Double::KIND_FLOAT; 697 } 698 699 if (strpos($cast, 'real') !== false) { 700 return Double::KIND_REAL; 701 } 702 703 return Double::KIND_DOUBLE; 704 } 705 706 protected function parseLNumber($str, $attributes, $allowInvalidOctal = false) { 707 try { 708 return LNumber::fromString($str, $attributes, $allowInvalidOctal); 709 } catch (Error $error) { 710 $this->emitError($error); 711 // Use dummy value 712 return new LNumber(0, $attributes); 713 } 714 } 715 716 /** 717 * Parse a T_NUM_STRING token into either an integer or string node. 718 * 719 * @param string $str Number string 720 * @param array $attributes Attributes 721 * 722 * @return LNumber|String_ Integer or string node. 723 */ 724 protected function parseNumString(string $str, array $attributes) { 725 if (!preg_match('/^(?:0|-?[1-9][0-9]*)$/', $str)) { 726 return new String_($str, $attributes); 727 } 728 729 $num = +$str; 730 if (!is_int($num)) { 731 return new String_($str, $attributes); 732 } 733 734 return new LNumber($num, $attributes); 735 } 736 737 protected function stripIndentation( 738 string $string, int $indentLen, string $indentChar, 739 bool $newlineAtStart, bool $newlineAtEnd, array $attributes 740 ) { 741 if ($indentLen === 0) { 742 return $string; 743 } 744 745 $start = $newlineAtStart ? '(?:(?<=\n)|\A)' : '(?<=\n)'; 746 $end = $newlineAtEnd ? '(?:(?=[\r\n])|\z)' : '(?=[\r\n])'; 747 $regex = '/' . $start . '([ \t]*)(' . $end . ')?/'; 748 return preg_replace_callback( 749 $regex, 750 function ($matches) use ($indentLen, $indentChar, $attributes) { 751 $prefix = substr($matches[1], 0, $indentLen); 752 if (false !== strpos($prefix, $indentChar === " " ? "\t" : " ")) { 753 $this->emitError(new Error( 754 'Invalid indentation - tabs and spaces cannot be mixed', $attributes 755 )); 756 } elseif (strlen($prefix) < $indentLen && !isset($matches[2])) { 757 $this->emitError(new Error( 758 'Invalid body indentation level ' . 759 '(expecting an indentation level of at least ' . $indentLen . ')', 760 $attributes 761 )); 762 } 763 return substr($matches[0], strlen($prefix)); 764 }, 765 $string 766 ); 767 } 768 769 protected function parseDocString( 770 string $startToken, $contents, string $endToken, 771 array $attributes, array $endTokenAttributes, bool $parseUnicodeEscape 772 ) { 773 $kind = strpos($startToken, "'") === false 774 ? String_::KIND_HEREDOC : String_::KIND_NOWDOC; 775 776 $regex = '/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/'; 777 $result = preg_match($regex, $startToken, $matches); 778 assert($result === 1); 779 $label = $matches[1]; 780 781 $result = preg_match('/\A[ \t]*/', $endToken, $matches); 782 assert($result === 1); 783 $indentation = $matches[0]; 784 785 $attributes['kind'] = $kind; 786 $attributes['docLabel'] = $label; 787 $attributes['docIndentation'] = $indentation; 788 789 $indentHasSpaces = false !== strpos($indentation, " "); 790 $indentHasTabs = false !== strpos($indentation, "\t"); 791 if ($indentHasSpaces && $indentHasTabs) { 792 $this->emitError(new Error( 793 'Invalid indentation - tabs and spaces cannot be mixed', 794 $endTokenAttributes 795 )); 796 797 // Proceed processing as if this doc string is not indented 798 $indentation = ''; 799 } 800 801 $indentLen = \strlen($indentation); 802 $indentChar = $indentHasSpaces ? " " : "\t"; 803 804 if (\is_string($contents)) { 805 if ($contents === '') { 806 return new String_('', $attributes); 807 } 808 809 $contents = $this->stripIndentation( 810 $contents, $indentLen, $indentChar, true, true, $attributes 811 ); 812 $contents = preg_replace('~(\r\n|\n|\r)\z~', '', $contents); 813 814 if ($kind === String_::KIND_HEREDOC) { 815 $contents = String_::parseEscapeSequences($contents, null, $parseUnicodeEscape); 816 } 817 818 return new String_($contents, $attributes); 819 } else { 820 assert(count($contents) > 0); 821 if (!$contents[0] instanceof Node\Scalar\EncapsedStringPart) { 822 // If there is no leading encapsed string part, pretend there is an empty one 823 $this->stripIndentation( 824 '', $indentLen, $indentChar, true, false, $contents[0]->getAttributes() 825 ); 826 } 827 828 $newContents = []; 829 foreach ($contents as $i => $part) { 830 if ($part instanceof Node\Scalar\EncapsedStringPart) { 831 $isLast = $i === \count($contents) - 1; 832 $part->value = $this->stripIndentation( 833 $part->value, $indentLen, $indentChar, 834 $i === 0, $isLast, $part->getAttributes() 835 ); 836 $part->value = String_::parseEscapeSequences($part->value, null, $parseUnicodeEscape); 837 if ($isLast) { 838 $part->value = preg_replace('~(\r\n|\n|\r)\z~', '', $part->value); 839 } 840 if ('' === $part->value) { 841 continue; 842 } 843 } 844 $newContents[] = $part; 845 } 846 return new Encapsed($newContents, $attributes); 847 } 848 } 849 850 /** 851 * Create attributes for a zero-length common-capturing nop. 852 * 853 * @param Comment[] $comments 854 * @return array 855 */ 856 protected function createCommentNopAttributes(array $comments) { 857 $comment = $comments[count($comments) - 1]; 858 $commentEndLine = $comment->getEndLine(); 859 $commentEndFilePos = $comment->getEndFilePos(); 860 $commentEndTokenPos = $comment->getEndTokenPos(); 861 862 $attributes = ['comments' => $comments]; 863 if (-1 !== $commentEndLine) { 864 $attributes['startLine'] = $commentEndLine; 865 $attributes['endLine'] = $commentEndLine; 866 } 867 if (-1 !== $commentEndFilePos) { 868 $attributes['startFilePos'] = $commentEndFilePos + 1; 869 $attributes['endFilePos'] = $commentEndFilePos; 870 } 871 if (-1 !== $commentEndTokenPos) { 872 $attributes['startTokenPos'] = $commentEndTokenPos + 1; 873 $attributes['endTokenPos'] = $commentEndTokenPos; 874 } 875 return $attributes; 876 } 877 878 protected function checkModifier($a, $b, $modifierPos) { 879 // Jumping through some hoops here because verifyModifier() is also used elsewhere 880 try { 881 Class_::verifyModifier($a, $b); 882 } catch (Error $error) { 883 $error->setAttributes($this->getAttributesAt($modifierPos)); 884 $this->emitError($error); 885 } 886 } 887 888 protected function checkParam(Param $node) { 889 if ($node->variadic && null !== $node->default) { 890 $this->emitError(new Error( 891 'Variadic parameter cannot have a default value', 892 $node->default->getAttributes() 893 )); 894 } 895 } 896 897 protected function checkTryCatch(TryCatch $node) { 898 if (empty($node->catches) && null === $node->finally) { 899 $this->emitError(new Error( 900 'Cannot use try without catch or finally', $node->getAttributes() 901 )); 902 } 903 } 904 905 protected function checkNamespace(Namespace_ $node) { 906 if (null !== $node->stmts) { 907 foreach ($node->stmts as $stmt) { 908 if ($stmt instanceof Namespace_) { 909 $this->emitError(new Error( 910 'Namespace declarations cannot be nested', $stmt->getAttributes() 911 )); 912 } 913 } 914 } 915 } 916 917 private function checkClassName($name, $namePos) { 918 if (null !== $name && $name->isSpecialClassName()) { 919 $this->emitError(new Error( 920 sprintf('Cannot use \'%s\' as class name as it is reserved', $name), 921 $this->getAttributesAt($namePos) 922 )); 923 } 924 } 925 926 private function checkImplementedInterfaces(array $interfaces) { 927 foreach ($interfaces as $interface) { 928 if ($interface->isSpecialClassName()) { 929 $this->emitError(new Error( 930 sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface), 931 $interface->getAttributes() 932 )); 933 } 934 } 935 } 936 937 protected function checkClass(Class_ $node, $namePos) { 938 $this->checkClassName($node->name, $namePos); 939 940 if ($node->extends && $node->extends->isSpecialClassName()) { 941 $this->emitError(new Error( 942 sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends), 943 $node->extends->getAttributes() 944 )); 945 } 946 947 $this->checkImplementedInterfaces($node->implements); 948 } 949 950 protected function checkInterface(Interface_ $node, $namePos) { 951 $this->checkClassName($node->name, $namePos); 952 $this->checkImplementedInterfaces($node->extends); 953 } 954 955 protected function checkEnum(Enum_ $node, $namePos) { 956 $this->checkClassName($node->name, $namePos); 957 $this->checkImplementedInterfaces($node->implements); 958 } 959 960 protected function checkClassMethod(ClassMethod $node, $modifierPos) { 961 if ($node->flags & Class_::MODIFIER_STATIC) { 962 switch ($node->name->toLowerString()) { 963 case '__construct': 964 $this->emitError(new Error( 965 sprintf('Constructor %s() cannot be static', $node->name), 966 $this->getAttributesAt($modifierPos))); 967 break; 968 case '__destruct': 969 $this->emitError(new Error( 970 sprintf('Destructor %s() cannot be static', $node->name), 971 $this->getAttributesAt($modifierPos))); 972 break; 973 case '__clone': 974 $this->emitError(new Error( 975 sprintf('Clone method %s() cannot be static', $node->name), 976 $this->getAttributesAt($modifierPos))); 977 break; 978 } 979 } 980 } 981 982 protected function checkClassConst(ClassConst $node, $modifierPos) { 983 if ($node->flags & Class_::MODIFIER_STATIC) { 984 $this->emitError(new Error( 985 "Cannot use 'static' as constant modifier", 986 $this->getAttributesAt($modifierPos))); 987 } 988 if ($node->flags & Class_::MODIFIER_ABSTRACT) { 989 $this->emitError(new Error( 990 "Cannot use 'abstract' as constant modifier", 991 $this->getAttributesAt($modifierPos))); 992 } 993 if ($node->flags & Class_::MODIFIER_FINAL) { 994 $this->emitError(new Error( 995 "Cannot use 'final' as constant modifier", 996 $this->getAttributesAt($modifierPos))); 997 } 998 } 999 1000 protected function checkProperty(Property $node, $modifierPos) { 1001 if ($node->flags & Class_::MODIFIER_ABSTRACT) { 1002 $this->emitError(new Error('Properties cannot be declared abstract', 1003 $this->getAttributesAt($modifierPos))); 1004 } 1005 1006 if ($node->flags & Class_::MODIFIER_FINAL) { 1007 $this->emitError(new Error('Properties cannot be declared final', 1008 $this->getAttributesAt($modifierPos))); 1009 } 1010 } 1011 1012 protected function checkUseUse(UseUse $node, $namePos) { 1013 if ($node->alias && $node->alias->isSpecialClassName()) { 1014 $this->emitError(new Error( 1015 sprintf( 1016 'Cannot use %s as %s because \'%2$s\' is a special class name', 1017 $node->name, $node->alias 1018 ), 1019 $this->getAttributesAt($namePos) 1020 )); 1021 } 1022 } 1023} 1024