1: <?php declare(strict_types=1);
2:
3: namespace 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: */
9: use PhpParser\Node\Expr;
10: use PhpParser\Node\Expr\Cast\Double;
11: use PhpParser\Node\Name;
12: use PhpParser\Node\Param;
13: use PhpParser\Node\Scalar\Encapsed;
14: use PhpParser\Node\Scalar\LNumber;
15: use PhpParser\Node\Scalar\String_;
16: use PhpParser\Node\Stmt\Class_;
17: use PhpParser\Node\Stmt\ClassConst;
18: use PhpParser\Node\Stmt\ClassMethod;
19: use PhpParser\Node\Stmt\Enum_;
20: use PhpParser\Node\Stmt\Interface_;
21: use PhpParser\Node\Stmt\Namespace_;
22: use PhpParser\Node\Stmt\Property;
23: use PhpParser\Node\Stmt\TryCatch;
24: use PhpParser\Node\Stmt\UseUse;
25: use PhpParser\Node\VarLikeIdentifier;
26:
27: abstract 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: 'true' => true,
668: ];
669:
670: if (!$name->isUnqualified()) {
671: return $name;
672: }
673:
674: $lowerName = $name->toLowerString();
675: if (!isset($builtinTypes[$lowerName])) {
676: return $name;
677: }
678:
679: return new Node\Identifier($lowerName, $name->getAttributes());
680: }
681:
682: /**
683: * Get combined start and end attributes at a stack location
684: *
685: * @param int $pos Stack location
686: *
687: * @return array Combined start and end attributes
688: */
689: protected function getAttributesAt(int $pos) : array {
690: return $this->startAttributeStack[$pos] + $this->endAttributeStack[$pos];
691: }
692:
693: protected function getFloatCastKind(string $cast): int
694: {
695: $cast = strtolower($cast);
696: if (strpos($cast, 'float') !== false) {
697: return Double::KIND_FLOAT;
698: }
699:
700: if (strpos($cast, 'real') !== false) {
701: return Double::KIND_REAL;
702: }
703:
704: return Double::KIND_DOUBLE;
705: }
706:
707: protected function parseLNumber($str, $attributes, $allowInvalidOctal = false) {
708: try {
709: return LNumber::fromString($str, $attributes, $allowInvalidOctal);
710: } catch (Error $error) {
711: $this->emitError($error);
712: // Use dummy value
713: return new LNumber(0, $attributes);
714: }
715: }
716:
717: /**
718: * Parse a T_NUM_STRING token into either an integer or string node.
719: *
720: * @param string $str Number string
721: * @param array $attributes Attributes
722: *
723: * @return LNumber|String_ Integer or string node.
724: */
725: protected function parseNumString(string $str, array $attributes) {
726: if (!preg_match('/^(?:0|-?[1-9][0-9]*)$/', $str)) {
727: return new String_($str, $attributes);
728: }
729:
730: $num = +$str;
731: if (!is_int($num)) {
732: return new String_($str, $attributes);
733: }
734:
735: return new LNumber($num, $attributes);
736: }
737:
738: protected function stripIndentation(
739: string $string, int $indentLen, string $indentChar,
740: bool $newlineAtStart, bool $newlineAtEnd, array $attributes
741: ) {
742: if ($indentLen === 0) {
743: return $string;
744: }
745:
746: $start = $newlineAtStart ? '(?:(?<=\n)|\A)' : '(?<=\n)';
747: $end = $newlineAtEnd ? '(?:(?=[\r\n])|\z)' : '(?=[\r\n])';
748: $regex = '/' . $start . '([ \t]*)(' . $end . ')?/';
749: return preg_replace_callback(
750: $regex,
751: function ($matches) use ($indentLen, $indentChar, $attributes) {
752: $prefix = substr($matches[1], 0, $indentLen);
753: if (false !== strpos($prefix, $indentChar === " " ? "\t" : " ")) {
754: $this->emitError(new Error(
755: 'Invalid indentation - tabs and spaces cannot be mixed', $attributes
756: ));
757: } elseif (strlen($prefix) < $indentLen && !isset($matches[2])) {
758: $this->emitError(new Error(
759: 'Invalid body indentation level ' .
760: '(expecting an indentation level of at least ' . $indentLen . ')',
761: $attributes
762: ));
763: }
764: return substr($matches[0], strlen($prefix));
765: },
766: $string
767: );
768: }
769:
770: protected function parseDocString(
771: string $startToken, $contents, string $endToken,
772: array $attributes, array $endTokenAttributes, bool $parseUnicodeEscape
773: ) {
774: $kind = strpos($startToken, "'") === false
775: ? String_::KIND_HEREDOC : String_::KIND_NOWDOC;
776:
777: $regex = '/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/';
778: $result = preg_match($regex, $startToken, $matches);
779: assert($result === 1);
780: $label = $matches[1];
781:
782: $result = preg_match('/\A[ \t]*/', $endToken, $matches);
783: assert($result === 1);
784: $indentation = $matches[0];
785:
786: $attributes['kind'] = $kind;
787: $attributes['docLabel'] = $label;
788: $attributes['docIndentation'] = $indentation;
789:
790: $indentHasSpaces = false !== strpos($indentation, " ");
791: $indentHasTabs = false !== strpos($indentation, "\t");
792: if ($indentHasSpaces && $indentHasTabs) {
793: $this->emitError(new Error(
794: 'Invalid indentation - tabs and spaces cannot be mixed',
795: $endTokenAttributes
796: ));
797:
798: // Proceed processing as if this doc string is not indented
799: $indentation = '';
800: }
801:
802: $indentLen = \strlen($indentation);
803: $indentChar = $indentHasSpaces ? " " : "\t";
804:
805: if (\is_string($contents)) {
806: if ($contents === '') {
807: return new String_('', $attributes);
808: }
809:
810: $contents = $this->stripIndentation(
811: $contents, $indentLen, $indentChar, true, true, $attributes
812: );
813: $contents = preg_replace('~(\r\n|\n|\r)\z~', '', $contents);
814:
815: if ($kind === String_::KIND_HEREDOC) {
816: $contents = String_::parseEscapeSequences($contents, null, $parseUnicodeEscape);
817: }
818:
819: return new String_($contents, $attributes);
820: } else {
821: assert(count($contents) > 0);
822: if (!$contents[0] instanceof Node\Scalar\EncapsedStringPart) {
823: // If there is no leading encapsed string part, pretend there is an empty one
824: $this->stripIndentation(
825: '', $indentLen, $indentChar, true, false, $contents[0]->getAttributes()
826: );
827: }
828:
829: $newContents = [];
830: foreach ($contents as $i => $part) {
831: if ($part instanceof Node\Scalar\EncapsedStringPart) {
832: $isLast = $i === \count($contents) - 1;
833: $part->value = $this->stripIndentation(
834: $part->value, $indentLen, $indentChar,
835: $i === 0, $isLast, $part->getAttributes()
836: );
837: $part->value = String_::parseEscapeSequences($part->value, null, $parseUnicodeEscape);
838: if ($isLast) {
839: $part->value = preg_replace('~(\r\n|\n|\r)\z~', '', $part->value);
840: }
841: if ('' === $part->value) {
842: continue;
843: }
844: }
845: $newContents[] = $part;
846: }
847: return new Encapsed($newContents, $attributes);
848: }
849: }
850:
851: /**
852: * Create attributes for a zero-length common-capturing nop.
853: *
854: * @param Comment[] $comments
855: * @return array
856: */
857: protected function createCommentNopAttributes(array $comments) {
858: $comment = $comments[count($comments) - 1];
859: $commentEndLine = $comment->getEndLine();
860: $commentEndFilePos = $comment->getEndFilePos();
861: $commentEndTokenPos = $comment->getEndTokenPos();
862:
863: $attributes = ['comments' => $comments];
864: if (-1 !== $commentEndLine) {
865: $attributes['startLine'] = $commentEndLine;
866: $attributes['endLine'] = $commentEndLine;
867: }
868: if (-1 !== $commentEndFilePos) {
869: $attributes['startFilePos'] = $commentEndFilePos + 1;
870: $attributes['endFilePos'] = $commentEndFilePos;
871: }
872: if (-1 !== $commentEndTokenPos) {
873: $attributes['startTokenPos'] = $commentEndTokenPos + 1;
874: $attributes['endTokenPos'] = $commentEndTokenPos;
875: }
876: return $attributes;
877: }
878:
879: protected function checkClassModifier($a, $b, $modifierPos) {
880: try {
881: Class_::verifyClassModifier($a, $b);
882: } catch (Error $error) {
883: $error->setAttributes($this->getAttributesAt($modifierPos));
884: $this->emitError($error);
885: }
886: }
887:
888: protected function checkModifier($a, $b, $modifierPos) {
889: // Jumping through some hoops here because verifyModifier() is also used elsewhere
890: try {
891: Class_::verifyModifier($a, $b);
892: } catch (Error $error) {
893: $error->setAttributes($this->getAttributesAt($modifierPos));
894: $this->emitError($error);
895: }
896: }
897:
898: protected function checkParam(Param $node) {
899: if ($node->variadic && null !== $node->default) {
900: $this->emitError(new Error(
901: 'Variadic parameter cannot have a default value',
902: $node->default->getAttributes()
903: ));
904: }
905: }
906:
907: protected function checkTryCatch(TryCatch $node) {
908: if (empty($node->catches) && null === $node->finally) {
909: $this->emitError(new Error(
910: 'Cannot use try without catch or finally', $node->getAttributes()
911: ));
912: }
913: }
914:
915: protected function checkNamespace(Namespace_ $node) {
916: if (null !== $node->stmts) {
917: foreach ($node->stmts as $stmt) {
918: if ($stmt instanceof Namespace_) {
919: $this->emitError(new Error(
920: 'Namespace declarations cannot be nested', $stmt->getAttributes()
921: ));
922: }
923: }
924: }
925: }
926:
927: private function checkClassName($name, $namePos) {
928: if (null !== $name && $name->isSpecialClassName()) {
929: $this->emitError(new Error(
930: sprintf('Cannot use \'%s\' as class name as it is reserved', $name),
931: $this->getAttributesAt($namePos)
932: ));
933: }
934: }
935:
936: private function checkImplementedInterfaces(array $interfaces) {
937: foreach ($interfaces as $interface) {
938: if ($interface->isSpecialClassName()) {
939: $this->emitError(new Error(
940: sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
941: $interface->getAttributes()
942: ));
943: }
944: }
945: }
946:
947: protected function checkClass(Class_ $node, $namePos) {
948: $this->checkClassName($node->name, $namePos);
949:
950: if ($node->extends && $node->extends->isSpecialClassName()) {
951: $this->emitError(new Error(
952: sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends),
953: $node->extends->getAttributes()
954: ));
955: }
956:
957: $this->checkImplementedInterfaces($node->implements);
958: }
959:
960: protected function checkInterface(Interface_ $node, $namePos) {
961: $this->checkClassName($node->name, $namePos);
962: $this->checkImplementedInterfaces($node->extends);
963: }
964:
965: protected function checkEnum(Enum_ $node, $namePos) {
966: $this->checkClassName($node->name, $namePos);
967: $this->checkImplementedInterfaces($node->implements);
968: }
969:
970: protected function checkClassMethod(ClassMethod $node, $modifierPos) {
971: if ($node->flags & Class_::MODIFIER_STATIC) {
972: switch ($node->name->toLowerString()) {
973: case '__construct':
974: $this->emitError(new Error(
975: sprintf('Constructor %s() cannot be static', $node->name),
976: $this->getAttributesAt($modifierPos)));
977: break;
978: case '__destruct':
979: $this->emitError(new Error(
980: sprintf('Destructor %s() cannot be static', $node->name),
981: $this->getAttributesAt($modifierPos)));
982: break;
983: case '__clone':
984: $this->emitError(new Error(
985: sprintf('Clone method %s() cannot be static', $node->name),
986: $this->getAttributesAt($modifierPos)));
987: break;
988: }
989: }
990:
991: if ($node->flags & Class_::MODIFIER_READONLY) {
992: $this->emitError(new Error(
993: sprintf('Method %s() cannot be readonly', $node->name),
994: $this->getAttributesAt($modifierPos)));
995: }
996: }
997:
998: protected function checkClassConst(ClassConst $node, $modifierPos) {
999: if ($node->flags & Class_::MODIFIER_STATIC) {
1000: $this->emitError(new Error(
1001: "Cannot use 'static' as constant modifier",
1002: $this->getAttributesAt($modifierPos)));
1003: }
1004: if ($node->flags & Class_::MODIFIER_ABSTRACT) {
1005: $this->emitError(new Error(
1006: "Cannot use 'abstract' as constant modifier",
1007: $this->getAttributesAt($modifierPos)));
1008: }
1009: if ($node->flags & Class_::MODIFIER_READONLY) {
1010: $this->emitError(new Error(
1011: "Cannot use 'readonly' as constant modifier",
1012: $this->getAttributesAt($modifierPos)));
1013: }
1014: }
1015:
1016: protected function checkProperty(Property $node, $modifierPos) {
1017: if ($node->flags & Class_::MODIFIER_ABSTRACT) {
1018: $this->emitError(new Error('Properties cannot be declared abstract',
1019: $this->getAttributesAt($modifierPos)));
1020: }
1021:
1022: if ($node->flags & Class_::MODIFIER_FINAL) {
1023: $this->emitError(new Error('Properties cannot be declared final',
1024: $this->getAttributesAt($modifierPos)));
1025: }
1026: }
1027:
1028: protected function checkUseUse(UseUse $node, $namePos) {
1029: if ($node->alias && $node->alias->isSpecialClassName()) {
1030: $this->emitError(new Error(
1031: sprintf(
1032: 'Cannot use %s as %s because \'%2$s\' is a special class name',
1033: $node->name, $node->alias
1034: ),
1035: $this->getAttributesAt($namePos)
1036: ));
1037: }
1038: }
1039: }
1040: