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:
10: use PhpParser\Node\Arg;
11: use PhpParser\Node\Expr;
12: use PhpParser\Node\Expr\Array_;
13: use PhpParser\Node\Expr\Cast\Double;
14: use PhpParser\Node\Identifier;
15: use PhpParser\Node\InterpolatedStringPart;
16: use PhpParser\Node\Name;
17: use PhpParser\Node\Param;
18: use PhpParser\Node\PropertyHook;
19: use PhpParser\Node\Scalar\InterpolatedString;
20: use PhpParser\Node\Scalar\Int_;
21: use PhpParser\Node\Scalar\String_;
22: use PhpParser\Node\Stmt;
23: use PhpParser\Node\Stmt\Class_;
24: use PhpParser\Node\Stmt\ClassConst;
25: use PhpParser\Node\Stmt\ClassMethod;
26: use PhpParser\Node\Stmt\Const_;
27: use PhpParser\Node\Stmt\Else_;
28: use PhpParser\Node\Stmt\ElseIf_;
29: use PhpParser\Node\Stmt\Enum_;
30: use PhpParser\Node\Stmt\Interface_;
31: use PhpParser\Node\Stmt\Namespace_;
32: use PhpParser\Node\Stmt\Nop;
33: use PhpParser\Node\Stmt\Property;
34: use PhpParser\Node\Stmt\TryCatch;
35: use PhpParser\Node\UseItem;
36: use PhpParser\Node\VarLikeIdentifier;
37: use PhpParser\NodeVisitor\CommentAnnotatingVisitor;
38:
39: abstract class ParserAbstract implements Parser {
40: private const SYMBOL_NONE = -1;
41:
42: /** @var Lexer Lexer that is used when parsing */
43: protected Lexer $lexer;
44: /** @var PhpVersion PHP version to target on a best-effort basis */
45: protected PhpVersion $phpVersion;
46:
47: /*
48: * The following members will be filled with generated parsing data:
49: */
50:
51: /** @var int Size of $tokenToSymbol map */
52: protected int $tokenToSymbolMapSize;
53: /** @var int Size of $action table */
54: protected int $actionTableSize;
55: /** @var int Size of $goto table */
56: protected int $gotoTableSize;
57:
58: /** @var int Symbol number signifying an invalid token */
59: protected int $invalidSymbol;
60: /** @var int Symbol number of error recovery token */
61: protected int $errorSymbol;
62: /** @var int Action number signifying default action */
63: protected int $defaultAction;
64: /** @var int Rule number signifying that an unexpected token was encountered */
65: protected int $unexpectedTokenRule;
66:
67: protected int $YY2TBLSTATE;
68: /** @var int Number of non-leaf states */
69: protected int $numNonLeafStates;
70:
71: /** @var int[] Map of PHP token IDs to internal symbols */
72: protected array $phpTokenToSymbol;
73: /** @var array<int, bool> Map of PHP token IDs to drop */
74: protected array $dropTokens;
75: /** @var int[] Map of external symbols (static::T_*) to internal symbols */
76: protected array $tokenToSymbol;
77: /** @var string[] Map of symbols to their names */
78: protected array $symbolToName;
79: /** @var array<int, string> Names of the production rules (only necessary for debugging) */
80: protected array $productions;
81:
82: /** @var int[] Map of states to a displacement into the $action table. The corresponding action for this
83: * state/symbol pair is $action[$actionBase[$state] + $symbol]. If $actionBase[$state] is 0, the
84: * action is defaulted, i.e. $actionDefault[$state] should be used instead. */
85: protected array $actionBase;
86: /** @var int[] Table of actions. Indexed according to $actionBase comment. */
87: protected array $action;
88: /** @var int[] Table indexed analogously to $action. If $actionCheck[$actionBase[$state] + $symbol] != $symbol
89: * then the action is defaulted, i.e. $actionDefault[$state] should be used instead. */
90: protected array $actionCheck;
91: /** @var int[] Map of states to their default action */
92: protected array $actionDefault;
93: /** @var callable[] Semantic action callbacks */
94: protected array $reduceCallbacks;
95:
96: /** @var int[] Map of non-terminals to a displacement into the $goto table. The corresponding goto state for this
97: * non-terminal/state pair is $goto[$gotoBase[$nonTerminal] + $state] (unless defaulted) */
98: protected array $gotoBase;
99: /** @var int[] Table of states to goto after reduction. Indexed according to $gotoBase comment. */
100: protected array $goto;
101: /** @var int[] Table indexed analogously to $goto. If $gotoCheck[$gotoBase[$nonTerminal] + $state] != $nonTerminal
102: * then the goto state is defaulted, i.e. $gotoDefault[$nonTerminal] should be used. */
103: protected array $gotoCheck;
104: /** @var int[] Map of non-terminals to the default state to goto after their reduction */
105: protected array $gotoDefault;
106:
107: /** @var int[] Map of rules to the non-terminal on their left-hand side, i.e. the non-terminal to use for
108: * determining the state to goto after reduction. */
109: protected array $ruleToNonTerminal;
110: /** @var int[] Map of rules to the length of their right-hand side, which is the number of elements that have to
111: * be popped from the stack(s) on reduction. */
112: protected array $ruleToLength;
113:
114: /*
115: * The following members are part of the parser state:
116: */
117:
118: /** @var mixed Temporary value containing the result of last semantic action (reduction) */
119: protected $semValue;
120: /** @var mixed[] Semantic value stack (contains values of tokens and semantic action results) */
121: protected array $semStack;
122: /** @var int[] Token start position stack */
123: protected array $tokenStartStack;
124: /** @var int[] Token end position stack */
125: protected array $tokenEndStack;
126:
127: /** @var ErrorHandler Error handler */
128: protected ErrorHandler $errorHandler;
129: /** @var int Error state, used to avoid error floods */
130: protected int $errorState;
131:
132: /** @var \SplObjectStorage<Array_, null>|null Array nodes created during parsing, for postprocessing of empty elements. */
133: protected ?\SplObjectStorage $createdArrays;
134:
135: /** @var \SplObjectStorage<Expr\ArrowFunction, null>|null
136: * Arrow functions that are wrapped in parentheses, to enforce the pipe operator parentheses requirements.
137: */
138: protected ?\SplObjectStorage $parenthesizedArrowFunctions;
139:
140: /** @var Token[] Tokens for the current parse */
141: protected array $tokens;
142: /** @var int Current position in token array */
143: protected int $tokenPos;
144:
145: /**
146: * Initialize $reduceCallbacks map.
147: */
148: abstract protected function initReduceCallbacks(): void;
149:
150: /**
151: * Creates a parser instance.
152: *
153: * Options:
154: * * phpVersion: ?PhpVersion,
155: *
156: * @param Lexer $lexer A lexer
157: * @param PhpVersion $phpVersion PHP version to target, defaults to latest supported. This
158: * option is best-effort: Even if specified, parsing will generally assume the latest
159: * supported version and only adjust behavior in minor ways, for example by omitting
160: * errors in older versions and interpreting type hints as a name or identifier depending
161: * on version.
162: */
163: public function __construct(Lexer $lexer, ?PhpVersion $phpVersion = null) {
164: $this->lexer = $lexer;
165: $this->phpVersion = $phpVersion ?? PhpVersion::getNewestSupported();
166:
167: $this->initReduceCallbacks();
168: $this->phpTokenToSymbol = $this->createTokenMap();
169: $this->dropTokens = array_fill_keys(
170: [\T_WHITESPACE, \T_OPEN_TAG, \T_COMMENT, \T_DOC_COMMENT, \T_BAD_CHARACTER], true
171: );
172: }
173:
174: /**
175: * Parses PHP code into a node tree.
176: *
177: * If a non-throwing error handler is used, the parser will continue parsing after an error
178: * occurred and attempt to build a partial AST.
179: *
180: * @param string $code The source code to parse
181: * @param ErrorHandler|null $errorHandler Error handler to use for lexer/parser errors, defaults
182: * to ErrorHandler\Throwing.
183: *
184: * @return Node\Stmt[]|null Array of statements (or null non-throwing error handler is used and
185: * the parser was unable to recover from an error).
186: */
187: public function parse(string $code, ?ErrorHandler $errorHandler = null): ?array {
188: $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing();
189: $this->createdArrays = new \SplObjectStorage();
190: $this->parenthesizedArrowFunctions = new \SplObjectStorage();
191:
192: $this->tokens = $this->lexer->tokenize($code, $this->errorHandler);
193: $result = $this->doParse();
194:
195: // Report errors for any empty elements used inside arrays. This is delayed until after the main parse,
196: // because we don't know a priori whether a given array expression will be used in a destructuring context
197: // or not.
198: foreach ($this->createdArrays as $node) {
199: foreach ($node->items as $item) {
200: if ($item->value instanceof Expr\Error) {
201: $this->errorHandler->handleError(
202: new Error('Cannot use empty array elements in arrays', $item->getAttributes()));
203: }
204: }
205: }
206:
207: // Clear out some of the interior state, so we don't hold onto unnecessary
208: // memory between uses of the parser
209: $this->tokenStartStack = [];
210: $this->tokenEndStack = [];
211: $this->semStack = [];
212: $this->semValue = null;
213: $this->createdArrays = null;
214: $this->parenthesizedArrowFunctions = null;
215:
216: if ($result !== null) {
217: $traverser = new NodeTraverser(new CommentAnnotatingVisitor($this->tokens));
218: $traverser->traverse($result);
219: }
220:
221: return $result;
222: }
223:
224: public function getTokens(): array {
225: return $this->tokens;
226: }
227:
228: /** @return Stmt[]|null */
229: protected function doParse(): ?array {
230: // We start off with no lookahead-token
231: $symbol = self::SYMBOL_NONE;
232: $tokenValue = null;
233: $this->tokenPos = -1;
234:
235: // Keep stack of start and end attributes
236: $this->tokenStartStack = [];
237: $this->tokenEndStack = [0];
238:
239: // Start off in the initial state and keep a stack of previous states
240: $state = 0;
241: $stateStack = [$state];
242:
243: // Semantic value stack (contains values of tokens and semantic action results)
244: $this->semStack = [];
245:
246: // Current position in the stack(s)
247: $stackPos = 0;
248:
249: $this->errorState = 0;
250:
251: for (;;) {
252: //$this->traceNewState($state, $symbol);
253:
254: if ($this->actionBase[$state] === 0) {
255: $rule = $this->actionDefault[$state];
256: } else {
257: if ($symbol === self::SYMBOL_NONE) {
258: do {
259: $token = $this->tokens[++$this->tokenPos];
260: $tokenId = $token->id;
261: } while (isset($this->dropTokens[$tokenId]));
262:
263: // Map the lexer token id to the internally used symbols.
264: $tokenValue = $token->text;
265: if (!isset($this->phpTokenToSymbol[$tokenId])) {
266: throw new \RangeException(sprintf(
267: 'The lexer returned an invalid token (id=%d, value=%s)',
268: $tokenId, $tokenValue
269: ));
270: }
271: $symbol = $this->phpTokenToSymbol[$tokenId];
272:
273: //$this->traceRead($symbol);
274: }
275:
276: $idx = $this->actionBase[$state] + $symbol;
277: if ((($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol)
278: || ($state < $this->YY2TBLSTATE
279: && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0
280: && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol))
281: && ($action = $this->action[$idx]) !== $this->defaultAction) {
282: /*
283: * >= numNonLeafStates: shift and reduce
284: * > 0: shift
285: * = 0: accept
286: * < 0: reduce
287: * = -YYUNEXPECTED: error
288: */
289: if ($action > 0) {
290: /* shift */
291: //$this->traceShift($symbol);
292:
293: ++$stackPos;
294: $stateStack[$stackPos] = $state = $action;
295: $this->semStack[$stackPos] = $tokenValue;
296: $this->tokenStartStack[$stackPos] = $this->tokenPos;
297: $this->tokenEndStack[$stackPos] = $this->tokenPos;
298: $symbol = self::SYMBOL_NONE;
299:
300: if ($this->errorState) {
301: --$this->errorState;
302: }
303:
304: if ($action < $this->numNonLeafStates) {
305: continue;
306: }
307:
308: /* $yyn >= numNonLeafStates means shift-and-reduce */
309: $rule = $action - $this->numNonLeafStates;
310: } else {
311: $rule = -$action;
312: }
313: } else {
314: $rule = $this->actionDefault[$state];
315: }
316: }
317:
318: for (;;) {
319: if ($rule === 0) {
320: /* accept */
321: //$this->traceAccept();
322: return $this->semValue;
323: }
324: if ($rule !== $this->unexpectedTokenRule) {
325: /* reduce */
326: //$this->traceReduce($rule);
327:
328: $ruleLength = $this->ruleToLength[$rule];
329: try {
330: $callback = $this->reduceCallbacks[$rule];
331: if ($callback !== null) {
332: $callback($this, $stackPos);
333: } elseif ($ruleLength > 0) {
334: $this->semValue = $this->semStack[$stackPos - $ruleLength + 1];
335: }
336: } catch (Error $e) {
337: if (-1 === $e->getStartLine()) {
338: $e->setStartLine($this->tokens[$this->tokenPos]->line);
339: }
340:
341: $this->emitError($e);
342: // Can't recover from this type of error
343: return null;
344: }
345:
346: /* Goto - shift nonterminal */
347: $lastTokenEnd = $this->tokenEndStack[$stackPos];
348: $stackPos -= $ruleLength;
349: $nonTerminal = $this->ruleToNonTerminal[$rule];
350: $idx = $this->gotoBase[$nonTerminal] + $stateStack[$stackPos];
351: if ($idx >= 0 && $idx < $this->gotoTableSize && $this->gotoCheck[$idx] === $nonTerminal) {
352: $state = $this->goto[$idx];
353: } else {
354: $state = $this->gotoDefault[$nonTerminal];
355: }
356:
357: ++$stackPos;
358: $stateStack[$stackPos] = $state;
359: $this->semStack[$stackPos] = $this->semValue;
360: $this->tokenEndStack[$stackPos] = $lastTokenEnd;
361: if ($ruleLength === 0) {
362: // Empty productions use the start attributes of the lookahead token.
363: $this->tokenStartStack[$stackPos] = $this->tokenPos;
364: }
365: } else {
366: /* error */
367: switch ($this->errorState) {
368: case 0:
369: $msg = $this->getErrorMessage($symbol, $state);
370: $this->emitError(new Error($msg, $this->getAttributesForToken($this->tokenPos)));
371: // Break missing intentionally
372: // no break
373: case 1:
374: case 2:
375: $this->errorState = 3;
376:
377: // Pop until error-expecting state uncovered
378: while (!(
379: (($idx = $this->actionBase[$state] + $this->errorSymbol) >= 0
380: && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol)
381: || ($state < $this->YY2TBLSTATE
382: && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $this->errorSymbol) >= 0
383: && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol)
384: ) || ($action = $this->action[$idx]) === $this->defaultAction) { // Not totally sure about this
385: if ($stackPos <= 0) {
386: // Could not recover from error
387: return null;
388: }
389: $state = $stateStack[--$stackPos];
390: //$this->tracePop($state);
391: }
392:
393: //$this->traceShift($this->errorSymbol);
394: ++$stackPos;
395: $stateStack[$stackPos] = $state = $action;
396:
397: // We treat the error symbol as being empty, so we reset the end attributes
398: // to the end attributes of the last non-error symbol
399: $this->tokenStartStack[$stackPos] = $this->tokenPos;
400: $this->tokenEndStack[$stackPos] = $this->tokenEndStack[$stackPos - 1];
401: break;
402:
403: case 3:
404: if ($symbol === 0) {
405: // Reached EOF without recovering from error
406: return null;
407: }
408:
409: //$this->traceDiscard($symbol);
410: $symbol = self::SYMBOL_NONE;
411: break 2;
412: }
413: }
414:
415: if ($state < $this->numNonLeafStates) {
416: break;
417: }
418:
419: /* >= numNonLeafStates means shift-and-reduce */
420: $rule = $state - $this->numNonLeafStates;
421: }
422: }
423: }
424:
425: protected function emitError(Error $error): void {
426: $this->errorHandler->handleError($error);
427: }
428:
429: /**
430: * Format error message including expected tokens.
431: *
432: * @param int $symbol Unexpected symbol
433: * @param int $state State at time of error
434: *
435: * @return string Formatted error message
436: */
437: protected function getErrorMessage(int $symbol, int $state): string {
438: $expectedString = '';
439: if ($expected = $this->getExpectedTokens($state)) {
440: $expectedString = ', expecting ' . implode(' or ', $expected);
441: }
442:
443: return 'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString;
444: }
445:
446: /**
447: * Get limited number of expected tokens in given state.
448: *
449: * @param int $state State
450: *
451: * @return string[] Expected tokens. If too many, an empty array is returned.
452: */
453: protected function getExpectedTokens(int $state): array {
454: $expected = [];
455:
456: $base = $this->actionBase[$state];
457: foreach ($this->symbolToName as $symbol => $name) {
458: $idx = $base + $symbol;
459: if ($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol
460: || $state < $this->YY2TBLSTATE
461: && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0
462: && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol
463: ) {
464: if ($this->action[$idx] !== $this->unexpectedTokenRule
465: && $this->action[$idx] !== $this->defaultAction
466: && $symbol !== $this->errorSymbol
467: ) {
468: if (count($expected) === 4) {
469: /* Too many expected tokens */
470: return [];
471: }
472:
473: $expected[] = $name;
474: }
475: }
476: }
477:
478: return $expected;
479: }
480:
481: /**
482: * Get attributes for a node with the given start and end token positions.
483: *
484: * @param int $tokenStartPos Token position the node starts at
485: * @param int $tokenEndPos Token position the node ends at
486: * @return array<string, mixed> Attributes
487: */
488: protected function getAttributes(int $tokenStartPos, int $tokenEndPos): array {
489: $startToken = $this->tokens[$tokenStartPos];
490: $afterEndToken = $this->tokens[$tokenEndPos + 1];
491: return [
492: 'startLine' => $startToken->line,
493: 'startTokenPos' => $tokenStartPos,
494: 'startFilePos' => $startToken->pos,
495: 'endLine' => $afterEndToken->line,
496: 'endTokenPos' => $tokenEndPos,
497: 'endFilePos' => $afterEndToken->pos - 1,
498: ];
499: }
500:
501: /**
502: * Get attributes for a single token at the given token position.
503: *
504: * @return array<string, mixed> Attributes
505: */
506: protected function getAttributesForToken(int $tokenPos): array {
507: if ($tokenPos < \count($this->tokens) - 1) {
508: return $this->getAttributes($tokenPos, $tokenPos);
509: }
510:
511: // Get attributes for the sentinel token.
512: $token = $this->tokens[$tokenPos];
513: return [
514: 'startLine' => $token->line,
515: 'startTokenPos' => $tokenPos,
516: 'startFilePos' => $token->pos,
517: 'endLine' => $token->line,
518: 'endTokenPos' => $tokenPos,
519: 'endFilePos' => $token->pos,
520: ];
521: }
522:
523: /*
524: * Tracing functions used for debugging the parser.
525: */
526:
527: /*
528: protected function traceNewState($state, $symbol): void {
529: echo '% State ' . $state
530: . ', Lookahead ' . ($symbol == self::SYMBOL_NONE ? '--none--' : $this->symbolToName[$symbol]) . "\n";
531: }
532:
533: protected function traceRead($symbol): void {
534: echo '% Reading ' . $this->symbolToName[$symbol] . "\n";
535: }
536:
537: protected function traceShift($symbol): void {
538: echo '% Shift ' . $this->symbolToName[$symbol] . "\n";
539: }
540:
541: protected function traceAccept(): void {
542: echo "% Accepted.\n";
543: }
544:
545: protected function traceReduce($n): void {
546: echo '% Reduce by (' . $n . ') ' . $this->productions[$n] . "\n";
547: }
548:
549: protected function tracePop($state): void {
550: echo '% Recovering, uncovered state ' . $state . "\n";
551: }
552:
553: protected function traceDiscard($symbol): void {
554: echo '% Discard ' . $this->symbolToName[$symbol] . "\n";
555: }
556: */
557:
558: /*
559: * Helper functions invoked by semantic actions
560: */
561:
562: /**
563: * Moves statements of semicolon-style namespaces into $ns->stmts and checks various error conditions.
564: *
565: * @param Node\Stmt[] $stmts
566: * @return Node\Stmt[]
567: */
568: protected function handleNamespaces(array $stmts): array {
569: $hasErrored = false;
570: $style = $this->getNamespacingStyle($stmts);
571: if (null === $style) {
572: // not namespaced, nothing to do
573: return $stmts;
574: }
575: if ('brace' === $style) {
576: // For braced namespaces we only have to check that there are no invalid statements between the namespaces
577: $afterFirstNamespace = false;
578: foreach ($stmts as $stmt) {
579: if ($stmt instanceof Node\Stmt\Namespace_) {
580: $afterFirstNamespace = true;
581: } elseif (!$stmt instanceof Node\Stmt\HaltCompiler
582: && !$stmt instanceof Node\Stmt\Nop
583: && $afterFirstNamespace && !$hasErrored) {
584: $this->emitError(new Error(
585: 'No code may exist outside of namespace {}', $stmt->getAttributes()));
586: $hasErrored = true; // Avoid one error for every statement
587: }
588: }
589: return $stmts;
590: } else {
591: // For semicolon namespaces we have to move the statements after a namespace declaration into ->stmts
592: $resultStmts = [];
593: $targetStmts = &$resultStmts;
594: $lastNs = null;
595: foreach ($stmts as $stmt) {
596: if ($stmt instanceof Node\Stmt\Namespace_) {
597: if ($lastNs !== null) {
598: $this->fixupNamespaceAttributes($lastNs);
599: }
600: if ($stmt->stmts === null) {
601: $stmt->stmts = [];
602: $targetStmts = &$stmt->stmts;
603: $resultStmts[] = $stmt;
604: } else {
605: // This handles the invalid case of mixed style namespaces
606: $resultStmts[] = $stmt;
607: $targetStmts = &$resultStmts;
608: }
609: $lastNs = $stmt;
610: } elseif ($stmt instanceof Node\Stmt\HaltCompiler) {
611: // __halt_compiler() is not moved into the namespace
612: $resultStmts[] = $stmt;
613: } else {
614: $targetStmts[] = $stmt;
615: }
616: }
617: if ($lastNs !== null) {
618: $this->fixupNamespaceAttributes($lastNs);
619: }
620: return $resultStmts;
621: }
622: }
623:
624: private function fixupNamespaceAttributes(Node\Stmt\Namespace_ $stmt): void {
625: // We moved the statements into the namespace node, as such the end of the namespace node
626: // needs to be extended to the end of the statements.
627: if (empty($stmt->stmts)) {
628: return;
629: }
630:
631: // We only move the builtin end attributes here. This is the best we can do with the
632: // knowledge we have.
633: $endAttributes = ['endLine', 'endFilePos', 'endTokenPos'];
634: $lastStmt = $stmt->stmts[count($stmt->stmts) - 1];
635: foreach ($endAttributes as $endAttribute) {
636: if ($lastStmt->hasAttribute($endAttribute)) {
637: $stmt->setAttribute($endAttribute, $lastStmt->getAttribute($endAttribute));
638: }
639: }
640: }
641:
642: /** @return array<string, mixed> */
643: private function getNamespaceErrorAttributes(Namespace_ $node): array {
644: $attrs = $node->getAttributes();
645: // Adjust end attributes to only cover the "namespace" keyword, not the whole namespace.
646: if (isset($attrs['startLine'])) {
647: $attrs['endLine'] = $attrs['startLine'];
648: }
649: if (isset($attrs['startTokenPos'])) {
650: $attrs['endTokenPos'] = $attrs['startTokenPos'];
651: }
652: if (isset($attrs['startFilePos'])) {
653: $attrs['endFilePos'] = $attrs['startFilePos'] + \strlen('namespace') - 1;
654: }
655: return $attrs;
656: }
657:
658: /**
659: * Determine namespacing style (semicolon or brace)
660: *
661: * @param Node[] $stmts Top-level statements.
662: *
663: * @return null|string One of "semicolon", "brace" or null (no namespaces)
664: */
665: private function getNamespacingStyle(array $stmts): ?string {
666: $style = null;
667: $hasNotAllowedStmts = false;
668: foreach ($stmts as $i => $stmt) {
669: if ($stmt instanceof Node\Stmt\Namespace_) {
670: $currentStyle = null === $stmt->stmts ? 'semicolon' : 'brace';
671: if (null === $style) {
672: $style = $currentStyle;
673: if ($hasNotAllowedStmts) {
674: $this->emitError(new Error(
675: 'Namespace declaration statement has to be the very first statement in the script',
676: $this->getNamespaceErrorAttributes($stmt)
677: ));
678: }
679: } elseif ($style !== $currentStyle) {
680: $this->emitError(new Error(
681: 'Cannot mix bracketed namespace declarations with unbracketed namespace declarations',
682: $this->getNamespaceErrorAttributes($stmt)
683: ));
684: // Treat like semicolon style for namespace normalization
685: return 'semicolon';
686: }
687: continue;
688: }
689:
690: /* declare(), __halt_compiler() and nops can be used before a namespace declaration */
691: if ($stmt instanceof Node\Stmt\Declare_
692: || $stmt instanceof Node\Stmt\HaltCompiler
693: || $stmt instanceof Node\Stmt\Nop) {
694: continue;
695: }
696:
697: /* There may be a hashbang line at the very start of the file */
698: if ($i === 0 && $stmt instanceof Node\Stmt\InlineHTML && preg_match('/\A#!.*\r?\n\z/', $stmt->value)) {
699: continue;
700: }
701:
702: /* Everything else if forbidden before namespace declarations */
703: $hasNotAllowedStmts = true;
704: }
705: return $style;
706: }
707:
708: /** @return Name|Identifier */
709: protected function handleBuiltinTypes(Name $name) {
710: if (!$name->isUnqualified()) {
711: return $name;
712: }
713:
714: $lowerName = $name->toLowerString();
715: if (!$this->phpVersion->supportsBuiltinType($lowerName)) {
716: return $name;
717: }
718:
719: return new Node\Identifier($lowerName, $name->getAttributes());
720: }
721:
722: /**
723: * Get combined start and end attributes at a stack location
724: *
725: * @param int $stackPos Stack location
726: *
727: * @return array<string, mixed> Combined start and end attributes
728: */
729: protected function getAttributesAt(int $stackPos): array {
730: return $this->getAttributes($this->tokenStartStack[$stackPos], $this->tokenEndStack[$stackPos]);
731: }
732:
733: protected function getFloatCastKind(string $cast): int {
734: $cast = strtolower($cast);
735: if (strpos($cast, 'float') !== false) {
736: return Double::KIND_FLOAT;
737: }
738:
739: if (strpos($cast, 'real') !== false) {
740: return Double::KIND_REAL;
741: }
742:
743: return Double::KIND_DOUBLE;
744: }
745:
746: protected function getIntCastKind(string $cast): int {
747: $cast = strtolower($cast);
748: if (strpos($cast, 'integer') !== false) {
749: return Expr\Cast\Int_::KIND_INTEGER;
750: }
751:
752: return Expr\Cast\Int_::KIND_INT;
753: }
754:
755: protected function getBoolCastKind(string $cast): int {
756: $cast = strtolower($cast);
757: if (strpos($cast, 'boolean') !== false) {
758: return Expr\Cast\Bool_::KIND_BOOLEAN;
759: }
760:
761: return Expr\Cast\Bool_::KIND_BOOL;
762: }
763:
764: protected function getStringCastKind(string $cast): int {
765: $cast = strtolower($cast);
766: if (strpos($cast, 'binary') !== false) {
767: return Expr\Cast\String_::KIND_BINARY;
768: }
769:
770: return Expr\Cast\String_::KIND_STRING;
771: }
772:
773: /** @param array<string, mixed> $attributes */
774: protected function parseLNumber(string $str, array $attributes, bool $allowInvalidOctal = false): Int_ {
775: try {
776: return Int_::fromString($str, $attributes, $allowInvalidOctal);
777: } catch (Error $error) {
778: $this->emitError($error);
779: // Use dummy value
780: return new Int_(0, $attributes);
781: }
782: }
783:
784: /**
785: * Parse a T_NUM_STRING token into either an integer or string node.
786: *
787: * @param string $str Number string
788: * @param array<string, mixed> $attributes Attributes
789: *
790: * @return Int_|String_ Integer or string node.
791: */
792: protected function parseNumString(string $str, array $attributes) {
793: if (!preg_match('/^(?:0|-?[1-9][0-9]*)$/', $str)) {
794: return new String_($str, $attributes);
795: }
796:
797: $num = +$str;
798: if (!is_int($num)) {
799: return new String_($str, $attributes);
800: }
801:
802: return new Int_($num, $attributes);
803: }
804:
805: /** @param array<string, mixed> $attributes */
806: protected function stripIndentation(
807: string $string, int $indentLen, string $indentChar,
808: bool $newlineAtStart, bool $newlineAtEnd, array $attributes
809: ): string {
810: if ($indentLen === 0) {
811: return $string;
812: }
813:
814: $start = $newlineAtStart ? '(?:(?<=\n)|\A)' : '(?<=\n)';
815: $end = $newlineAtEnd ? '(?:(?=[\r\n])|\z)' : '(?=[\r\n])';
816: $regex = '/' . $start . '([ \t]*)(' . $end . ')?/';
817: return preg_replace_callback(
818: $regex,
819: function ($matches) use ($indentLen, $indentChar, $attributes) {
820: $prefix = substr($matches[1], 0, $indentLen);
821: if (false !== strpos($prefix, $indentChar === " " ? "\t" : " ")) {
822: $this->emitError(new Error(
823: 'Invalid indentation - tabs and spaces cannot be mixed', $attributes
824: ));
825: } elseif (strlen($prefix) < $indentLen && !isset($matches[2])) {
826: $this->emitError(new Error(
827: 'Invalid body indentation level ' .
828: '(expecting an indentation level of at least ' . $indentLen . ')',
829: $attributes
830: ));
831: }
832: return substr($matches[0], strlen($prefix));
833: },
834: $string
835: );
836: }
837:
838: /**
839: * @param string|(Expr|InterpolatedStringPart)[] $contents
840: * @param array<string, mixed> $attributes
841: * @param array<string, mixed> $endTokenAttributes
842: */
843: protected function parseDocString(
844: string $startToken, $contents, string $endToken,
845: array $attributes, array $endTokenAttributes, bool $parseUnicodeEscape
846: ): Expr {
847: $kind = strpos($startToken, "'") === false
848: ? String_::KIND_HEREDOC : String_::KIND_NOWDOC;
849:
850: $regex = '/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/';
851: $result = preg_match($regex, $startToken, $matches);
852: assert($result === 1);
853: $label = $matches[1];
854:
855: $result = preg_match('/\A[ \t]*/', $endToken, $matches);
856: assert($result === 1);
857: $indentation = $matches[0];
858:
859: $attributes['kind'] = $kind;
860: $attributes['docLabel'] = $label;
861: $attributes['docIndentation'] = $indentation;
862:
863: $indentHasSpaces = false !== strpos($indentation, " ");
864: $indentHasTabs = false !== strpos($indentation, "\t");
865: if ($indentHasSpaces && $indentHasTabs) {
866: $this->emitError(new Error(
867: 'Invalid indentation - tabs and spaces cannot be mixed',
868: $endTokenAttributes
869: ));
870:
871: // Proceed processing as if this doc string is not indented
872: $indentation = '';
873: }
874:
875: $indentLen = \strlen($indentation);
876: $indentChar = $indentHasSpaces ? " " : "\t";
877:
878: if (\is_string($contents)) {
879: if ($contents === '') {
880: $attributes['rawValue'] = $contents;
881: return new String_('', $attributes);
882: }
883:
884: $contents = $this->stripIndentation(
885: $contents, $indentLen, $indentChar, true, true, $attributes
886: );
887: $contents = preg_replace('~(\r\n|\n|\r)\z~', '', $contents);
888: $attributes['rawValue'] = $contents;
889:
890: if ($kind === String_::KIND_HEREDOC) {
891: $contents = String_::parseEscapeSequences($contents, null, $parseUnicodeEscape);
892: }
893:
894: return new String_($contents, $attributes);
895: } else {
896: assert(count($contents) > 0);
897: if (!$contents[0] instanceof Node\InterpolatedStringPart) {
898: // If there is no leading encapsed string part, pretend there is an empty one
899: $this->stripIndentation(
900: '', $indentLen, $indentChar, true, false, $contents[0]->getAttributes()
901: );
902: }
903:
904: $newContents = [];
905: foreach ($contents as $i => $part) {
906: if ($part instanceof Node\InterpolatedStringPart) {
907: $isLast = $i === \count($contents) - 1;
908: $part->value = $this->stripIndentation(
909: $part->value, $indentLen, $indentChar,
910: $i === 0, $isLast, $part->getAttributes()
911: );
912: if ($isLast) {
913: $part->value = preg_replace('~(\r\n|\n|\r)\z~', '', $part->value);
914: }
915: $part->setAttribute('rawValue', $part->value);
916: $part->value = String_::parseEscapeSequences($part->value, null, $parseUnicodeEscape);
917: if ('' === $part->value) {
918: continue;
919: }
920: }
921: $newContents[] = $part;
922: }
923: return new InterpolatedString($newContents, $attributes);
924: }
925: }
926:
927: protected function createCommentFromToken(Token $token, int $tokenPos): Comment {
928: assert($token->id === \T_COMMENT || $token->id == \T_DOC_COMMENT);
929: return \T_DOC_COMMENT === $token->id
930: ? new Comment\Doc($token->text, $token->line, $token->pos, $tokenPos,
931: $token->getEndLine(), $token->getEndPos() - 1, $tokenPos)
932: : new Comment($token->text, $token->line, $token->pos, $tokenPos,
933: $token->getEndLine(), $token->getEndPos() - 1, $tokenPos);
934: }
935:
936: /**
937: * Get last comment before the given token position, if any
938: */
939: protected function getCommentBeforeToken(int $tokenPos): ?Comment {
940: while (--$tokenPos >= 0) {
941: $token = $this->tokens[$tokenPos];
942: if (!isset($this->dropTokens[$token->id])) {
943: break;
944: }
945:
946: if ($token->id === \T_COMMENT || $token->id === \T_DOC_COMMENT) {
947: return $this->createCommentFromToken($token, $tokenPos);
948: }
949: }
950: return null;
951: }
952:
953: /**
954: * Create a zero-length nop to capture preceding comments, if any.
955: */
956: protected function maybeCreateZeroLengthNop(int $tokenPos): ?Nop {
957: $comment = $this->getCommentBeforeToken($tokenPos);
958: if ($comment === null) {
959: return null;
960: }
961:
962: $commentEndLine = $comment->getEndLine();
963: $commentEndFilePos = $comment->getEndFilePos();
964: $commentEndTokenPos = $comment->getEndTokenPos();
965: $attributes = [
966: 'startLine' => $commentEndLine,
967: 'endLine' => $commentEndLine,
968: 'startFilePos' => $commentEndFilePos + 1,
969: 'endFilePos' => $commentEndFilePos,
970: 'startTokenPos' => $commentEndTokenPos + 1,
971: 'endTokenPos' => $commentEndTokenPos,
972: ];
973: return new Nop($attributes);
974: }
975:
976: protected function maybeCreateNop(int $tokenStartPos, int $tokenEndPos): ?Nop {
977: if ($this->getCommentBeforeToken($tokenStartPos) === null) {
978: return null;
979: }
980: return new Nop($this->getAttributes($tokenStartPos, $tokenEndPos));
981: }
982:
983: protected function handleHaltCompiler(): string {
984: // Prevent the lexer from returning any further tokens.
985: $nextToken = $this->tokens[$this->tokenPos + 1];
986: $this->tokenPos = \count($this->tokens) - 2;
987:
988: // Return text after __halt_compiler.
989: return $nextToken->id === \T_INLINE_HTML ? $nextToken->text : '';
990: }
991:
992: protected function inlineHtmlHasLeadingNewline(int $stackPos): bool {
993: $tokenPos = $this->tokenStartStack[$stackPos];
994: $token = $this->tokens[$tokenPos];
995: assert($token->id == \T_INLINE_HTML);
996: if ($tokenPos > 0) {
997: $prevToken = $this->tokens[$tokenPos - 1];
998: assert($prevToken->id == \T_CLOSE_TAG);
999: return false !== strpos($prevToken->text, "\n")
1000: || false !== strpos($prevToken->text, "\r");
1001: }
1002: return true;
1003: }
1004:
1005: /**
1006: * @return array<string, mixed>
1007: */
1008: protected function createEmptyElemAttributes(int $tokenPos): array {
1009: return $this->getAttributesForToken($tokenPos);
1010: }
1011:
1012: protected function fixupArrayDestructuring(Array_ $node): Expr\List_ {
1013: $this->createdArrays->offsetUnset($node);
1014: return new Expr\List_(array_map(function (Node\ArrayItem $item) {
1015: if ($item->value instanceof Expr\Error) {
1016: // We used Error as a placeholder for empty elements, which are legal for destructuring.
1017: return null;
1018: }
1019: if ($item->value instanceof Array_) {
1020: return new Node\ArrayItem(
1021: $this->fixupArrayDestructuring($item->value),
1022: $item->key, $item->byRef, $item->getAttributes());
1023: }
1024: return $item;
1025: }, $node->items), ['kind' => Expr\List_::KIND_ARRAY] + $node->getAttributes());
1026: }
1027:
1028: protected function postprocessList(Expr\List_ $node): void {
1029: foreach ($node->items as $i => $item) {
1030: if ($item->value instanceof Expr\Error) {
1031: // We used Error as a placeholder for empty elements, which are legal for destructuring.
1032: $node->items[$i] = null;
1033: }
1034: }
1035: }
1036:
1037: /** @param ElseIf_|Else_ $node */
1038: protected function fixupAlternativeElse($node): void {
1039: // Make sure a trailing nop statement carrying comments is part of the node.
1040: $numStmts = \count($node->stmts);
1041: if ($numStmts !== 0 && $node->stmts[$numStmts - 1] instanceof Nop) {
1042: $nopAttrs = $node->stmts[$numStmts - 1]->getAttributes();
1043: if (isset($nopAttrs['endLine'])) {
1044: $node->setAttribute('endLine', $nopAttrs['endLine']);
1045: }
1046: if (isset($nopAttrs['endFilePos'])) {
1047: $node->setAttribute('endFilePos', $nopAttrs['endFilePos']);
1048: }
1049: if (isset($nopAttrs['endTokenPos'])) {
1050: $node->setAttribute('endTokenPos', $nopAttrs['endTokenPos']);
1051: }
1052: }
1053: }
1054:
1055: protected function checkClassModifier(int $a, int $b, int $modifierPos): void {
1056: try {
1057: Modifiers::verifyClassModifier($a, $b);
1058: } catch (Error $error) {
1059: $error->setAttributes($this->getAttributesAt($modifierPos));
1060: $this->emitError($error);
1061: }
1062: }
1063:
1064: protected function checkModifier(int $a, int $b, int $modifierPos): void {
1065: // Jumping through some hoops here because verifyModifier() is also used elsewhere
1066: try {
1067: Modifiers::verifyModifier($a, $b);
1068: } catch (Error $error) {
1069: $error->setAttributes($this->getAttributesAt($modifierPos));
1070: $this->emitError($error);
1071: }
1072: }
1073:
1074: protected function checkParam(Param $node): void {
1075: if ($node->variadic && null !== $node->default) {
1076: $this->emitError(new Error(
1077: 'Variadic parameter cannot have a default value',
1078: $node->default->getAttributes()
1079: ));
1080: }
1081: }
1082:
1083: protected function checkTryCatch(TryCatch $node): void {
1084: if (empty($node->catches) && null === $node->finally) {
1085: $this->emitError(new Error(
1086: 'Cannot use try without catch or finally', $node->getAttributes()
1087: ));
1088: }
1089: }
1090:
1091: protected function checkNamespace(Namespace_ $node): void {
1092: if (null !== $node->stmts) {
1093: foreach ($node->stmts as $stmt) {
1094: if ($stmt instanceof Namespace_) {
1095: $this->emitError(new Error(
1096: 'Namespace declarations cannot be nested', $stmt->getAttributes()
1097: ));
1098: }
1099: }
1100: }
1101: }
1102:
1103: private function checkClassName(?Identifier $name, int $namePos): void {
1104: if (null !== $name && $name->isSpecialClassName()) {
1105: $this->emitError(new Error(
1106: sprintf('Cannot use \'%s\' as class name as it is reserved', $name),
1107: $this->getAttributesAt($namePos)
1108: ));
1109: }
1110: }
1111:
1112: /** @param Name[] $interfaces */
1113: private function checkImplementedInterfaces(array $interfaces): void {
1114: foreach ($interfaces as $interface) {
1115: if ($interface->isSpecialClassName()) {
1116: $this->emitError(new Error(
1117: sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
1118: $interface->getAttributes()
1119: ));
1120: }
1121: }
1122: }
1123:
1124: protected function checkClass(Class_ $node, int $namePos): void {
1125: $this->checkClassName($node->name, $namePos);
1126:
1127: if ($node->extends && $node->extends->isSpecialClassName()) {
1128: $this->emitError(new Error(
1129: sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends),
1130: $node->extends->getAttributes()
1131: ));
1132: }
1133:
1134: $this->checkImplementedInterfaces($node->implements);
1135: }
1136:
1137: protected function checkInterface(Interface_ $node, int $namePos): void {
1138: $this->checkClassName($node->name, $namePos);
1139: $this->checkImplementedInterfaces($node->extends);
1140: }
1141:
1142: protected function checkEnum(Enum_ $node, int $namePos): void {
1143: $this->checkClassName($node->name, $namePos);
1144: $this->checkImplementedInterfaces($node->implements);
1145: }
1146:
1147: protected function checkClassMethod(ClassMethod $node, int $modifierPos): void {
1148: if ($node->flags & Modifiers::STATIC) {
1149: switch ($node->name->toLowerString()) {
1150: case '__construct':
1151: $this->emitError(new Error(
1152: sprintf('Constructor %s() cannot be static', $node->name),
1153: $this->getAttributesAt($modifierPos)));
1154: break;
1155: case '__destruct':
1156: $this->emitError(new Error(
1157: sprintf('Destructor %s() cannot be static', $node->name),
1158: $this->getAttributesAt($modifierPos)));
1159: break;
1160: case '__clone':
1161: $this->emitError(new Error(
1162: sprintf('Clone method %s() cannot be static', $node->name),
1163: $this->getAttributesAt($modifierPos)));
1164: break;
1165: }
1166: }
1167:
1168: if ($node->flags & Modifiers::READONLY) {
1169: $this->emitError(new Error(
1170: sprintf('Method %s() cannot be readonly', $node->name),
1171: $this->getAttributesAt($modifierPos)));
1172: }
1173: }
1174:
1175: protected function checkClassConst(ClassConst $node, int $modifierPos): void {
1176: foreach ([Modifiers::STATIC, Modifiers::ABSTRACT, Modifiers::READONLY] as $modifier) {
1177: if ($node->flags & $modifier) {
1178: $this->emitError(new Error(
1179: "Cannot use '" . Modifiers::toString($modifier) . "' as constant modifier",
1180: $this->getAttributesAt($modifierPos)));
1181: }
1182: }
1183: }
1184:
1185: protected function checkUseUse(UseItem $node, int $namePos): void {
1186: if ($node->alias && $node->alias->isSpecialClassName()) {
1187: $this->emitError(new Error(
1188: sprintf(
1189: 'Cannot use %s as %s because \'%2$s\' is a special class name',
1190: $node->name, $node->alias
1191: ),
1192: $this->getAttributesAt($namePos)
1193: ));
1194: }
1195: }
1196:
1197: protected function checkPropertyHooksForMultiProperty(Property $property, int $hookPos): void {
1198: if (count($property->props) > 1) {
1199: $this->emitError(new Error(
1200: 'Cannot use hooks when declaring multiple properties', $this->getAttributesAt($hookPos)));
1201: }
1202: }
1203:
1204: /** @param PropertyHook[] $hooks */
1205: protected function checkEmptyPropertyHookList(array $hooks, int $hookPos): void {
1206: if (empty($hooks)) {
1207: $this->emitError(new Error(
1208: 'Property hook list cannot be empty', $this->getAttributesAt($hookPos)));
1209: }
1210: }
1211:
1212: protected function checkPropertyHook(PropertyHook $hook, ?int $paramListPos): void {
1213: $name = $hook->name->toLowerString();
1214: if ($name !== 'get' && $name !== 'set') {
1215: $this->emitError(new Error(
1216: 'Unknown hook "' . $hook->name . '", expected "get" or "set"',
1217: $hook->name->getAttributes()));
1218: }
1219: if ($name === 'get' && $paramListPos !== null) {
1220: $this->emitError(new Error(
1221: 'get hook must not have a parameter list', $this->getAttributesAt($paramListPos)));
1222: }
1223: }
1224:
1225: protected function checkPropertyHookModifiers(int $a, int $b, int $modifierPos): void {
1226: try {
1227: Modifiers::verifyModifier($a, $b);
1228: } catch (Error $error) {
1229: $error->setAttributes($this->getAttributesAt($modifierPos));
1230: $this->emitError($error);
1231: }
1232:
1233: if ($b != Modifiers::FINAL) {
1234: $this->emitError(new Error(
1235: 'Cannot use the ' . Modifiers::toString($b) . ' modifier on a property hook',
1236: $this->getAttributesAt($modifierPos)));
1237: }
1238: }
1239:
1240: protected function checkConstantAttributes(Const_ $node): void {
1241: if ($node->attrGroups !== [] && count($node->consts) > 1) {
1242: $this->emitError(new Error(
1243: 'Cannot use attributes on multiple constants at once', $node->getAttributes()));
1244: }
1245: }
1246:
1247: protected function checkPipeOperatorParentheses(Expr $node): void {
1248: if ($node instanceof Expr\ArrowFunction && !$this->parenthesizedArrowFunctions->offsetExists($node)) {
1249: $this->emitError(new Error(
1250: 'Arrow functions on the right hand side of |> must be parenthesized', $node->getAttributes()));
1251: }
1252: }
1253:
1254: /**
1255: * @param Property|Param $node
1256: */
1257: protected function addPropertyNameToHooks(Node $node): void {
1258: if ($node instanceof Property) {
1259: $name = $node->props[0]->name->toString();
1260: } else {
1261: $name = $node->var->name;
1262: }
1263: foreach ($node->hooks as $hook) {
1264: $hook->setAttribute('propertyName', $name);
1265: }
1266: }
1267:
1268: /** @param array<Node\Arg|Node\VariadicPlaceholder> $args */
1269: private function isSimpleExit(array $args): bool {
1270: if (\count($args) === 0) {
1271: return true;
1272: }
1273: if (\count($args) === 1) {
1274: $arg = $args[0];
1275: return $arg instanceof Arg && $arg->name === null &&
1276: $arg->byRef === false && $arg->unpack === false;
1277: }
1278: return false;
1279: }
1280:
1281: /**
1282: * @param array<Node\Arg|Node\VariadicPlaceholder> $args
1283: * @param array<string, mixed> $attrs
1284: */
1285: protected function createExitExpr(string $name, int $namePos, array $args, array $attrs): Expr {
1286: if ($this->isSimpleExit($args)) {
1287: // Create Exit node for backwards compatibility.
1288: $attrs['kind'] = strtolower($name) === 'exit' ? Expr\Exit_::KIND_EXIT : Expr\Exit_::KIND_DIE;
1289: return new Expr\Exit_(\count($args) === 1 ? $args[0]->value : null, $attrs);
1290: }
1291: return new Expr\FuncCall(new Name($name, $this->getAttributesAt($namePos)), $args, $attrs);
1292: }
1293:
1294: /**
1295: * Creates the token map.
1296: *
1297: * The token map maps the PHP internal token identifiers
1298: * to the identifiers used by the Parser. Additionally it
1299: * maps T_OPEN_TAG_WITH_ECHO to T_ECHO and T_CLOSE_TAG to ';'.
1300: *
1301: * @return array<int, int> The token map
1302: */
1303: protected function createTokenMap(): array {
1304: $tokenMap = [];
1305:
1306: // Single-char tokens use an identity mapping.
1307: for ($i = 0; $i < 256; ++$i) {
1308: $tokenMap[$i] = $i;
1309: }
1310:
1311: foreach ($this->symbolToName as $name) {
1312: if ($name[0] === 'T') {
1313: $tokenMap[\constant($name)] = constant(static::class . '::' . $name);
1314: }
1315: }
1316:
1317: // T_OPEN_TAG_WITH_ECHO with dropped T_OPEN_TAG results in T_ECHO
1318: $tokenMap[\T_OPEN_TAG_WITH_ECHO] = static::T_ECHO;
1319: // T_CLOSE_TAG is equivalent to ';'
1320: $tokenMap[\T_CLOSE_TAG] = ord(';');
1321:
1322: // We have created a map from PHP token IDs to external symbol IDs.
1323: // Now map them to the internal symbol ID.
1324: $fullTokenMap = [];
1325: foreach ($tokenMap as $phpToken => $extSymbol) {
1326: $intSymbol = $this->tokenToSymbol[$extSymbol];
1327: if ($intSymbol === $this->invalidSymbol) {
1328: continue;
1329: }
1330: $fullTokenMap[$phpToken] = $intSymbol;
1331: }
1332:
1333: return $fullTokenMap;
1334: }
1335: }
1336: