1: | <?php declare(strict_types=1); |
2: | |
3: | namespace PhpParser; |
4: | |
5: | |
6: | |
7: | |
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\Else_; |
27: | use PhpParser\Node\Stmt\ElseIf_; |
28: | use PhpParser\Node\Stmt\Enum_; |
29: | use PhpParser\Node\Stmt\Interface_; |
30: | use PhpParser\Node\Stmt\Namespace_; |
31: | use PhpParser\Node\Stmt\Nop; |
32: | use PhpParser\Node\Stmt\Property; |
33: | use PhpParser\Node\Stmt\TryCatch; |
34: | use PhpParser\Node\UseItem; |
35: | use PhpParser\NodeVisitor\CommentAnnotatingVisitor; |
36: | |
37: | abstract class ParserAbstract implements Parser { |
38: | private const SYMBOL_NONE = -1; |
39: | |
40: | |
41: | protected Lexer $lexer; |
42: | |
43: | protected PhpVersion $phpVersion; |
44: | |
45: | |
46: | |
47: | |
48: | |
49: | |
50: | protected int $tokenToSymbolMapSize; |
51: | |
52: | protected int $actionTableSize; |
53: | |
54: | protected int $gotoTableSize; |
55: | |
56: | |
57: | protected int $invalidSymbol; |
58: | |
59: | protected int $errorSymbol; |
60: | |
61: | protected int $defaultAction; |
62: | |
63: | protected int $unexpectedTokenRule; |
64: | |
65: | protected int $YY2TBLSTATE; |
66: | |
67: | protected int $numNonLeafStates; |
68: | |
69: | |
70: | protected array $phpTokenToSymbol; |
71: | |
72: | protected array $dropTokens; |
73: | |
74: | protected array $tokenToSymbol; |
75: | |
76: | protected array $symbolToName; |
77: | |
78: | protected array $productions; |
79: | |
80: | |
81: | |
82: | |
83: | protected array $actionBase; |
84: | |
85: | protected array $action; |
86: | |
87: | |
88: | protected array $actionCheck; |
89: | |
90: | protected array $actionDefault; |
91: | |
92: | protected array $reduceCallbacks; |
93: | |
94: | |
95: | |
96: | protected array $gotoBase; |
97: | |
98: | protected array $goto; |
99: | |
100: | |
101: | protected array $gotoCheck; |
102: | |
103: | protected array $gotoDefault; |
104: | |
105: | |
106: | |
107: | protected array $ruleToNonTerminal; |
108: | |
109: | |
110: | protected array $ruleToLength; |
111: | |
112: | |
113: | |
114: | |
115: | |
116: | |
117: | protected $semValue; |
118: | |
119: | protected array $semStack; |
120: | |
121: | protected array $tokenStartStack; |
122: | |
123: | protected array $tokenEndStack; |
124: | |
125: | |
126: | protected ErrorHandler $errorHandler; |
127: | |
128: | protected int $errorState; |
129: | |
130: | |
131: | protected ?\SplObjectStorage $createdArrays; |
132: | |
133: | |
134: | protected array $tokens; |
135: | |
136: | protected int $tokenPos; |
137: | |
138: | |
139: | |
140: | |
141: | abstract protected function initReduceCallbacks(): void; |
142: | |
143: | |
144: | |
145: | |
146: | |
147: | |
148: | |
149: | |
150: | |
151: | |
152: | |
153: | |
154: | |
155: | |
156: | public function __construct(Lexer $lexer, ?PhpVersion $phpVersion = null) { |
157: | $this->lexer = $lexer; |
158: | $this->phpVersion = $phpVersion ?? PhpVersion::getNewestSupported(); |
159: | |
160: | $this->initReduceCallbacks(); |
161: | $this->phpTokenToSymbol = $this->createTokenMap(); |
162: | $this->dropTokens = array_fill_keys( |
163: | [\T_WHITESPACE, \T_OPEN_TAG, \T_COMMENT, \T_DOC_COMMENT, \T_BAD_CHARACTER], true |
164: | ); |
165: | } |
166: | |
167: | |
168: | |
169: | |
170: | |
171: | |
172: | |
173: | |
174: | |
175: | |
176: | |
177: | |
178: | |
179: | |
180: | public function parse(string $code, ?ErrorHandler $errorHandler = null): ?array { |
181: | $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing(); |
182: | $this->createdArrays = new \SplObjectStorage(); |
183: | |
184: | $this->tokens = $this->lexer->tokenize($code, $this->errorHandler); |
185: | $result = $this->doParse(); |
186: | |
187: | |
188: | |
189: | |
190: | foreach ($this->createdArrays as $node) { |
191: | foreach ($node->items as $item) { |
192: | if ($item->value instanceof Expr\Error) { |
193: | $this->errorHandler->handleError( |
194: | new Error('Cannot use empty array elements in arrays', $item->getAttributes())); |
195: | } |
196: | } |
197: | } |
198: | |
199: | |
200: | |
201: | $this->tokenStartStack = []; |
202: | $this->tokenEndStack = []; |
203: | $this->semStack = []; |
204: | $this->semValue = null; |
205: | $this->createdArrays = null; |
206: | |
207: | if ($result !== null) { |
208: | $traverser = new NodeTraverser(new CommentAnnotatingVisitor($this->tokens)); |
209: | $traverser->traverse($result); |
210: | } |
211: | |
212: | return $result; |
213: | } |
214: | |
215: | public function getTokens(): array { |
216: | return $this->tokens; |
217: | } |
218: | |
219: | |
220: | protected function doParse(): ?array { |
221: | |
222: | $symbol = self::SYMBOL_NONE; |
223: | $tokenValue = null; |
224: | $this->tokenPos = -1; |
225: | |
226: | |
227: | $this->tokenStartStack = []; |
228: | $this->tokenEndStack = [0]; |
229: | |
230: | |
231: | $state = 0; |
232: | $stateStack = [$state]; |
233: | |
234: | |
235: | $this->semStack = []; |
236: | |
237: | |
238: | $stackPos = 0; |
239: | |
240: | $this->errorState = 0; |
241: | |
242: | for (;;) { |
243: | |
244: | |
245: | if ($this->actionBase[$state] === 0) { |
246: | $rule = $this->actionDefault[$state]; |
247: | } else { |
248: | if ($symbol === self::SYMBOL_NONE) { |
249: | do { |
250: | $token = $this->tokens[++$this->tokenPos]; |
251: | $tokenId = $token->id; |
252: | } while (isset($this->dropTokens[$tokenId])); |
253: | |
254: | |
255: | $tokenValue = $token->text; |
256: | if (!isset($this->phpTokenToSymbol[$tokenId])) { |
257: | throw new \RangeException(sprintf( |
258: | 'The lexer returned an invalid token (id=%d, value=%s)', |
259: | $tokenId, $tokenValue |
260: | )); |
261: | } |
262: | $symbol = $this->phpTokenToSymbol[$tokenId]; |
263: | |
264: | |
265: | } |
266: | |
267: | $idx = $this->actionBase[$state] + $symbol; |
268: | if ((($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol) |
269: | || ($state < $this->YY2TBLSTATE |
270: | && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0 |
271: | && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol)) |
272: | && ($action = $this->action[$idx]) !== $this->defaultAction) { |
273: | |
274: | |
275: | |
276: | |
277: | |
278: | |
279: | |
280: | if ($action > 0) { |
281: | |
282: | |
283: | |
284: | ++$stackPos; |
285: | $stateStack[$stackPos] = $state = $action; |
286: | $this->semStack[$stackPos] = $tokenValue; |
287: | $this->tokenStartStack[$stackPos] = $this->tokenPos; |
288: | $this->tokenEndStack[$stackPos] = $this->tokenPos; |
289: | $symbol = self::SYMBOL_NONE; |
290: | |
291: | if ($this->errorState) { |
292: | --$this->errorState; |
293: | } |
294: | |
295: | if ($action < $this->numNonLeafStates) { |
296: | continue; |
297: | } |
298: | |
299: | |
300: | $rule = $action - $this->numNonLeafStates; |
301: | } else { |
302: | $rule = -$action; |
303: | } |
304: | } else { |
305: | $rule = $this->actionDefault[$state]; |
306: | } |
307: | } |
308: | |
309: | for (;;) { |
310: | if ($rule === 0) { |
311: | |
312: | |
313: | return $this->semValue; |
314: | } |
315: | if ($rule !== $this->unexpectedTokenRule) { |
316: | |
317: | |
318: | |
319: | $ruleLength = $this->ruleToLength[$rule]; |
320: | try { |
321: | $callback = $this->reduceCallbacks[$rule]; |
322: | if ($callback !== null) { |
323: | $callback($this, $stackPos); |
324: | } elseif ($ruleLength > 0) { |
325: | $this->semValue = $this->semStack[$stackPos - $ruleLength + 1]; |
326: | } |
327: | } catch (Error $e) { |
328: | if (-1 === $e->getStartLine()) { |
329: | $e->setStartLine($this->tokens[$this->tokenPos]->line); |
330: | } |
331: | |
332: | $this->emitError($e); |
333: | |
334: | return null; |
335: | } |
336: | |
337: | |
338: | $lastTokenEnd = $this->tokenEndStack[$stackPos]; |
339: | $stackPos -= $ruleLength; |
340: | $nonTerminal = $this->ruleToNonTerminal[$rule]; |
341: | $idx = $this->gotoBase[$nonTerminal] + $stateStack[$stackPos]; |
342: | if ($idx >= 0 && $idx < $this->gotoTableSize && $this->gotoCheck[$idx] === $nonTerminal) { |
343: | $state = $this->goto[$idx]; |
344: | } else { |
345: | $state = $this->gotoDefault[$nonTerminal]; |
346: | } |
347: | |
348: | ++$stackPos; |
349: | $stateStack[$stackPos] = $state; |
350: | $this->semStack[$stackPos] = $this->semValue; |
351: | $this->tokenEndStack[$stackPos] = $lastTokenEnd; |
352: | if ($ruleLength === 0) { |
353: | |
354: | $this->tokenStartStack[$stackPos] = $this->tokenPos; |
355: | } |
356: | } else { |
357: | |
358: | switch ($this->errorState) { |
359: | case 0: |
360: | $msg = $this->getErrorMessage($symbol, $state); |
361: | $this->emitError(new Error($msg, $this->getAttributesForToken($this->tokenPos))); |
362: | |
363: | |
364: | case 1: |
365: | case 2: |
366: | $this->errorState = 3; |
367: | |
368: | |
369: | while (!( |
370: | (($idx = $this->actionBase[$state] + $this->errorSymbol) >= 0 |
371: | && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol) |
372: | || ($state < $this->YY2TBLSTATE |
373: | && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $this->errorSymbol) >= 0 |
374: | && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol) |
375: | ) || ($action = $this->action[$idx]) === $this->defaultAction) { |
376: | if ($stackPos <= 0) { |
377: | |
378: | return null; |
379: | } |
380: | $state = $stateStack[--$stackPos]; |
381: | |
382: | } |
383: | |
384: | |
385: | ++$stackPos; |
386: | $stateStack[$stackPos] = $state = $action; |
387: | |
388: | |
389: | |
390: | $this->tokenStartStack[$stackPos] = $this->tokenPos; |
391: | $this->tokenEndStack[$stackPos] = $this->tokenEndStack[$stackPos - 1]; |
392: | break; |
393: | |
394: | case 3: |
395: | if ($symbol === 0) { |
396: | |
397: | return null; |
398: | } |
399: | |
400: | |
401: | $symbol = self::SYMBOL_NONE; |
402: | break 2; |
403: | } |
404: | } |
405: | |
406: | if ($state < $this->numNonLeafStates) { |
407: | break; |
408: | } |
409: | |
410: | |
411: | $rule = $state - $this->numNonLeafStates; |
412: | } |
413: | } |
414: | |
415: | throw new \RuntimeException('Reached end of parser loop'); |
416: | } |
417: | |
418: | protected function emitError(Error $error): void { |
419: | $this->errorHandler->handleError($error); |
420: | } |
421: | |
422: | |
423: | |
424: | |
425: | |
426: | |
427: | |
428: | |
429: | |
430: | protected function getErrorMessage(int $symbol, int $state): string { |
431: | $expectedString = ''; |
432: | if ($expected = $this->getExpectedTokens($state)) { |
433: | $expectedString = ', expecting ' . implode(' or ', $expected); |
434: | } |
435: | |
436: | return 'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString; |
437: | } |
438: | |
439: | |
440: | |
441: | |
442: | |
443: | |
444: | |
445: | |
446: | protected function getExpectedTokens(int $state): array { |
447: | $expected = []; |
448: | |
449: | $base = $this->actionBase[$state]; |
450: | foreach ($this->symbolToName as $symbol => $name) { |
451: | $idx = $base + $symbol; |
452: | if ($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol |
453: | || $state < $this->YY2TBLSTATE |
454: | && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0 |
455: | && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol |
456: | ) { |
457: | if ($this->action[$idx] !== $this->unexpectedTokenRule |
458: | && $this->action[$idx] !== $this->defaultAction |
459: | && $symbol !== $this->errorSymbol |
460: | ) { |
461: | if (count($expected) === 4) { |
462: | |
463: | return []; |
464: | } |
465: | |
466: | $expected[] = $name; |
467: | } |
468: | } |
469: | } |
470: | |
471: | return $expected; |
472: | } |
473: | |
474: | |
475: | |
476: | |
477: | |
478: | |
479: | |
480: | |
481: | protected function getAttributes(int $tokenStartPos, int $tokenEndPos): array { |
482: | $startToken = $this->tokens[$tokenStartPos]; |
483: | $afterEndToken = $this->tokens[$tokenEndPos + 1]; |
484: | return [ |
485: | 'startLine' => $startToken->line, |
486: | 'startTokenPos' => $tokenStartPos, |
487: | 'startFilePos' => $startToken->pos, |
488: | 'endLine' => $afterEndToken->line, |
489: | 'endTokenPos' => $tokenEndPos, |
490: | 'endFilePos' => $afterEndToken->pos - 1, |
491: | ]; |
492: | } |
493: | |
494: | |
495: | |
496: | |
497: | |
498: | |
499: | protected function getAttributesForToken(int $tokenPos): array { |
500: | if ($tokenPos < \count($this->tokens) - 1) { |
501: | return $this->getAttributes($tokenPos, $tokenPos); |
502: | } |
503: | |
504: | |
505: | $token = $this->tokens[$tokenPos]; |
506: | return [ |
507: | 'startLine' => $token->line, |
508: | 'startTokenPos' => $tokenPos, |
509: | 'startFilePos' => $token->pos, |
510: | 'endLine' => $token->line, |
511: | 'endTokenPos' => $tokenPos, |
512: | 'endFilePos' => $token->pos, |
513: | ]; |
514: | } |
515: | |
516: | |
517: | |
518: | |
519: | |
520: | |
521: | |
522: | |
523: | |
524: | |
525: | |
526: | |
527: | |
528: | |
529: | |
530: | |
531: | |
532: | |
533: | |
534: | |
535: | |
536: | |
537: | |
538: | |
539: | |
540: | |
541: | |
542: | |
543: | |
544: | |
545: | |
546: | |
547: | |
548: | |
549: | |
550: | |
551: | |
552: | |
553: | |
554: | |
555: | |
556: | |
557: | |
558: | |
559: | |
560: | |
561: | protected function handleNamespaces(array $stmts): array { |
562: | $hasErrored = false; |
563: | $style = $this->getNamespacingStyle($stmts); |
564: | if (null === $style) { |
565: | |
566: | return $stmts; |
567: | } |
568: | if ('brace' === $style) { |
569: | |
570: | $afterFirstNamespace = false; |
571: | foreach ($stmts as $stmt) { |
572: | if ($stmt instanceof Node\Stmt\Namespace_) { |
573: | $afterFirstNamespace = true; |
574: | } elseif (!$stmt instanceof Node\Stmt\HaltCompiler |
575: | && !$stmt instanceof Node\Stmt\Nop |
576: | && $afterFirstNamespace && !$hasErrored) { |
577: | $this->emitError(new Error( |
578: | 'No code may exist outside of namespace {}', $stmt->getAttributes())); |
579: | $hasErrored = true; |
580: | } |
581: | } |
582: | return $stmts; |
583: | } else { |
584: | |
585: | $resultStmts = []; |
586: | $targetStmts = &$resultStmts; |
587: | $lastNs = null; |
588: | foreach ($stmts as $stmt) { |
589: | if ($stmt instanceof Node\Stmt\Namespace_) { |
590: | if ($lastNs !== null) { |
591: | $this->fixupNamespaceAttributes($lastNs); |
592: | } |
593: | if ($stmt->stmts === null) { |
594: | $stmt->stmts = []; |
595: | $targetStmts = &$stmt->stmts; |
596: | $resultStmts[] = $stmt; |
597: | } else { |
598: | |
599: | $resultStmts[] = $stmt; |
600: | $targetStmts = &$resultStmts; |
601: | } |
602: | $lastNs = $stmt; |
603: | } elseif ($stmt instanceof Node\Stmt\HaltCompiler) { |
604: | |
605: | $resultStmts[] = $stmt; |
606: | } else { |
607: | $targetStmts[] = $stmt; |
608: | } |
609: | } |
610: | if ($lastNs !== null) { |
611: | $this->fixupNamespaceAttributes($lastNs); |
612: | } |
613: | return $resultStmts; |
614: | } |
615: | } |
616: | |
617: | private function fixupNamespaceAttributes(Node\Stmt\Namespace_ $stmt): void { |
618: | |
619: | |
620: | if (empty($stmt->stmts)) { |
621: | return; |
622: | } |
623: | |
624: | |
625: | |
626: | $endAttributes = ['endLine', 'endFilePos', 'endTokenPos']; |
627: | $lastStmt = $stmt->stmts[count($stmt->stmts) - 1]; |
628: | foreach ($endAttributes as $endAttribute) { |
629: | if ($lastStmt->hasAttribute($endAttribute)) { |
630: | $stmt->setAttribute($endAttribute, $lastStmt->getAttribute($endAttribute)); |
631: | } |
632: | } |
633: | } |
634: | |
635: | |
636: | private function getNamespaceErrorAttributes(Namespace_ $node): array { |
637: | $attrs = $node->getAttributes(); |
638: | |
639: | if (isset($attrs['startLine'])) { |
640: | $attrs['endLine'] = $attrs['startLine']; |
641: | } |
642: | if (isset($attrs['startTokenPos'])) { |
643: | $attrs['endTokenPos'] = $attrs['startTokenPos']; |
644: | } |
645: | if (isset($attrs['startFilePos'])) { |
646: | $attrs['endFilePos'] = $attrs['startFilePos'] + \strlen('namespace') - 1; |
647: | } |
648: | return $attrs; |
649: | } |
650: | |
651: | |
652: | |
653: | |
654: | |
655: | |
656: | |
657: | |
658: | private function getNamespacingStyle(array $stmts): ?string { |
659: | $style = null; |
660: | $hasNotAllowedStmts = false; |
661: | foreach ($stmts as $i => $stmt) { |
662: | if ($stmt instanceof Node\Stmt\Namespace_) { |
663: | $currentStyle = null === $stmt->stmts ? 'semicolon' : 'brace'; |
664: | if (null === $style) { |
665: | $style = $currentStyle; |
666: | if ($hasNotAllowedStmts) { |
667: | $this->emitError(new Error( |
668: | 'Namespace declaration statement has to be the very first statement in the script', |
669: | $this->getNamespaceErrorAttributes($stmt) |
670: | )); |
671: | } |
672: | } elseif ($style !== $currentStyle) { |
673: | $this->emitError(new Error( |
674: | 'Cannot mix bracketed namespace declarations with unbracketed namespace declarations', |
675: | $this->getNamespaceErrorAttributes($stmt) |
676: | )); |
677: | |
678: | return 'semicolon'; |
679: | } |
680: | continue; |
681: | } |
682: | |
683: | |
684: | if ($stmt instanceof Node\Stmt\Declare_ |
685: | || $stmt instanceof Node\Stmt\HaltCompiler |
686: | || $stmt instanceof Node\Stmt\Nop) { |
687: | continue; |
688: | } |
689: | |
690: | |
691: | if ($i === 0 && $stmt instanceof Node\Stmt\InlineHTML && preg_match('/\A#!.*\r?\n\z/', $stmt->value)) { |
692: | continue; |
693: | } |
694: | |
695: | |
696: | $hasNotAllowedStmts = true; |
697: | } |
698: | return $style; |
699: | } |
700: | |
701: | |
702: | protected function handleBuiltinTypes(Name $name) { |
703: | if (!$name->isUnqualified()) { |
704: | return $name; |
705: | } |
706: | |
707: | $lowerName = $name->toLowerString(); |
708: | if (!$this->phpVersion->supportsBuiltinType($lowerName)) { |
709: | return $name; |
710: | } |
711: | |
712: | return new Node\Identifier($lowerName, $name->getAttributes()); |
713: | } |
714: | |
715: | |
716: | |
717: | |
718: | |
719: | |
720: | |
721: | |
722: | protected function getAttributesAt(int $stackPos): array { |
723: | return $this->getAttributes($this->tokenStartStack[$stackPos], $this->tokenEndStack[$stackPos]); |
724: | } |
725: | |
726: | protected function getFloatCastKind(string $cast): int { |
727: | $cast = strtolower($cast); |
728: | if (strpos($cast, 'float') !== false) { |
729: | return Double::KIND_FLOAT; |
730: | } |
731: | |
732: | if (strpos($cast, 'real') !== false) { |
733: | return Double::KIND_REAL; |
734: | } |
735: | |
736: | return Double::KIND_DOUBLE; |
737: | } |
738: | |
739: | |
740: | protected function parseLNumber(string $str, array $attributes, bool $allowInvalidOctal = false): Int_ { |
741: | try { |
742: | return Int_::fromString($str, $attributes, $allowInvalidOctal); |
743: | } catch (Error $error) { |
744: | $this->emitError($error); |
745: | |
746: | return new Int_(0, $attributes); |
747: | } |
748: | } |
749: | |
750: | |
751: | |
752: | |
753: | |
754: | |
755: | |
756: | |
757: | |
758: | protected function parseNumString(string $str, array $attributes) { |
759: | if (!preg_match('/^(?:0|-?[1-9][0-9]*)$/', $str)) { |
760: | return new String_($str, $attributes); |
761: | } |
762: | |
763: | $num = +$str; |
764: | if (!is_int($num)) { |
765: | return new String_($str, $attributes); |
766: | } |
767: | |
768: | return new Int_($num, $attributes); |
769: | } |
770: | |
771: | |
772: | protected function stripIndentation( |
773: | string $string, int $indentLen, string $indentChar, |
774: | bool $newlineAtStart, bool $newlineAtEnd, array $attributes |
775: | ): string { |
776: | if ($indentLen === 0) { |
777: | return $string; |
778: | } |
779: | |
780: | $start = $newlineAtStart ? '(?:(?<=\n)|\A)' : '(?<=\n)'; |
781: | $end = $newlineAtEnd ? '(?:(?=[\r\n])|\z)' : '(?=[\r\n])'; |
782: | $regex = '/' . $start . '([ \t]*)(' . $end . ')?/'; |
783: | return preg_replace_callback( |
784: | $regex, |
785: | function ($matches) use ($indentLen, $indentChar, $attributes) { |
786: | $prefix = substr($matches[1], 0, $indentLen); |
787: | if (false !== strpos($prefix, $indentChar === " " ? "\t" : " ")) { |
788: | $this->emitError(new Error( |
789: | 'Invalid indentation - tabs and spaces cannot be mixed', $attributes |
790: | )); |
791: | } elseif (strlen($prefix) < $indentLen && !isset($matches[2])) { |
792: | $this->emitError(new Error( |
793: | 'Invalid body indentation level ' . |
794: | '(expecting an indentation level of at least ' . $indentLen . ')', |
795: | $attributes |
796: | )); |
797: | } |
798: | return substr($matches[0], strlen($prefix)); |
799: | }, |
800: | $string |
801: | ); |
802: | } |
803: | |
804: | |
805: | |
806: | |
807: | |
808: | |
809: | protected function parseDocString( |
810: | string $startToken, $contents, string $endToken, |
811: | array $attributes, array $endTokenAttributes, bool $parseUnicodeEscape |
812: | ): Expr { |
813: | $kind = strpos($startToken, "'") === false |
814: | ? String_::KIND_HEREDOC : String_::KIND_NOWDOC; |
815: | |
816: | $regex = '/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/'; |
817: | $result = preg_match($regex, $startToken, $matches); |
818: | assert($result === 1); |
819: | $label = $matches[1]; |
820: | |
821: | $result = preg_match('/\A[ \t]*/', $endToken, $matches); |
822: | assert($result === 1); |
823: | $indentation = $matches[0]; |
824: | |
825: | $attributes['kind'] = $kind; |
826: | $attributes['docLabel'] = $label; |
827: | $attributes['docIndentation'] = $indentation; |
828: | |
829: | $indentHasSpaces = false !== strpos($indentation, " "); |
830: | $indentHasTabs = false !== strpos($indentation, "\t"); |
831: | if ($indentHasSpaces && $indentHasTabs) { |
832: | $this->emitError(new Error( |
833: | 'Invalid indentation - tabs and spaces cannot be mixed', |
834: | $endTokenAttributes |
835: | )); |
836: | |
837: | |
838: | $indentation = ''; |
839: | } |
840: | |
841: | $indentLen = \strlen($indentation); |
842: | $indentChar = $indentHasSpaces ? " " : "\t"; |
843: | |
844: | if (\is_string($contents)) { |
845: | if ($contents === '') { |
846: | $attributes['rawValue'] = $contents; |
847: | return new String_('', $attributes); |
848: | } |
849: | |
850: | $contents = $this->stripIndentation( |
851: | $contents, $indentLen, $indentChar, true, true, $attributes |
852: | ); |
853: | $contents = preg_replace('~(\r\n|\n|\r)\z~', '', $contents); |
854: | $attributes['rawValue'] = $contents; |
855: | |
856: | if ($kind === String_::KIND_HEREDOC) { |
857: | $contents = String_::parseEscapeSequences($contents, null, $parseUnicodeEscape); |
858: | } |
859: | |
860: | return new String_($contents, $attributes); |
861: | } else { |
862: | assert(count($contents) > 0); |
863: | if (!$contents[0] instanceof Node\InterpolatedStringPart) { |
864: | |
865: | $this->stripIndentation( |
866: | '', $indentLen, $indentChar, true, false, $contents[0]->getAttributes() |
867: | ); |
868: | } |
869: | |
870: | $newContents = []; |
871: | foreach ($contents as $i => $part) { |
872: | if ($part instanceof Node\InterpolatedStringPart) { |
873: | $isLast = $i === \count($contents) - 1; |
874: | $part->value = $this->stripIndentation( |
875: | $part->value, $indentLen, $indentChar, |
876: | $i === 0, $isLast, $part->getAttributes() |
877: | ); |
878: | if ($isLast) { |
879: | $part->value = preg_replace('~(\r\n|\n|\r)\z~', '', $part->value); |
880: | } |
881: | $part->setAttribute('rawValue', $part->value); |
882: | $part->value = String_::parseEscapeSequences($part->value, null, $parseUnicodeEscape); |
883: | if ('' === $part->value) { |
884: | continue; |
885: | } |
886: | } |
887: | $newContents[] = $part; |
888: | } |
889: | return new InterpolatedString($newContents, $attributes); |
890: | } |
891: | } |
892: | |
893: | protected function createCommentFromToken(Token $token, int $tokenPos): Comment { |
894: | assert($token->id === \T_COMMENT || $token->id == \T_DOC_COMMENT); |
895: | return \T_DOC_COMMENT === $token->id |
896: | ? new Comment\Doc($token->text, $token->line, $token->pos, $tokenPos, |
897: | $token->getEndLine(), $token->getEndPos() - 1, $tokenPos) |
898: | : new Comment($token->text, $token->line, $token->pos, $tokenPos, |
899: | $token->getEndLine(), $token->getEndPos() - 1, $tokenPos); |
900: | } |
901: | |
902: | |
903: | |
904: | |
905: | protected function getCommentBeforeToken(int $tokenPos): ?Comment { |
906: | while (--$tokenPos >= 0) { |
907: | $token = $this->tokens[$tokenPos]; |
908: | if (!isset($this->dropTokens[$token->id])) { |
909: | break; |
910: | } |
911: | |
912: | if ($token->id === \T_COMMENT || $token->id === \T_DOC_COMMENT) { |
913: | return $this->createCommentFromToken($token, $tokenPos); |
914: | } |
915: | } |
916: | return null; |
917: | } |
918: | |
919: | |
920: | |
921: | |
922: | protected function maybeCreateZeroLengthNop(int $tokenPos): ?Nop { |
923: | $comment = $this->getCommentBeforeToken($tokenPos); |
924: | if ($comment === null) { |
925: | return null; |
926: | } |
927: | |
928: | $commentEndLine = $comment->getEndLine(); |
929: | $commentEndFilePos = $comment->getEndFilePos(); |
930: | $commentEndTokenPos = $comment->getEndTokenPos(); |
931: | $attributes = [ |
932: | 'startLine' => $commentEndLine, |
933: | 'endLine' => $commentEndLine, |
934: | 'startFilePos' => $commentEndFilePos + 1, |
935: | 'endFilePos' => $commentEndFilePos, |
936: | 'startTokenPos' => $commentEndTokenPos + 1, |
937: | 'endTokenPos' => $commentEndTokenPos, |
938: | ]; |
939: | return new Nop($attributes); |
940: | } |
941: | |
942: | protected function maybeCreateNop(int $tokenStartPos, int $tokenEndPos): ?Nop { |
943: | if ($this->getCommentBeforeToken($tokenStartPos) === null) { |
944: | return null; |
945: | } |
946: | return new Nop($this->getAttributes($tokenStartPos, $tokenEndPos)); |
947: | } |
948: | |
949: | protected function handleHaltCompiler(): string { |
950: | |
951: | $nextToken = $this->tokens[$this->tokenPos + 1]; |
952: | $this->tokenPos = \count($this->tokens) - 2; |
953: | |
954: | |
955: | return $nextToken->id === \T_INLINE_HTML ? $nextToken->text : ''; |
956: | } |
957: | |
958: | protected function inlineHtmlHasLeadingNewline(int $stackPos): bool { |
959: | $tokenPos = $this->tokenStartStack[$stackPos]; |
960: | $token = $this->tokens[$tokenPos]; |
961: | assert($token->id == \T_INLINE_HTML); |
962: | if ($tokenPos > 0) { |
963: | $prevToken = $this->tokens[$tokenPos - 1]; |
964: | assert($prevToken->id == \T_CLOSE_TAG); |
965: | return false !== strpos($prevToken->text, "\n") |
966: | || false !== strpos($prevToken->text, "\r"); |
967: | } |
968: | return true; |
969: | } |
970: | |
971: | |
972: | |
973: | |
974: | protected function createEmptyElemAttributes(int $tokenPos): array { |
975: | return $this->getAttributesForToken($tokenPos); |
976: | } |
977: | |
978: | protected function fixupArrayDestructuring(Array_ $node): Expr\List_ { |
979: | $this->createdArrays->detach($node); |
980: | return new Expr\List_(array_map(function (Node\ArrayItem $item) { |
981: | if ($item->value instanceof Expr\Error) { |
982: | |
983: | return null; |
984: | } |
985: | if ($item->value instanceof Array_) { |
986: | return new Node\ArrayItem( |
987: | $this->fixupArrayDestructuring($item->value), |
988: | $item->key, $item->byRef, $item->getAttributes()); |
989: | } |
990: | return $item; |
991: | }, $node->items), ['kind' => Expr\List_::KIND_ARRAY] + $node->getAttributes()); |
992: | } |
993: | |
994: | protected function postprocessList(Expr\List_ $node): void { |
995: | foreach ($node->items as $i => $item) { |
996: | if ($item->value instanceof Expr\Error) { |
997: | |
998: | $node->items[$i] = null; |
999: | } |
1000: | } |
1001: | } |
1002: | |
1003: | |
1004: | protected function fixupAlternativeElse($node): void { |
1005: | |
1006: | $numStmts = \count($node->stmts); |
1007: | if ($numStmts !== 0 && $node->stmts[$numStmts - 1] instanceof Nop) { |
1008: | $nopAttrs = $node->stmts[$numStmts - 1]->getAttributes(); |
1009: | if (isset($nopAttrs['endLine'])) { |
1010: | $node->setAttribute('endLine', $nopAttrs['endLine']); |
1011: | } |
1012: | if (isset($nopAttrs['endFilePos'])) { |
1013: | $node->setAttribute('endFilePos', $nopAttrs['endFilePos']); |
1014: | } |
1015: | if (isset($nopAttrs['endTokenPos'])) { |
1016: | $node->setAttribute('endTokenPos', $nopAttrs['endTokenPos']); |
1017: | } |
1018: | } |
1019: | } |
1020: | |
1021: | protected function checkClassModifier(int $a, int $b, int $modifierPos): void { |
1022: | try { |
1023: | Modifiers::verifyClassModifier($a, $b); |
1024: | } catch (Error $error) { |
1025: | $error->setAttributes($this->getAttributesAt($modifierPos)); |
1026: | $this->emitError($error); |
1027: | } |
1028: | } |
1029: | |
1030: | protected function checkModifier(int $a, int $b, int $modifierPos): void { |
1031: | |
1032: | try { |
1033: | Modifiers::verifyModifier($a, $b); |
1034: | } catch (Error $error) { |
1035: | $error->setAttributes($this->getAttributesAt($modifierPos)); |
1036: | $this->emitError($error); |
1037: | } |
1038: | } |
1039: | |
1040: | protected function checkParam(Param $node): void { |
1041: | if ($node->variadic && null !== $node->default) { |
1042: | $this->emitError(new Error( |
1043: | 'Variadic parameter cannot have a default value', |
1044: | $node->default->getAttributes() |
1045: | )); |
1046: | } |
1047: | } |
1048: | |
1049: | protected function checkTryCatch(TryCatch $node): void { |
1050: | if (empty($node->catches) && null === $node->finally) { |
1051: | $this->emitError(new Error( |
1052: | 'Cannot use try without catch or finally', $node->getAttributes() |
1053: | )); |
1054: | } |
1055: | } |
1056: | |
1057: | protected function checkNamespace(Namespace_ $node): void { |
1058: | if (null !== $node->stmts) { |
1059: | foreach ($node->stmts as $stmt) { |
1060: | if ($stmt instanceof Namespace_) { |
1061: | $this->emitError(new Error( |
1062: | 'Namespace declarations cannot be nested', $stmt->getAttributes() |
1063: | )); |
1064: | } |
1065: | } |
1066: | } |
1067: | } |
1068: | |
1069: | private function checkClassName(?Identifier $name, int $namePos): void { |
1070: | if (null !== $name && $name->isSpecialClassName()) { |
1071: | $this->emitError(new Error( |
1072: | sprintf('Cannot use \'%s\' as class name as it is reserved', $name), |
1073: | $this->getAttributesAt($namePos) |
1074: | )); |
1075: | } |
1076: | } |
1077: | |
1078: | |
1079: | private function checkImplementedInterfaces(array $interfaces): void { |
1080: | foreach ($interfaces as $interface) { |
1081: | if ($interface->isSpecialClassName()) { |
1082: | $this->emitError(new Error( |
1083: | sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface), |
1084: | $interface->getAttributes() |
1085: | )); |
1086: | } |
1087: | } |
1088: | } |
1089: | |
1090: | protected function checkClass(Class_ $node, int $namePos): void { |
1091: | $this->checkClassName($node->name, $namePos); |
1092: | |
1093: | if ($node->extends && $node->extends->isSpecialClassName()) { |
1094: | $this->emitError(new Error( |
1095: | sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends), |
1096: | $node->extends->getAttributes() |
1097: | )); |
1098: | } |
1099: | |
1100: | $this->checkImplementedInterfaces($node->implements); |
1101: | } |
1102: | |
1103: | protected function checkInterface(Interface_ $node, int $namePos): void { |
1104: | $this->checkClassName($node->name, $namePos); |
1105: | $this->checkImplementedInterfaces($node->extends); |
1106: | } |
1107: | |
1108: | protected function checkEnum(Enum_ $node, int $namePos): void { |
1109: | $this->checkClassName($node->name, $namePos); |
1110: | $this->checkImplementedInterfaces($node->implements); |
1111: | } |
1112: | |
1113: | protected function checkClassMethod(ClassMethod $node, int $modifierPos): void { |
1114: | if ($node->flags & Modifiers::STATIC) { |
1115: | switch ($node->name->toLowerString()) { |
1116: | case '__construct': |
1117: | $this->emitError(new Error( |
1118: | sprintf('Constructor %s() cannot be static', $node->name), |
1119: | $this->getAttributesAt($modifierPos))); |
1120: | break; |
1121: | case '__destruct': |
1122: | $this->emitError(new Error( |
1123: | sprintf('Destructor %s() cannot be static', $node->name), |
1124: | $this->getAttributesAt($modifierPos))); |
1125: | break; |
1126: | case '__clone': |
1127: | $this->emitError(new Error( |
1128: | sprintf('Clone method %s() cannot be static', $node->name), |
1129: | $this->getAttributesAt($modifierPos))); |
1130: | break; |
1131: | } |
1132: | } |
1133: | |
1134: | if ($node->flags & Modifiers::READONLY) { |
1135: | $this->emitError(new Error( |
1136: | sprintf('Method %s() cannot be readonly', $node->name), |
1137: | $this->getAttributesAt($modifierPos))); |
1138: | } |
1139: | } |
1140: | |
1141: | protected function checkClassConst(ClassConst $node, int $modifierPos): void { |
1142: | foreach ([Modifiers::STATIC, Modifiers::ABSTRACT, Modifiers::READONLY] as $modifier) { |
1143: | if ($node->flags & $modifier) { |
1144: | $this->emitError(new Error( |
1145: | "Cannot use '" . Modifiers::toString($modifier) . "' as constant modifier", |
1146: | $this->getAttributesAt($modifierPos))); |
1147: | } |
1148: | } |
1149: | } |
1150: | |
1151: | protected function checkUseUse(UseItem $node, int $namePos): void { |
1152: | if ($node->alias && $node->alias->isSpecialClassName()) { |
1153: | $this->emitError(new Error( |
1154: | sprintf( |
1155: | 'Cannot use %s as %s because \'%2$s\' is a special class name', |
1156: | $node->name, $node->alias |
1157: | ), |
1158: | $this->getAttributesAt($namePos) |
1159: | )); |
1160: | } |
1161: | } |
1162: | |
1163: | |
1164: | protected function checkPropertyHookList(array $hooks, int $hookPos): void { |
1165: | if (empty($hooks)) { |
1166: | $this->emitError(new Error( |
1167: | 'Property hook list cannot be empty', $this->getAttributesAt($hookPos))); |
1168: | } |
1169: | } |
1170: | |
1171: | protected function checkPropertyHook(PropertyHook $hook, ?int $paramListPos): void { |
1172: | $name = $hook->name->toLowerString(); |
1173: | if ($name !== 'get' && $name !== 'set') { |
1174: | $this->emitError(new Error( |
1175: | 'Unknown hook "' . $hook->name . '", expected "get" or "set"', |
1176: | $hook->name->getAttributes())); |
1177: | } |
1178: | if ($name === 'get' && $paramListPos !== null) { |
1179: | $this->emitError(new Error( |
1180: | 'get hook must not have a parameter list', $this->getAttributesAt($paramListPos))); |
1181: | } |
1182: | } |
1183: | |
1184: | protected function checkPropertyHookModifiers(int $a, int $b, int $modifierPos): void { |
1185: | try { |
1186: | Modifiers::verifyModifier($a, $b); |
1187: | } catch (Error $error) { |
1188: | $error->setAttributes($this->getAttributesAt($modifierPos)); |
1189: | $this->emitError($error); |
1190: | } |
1191: | |
1192: | if ($b != Modifiers::FINAL) { |
1193: | $this->emitError(new Error( |
1194: | 'Cannot use the ' . Modifiers::toString($b) . ' modifier on a property hook', |
1195: | $this->getAttributesAt($modifierPos))); |
1196: | } |
1197: | } |
1198: | |
1199: | |
1200: | private function isSimpleExit(array $args): bool { |
1201: | if (\count($args) === 0) { |
1202: | return true; |
1203: | } |
1204: | if (\count($args) === 1) { |
1205: | $arg = $args[0]; |
1206: | return $arg instanceof Arg && $arg->name === null && |
1207: | $arg->byRef === false && $arg->unpack === false; |
1208: | } |
1209: | return false; |
1210: | } |
1211: | |
1212: | |
1213: | |
1214: | |
1215: | |
1216: | protected function createExitExpr(string $name, int $namePos, array $args, array $attrs): Expr { |
1217: | if ($this->isSimpleExit($args)) { |
1218: | |
1219: | $attrs['kind'] = strtolower($name) === 'exit' ? Expr\Exit_::KIND_EXIT : Expr\Exit_::KIND_DIE; |
1220: | return new Expr\Exit_(\count($args) === 1 ? $args[0]->value : null, $attrs); |
1221: | } |
1222: | return new Expr\FuncCall(new Name($name, $this->getAttributesAt($namePos)), $args, $attrs); |
1223: | } |
1224: | |
1225: | |
1226: | |
1227: | |
1228: | |
1229: | |
1230: | |
1231: | |
1232: | |
1233: | |
1234: | protected function createTokenMap(): array { |
1235: | $tokenMap = []; |
1236: | |
1237: | |
1238: | for ($i = 0; $i < 256; ++$i) { |
1239: | $tokenMap[$i] = $i; |
1240: | } |
1241: | |
1242: | foreach ($this->symbolToName as $name) { |
1243: | if ($name[0] === 'T') { |
1244: | $tokenMap[\constant($name)] = constant(static::class . '::' . $name); |
1245: | } |
1246: | } |
1247: | |
1248: | |
1249: | $tokenMap[\T_OPEN_TAG_WITH_ECHO] = static::T_ECHO; |
1250: | |
1251: | $tokenMap[\T_CLOSE_TAG] = ord(';'); |
1252: | |
1253: | |
1254: | |
1255: | $fullTokenMap = []; |
1256: | foreach ($tokenMap as $phpToken => $extSymbol) { |
1257: | $intSymbol = $this->tokenToSymbol[$extSymbol]; |
1258: | if ($intSymbol === $this->invalidSymbol) { |
1259: | continue; |
1260: | } |
1261: | $fullTokenMap[$phpToken] = $intSymbol; |
1262: | } |
1263: | |
1264: | return $fullTokenMap; |
1265: | } |
1266: | } |
1267: | |