PluginProbe
WPIDE – File Manager & Code Editor / 3.5.9
WPIDE – File Manager & Code Editor v3.5.9
3.5.9 3.5.8 3.5.7 2.0.14 2.0.15 2.0.16 2.0.2 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1 2.2 2.3 2.3.1 2.3.2 2.4.0 2.5 2.6 3.0 3.1 3.2 3.3 All 55 releases
wpide / vendor / nikic / php-parser / lib / PhpParser / ParserAbstract.php

ParserAbstract.php in WPIDE – File Manager & Code Editor 3.5.9, at vendor/nikic/php-parser/lib/PhpParser/ParserAbstract.php

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