1: | <?php declare(strict_types=1); |
2: | |
3: | namespace PhpParser; |
4: | |
5: | |
6: | |
7: | |
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: | |
36: | |
37: | |
38: | |
39: | protected $tokenToSymbolMapSize; |
40: | |
41: | protected $actionTableSize; |
42: | |
43: | protected $gotoTableSize; |
44: | |
45: | |
46: | protected $invalidSymbol; |
47: | |
48: | protected $errorSymbol; |
49: | |
50: | protected $defaultAction; |
51: | |
52: | protected $unexpectedTokenRule; |
53: | |
54: | protected $YY2TBLSTATE; |
55: | |
56: | protected $numNonLeafStates; |
57: | |
58: | |
59: | protected $tokenToSymbol; |
60: | |
61: | protected $symbolToName; |
62: | |
63: | protected $productions; |
64: | |
65: | |
66: | |
67: | |
68: | protected $actionBase; |
69: | |
70: | protected $action; |
71: | |
72: | |
73: | protected $actionCheck; |
74: | |
75: | protected $actionDefault; |
76: | |
77: | protected $reduceCallbacks; |
78: | |
79: | |
80: | |
81: | protected $gotoBase; |
82: | |
83: | protected $goto; |
84: | |
85: | |
86: | protected $gotoCheck; |
87: | |
88: | protected $gotoDefault; |
89: | |
90: | |
91: | |
92: | protected $ruleToNonTerminal; |
93: | |
94: | |
95: | protected $ruleToLength; |
96: | |
97: | |
98: | |
99: | |
100: | |
101: | |
102: | protected $lexer; |
103: | |
104: | protected $semValue; |
105: | |
106: | protected $semStack; |
107: | |
108: | protected $startAttributeStack; |
109: | |
110: | protected $endAttributeStack; |
111: | |
112: | protected $endAttributes; |
113: | |
114: | protected $lookaheadStartAttributes; |
115: | |
116: | |
117: | protected $errorHandler; |
118: | |
119: | protected $errorState; |
120: | |
121: | |
122: | |
123: | |
124: | abstract protected function initReduceCallbacks(); |
125: | |
126: | |
127: | |
128: | |
129: | |
130: | |
131: | |
132: | |
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: | |
147: | |
148: | |
149: | |
150: | |
151: | |
152: | |
153: | |
154: | |
155: | |
156: | |
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: | |
165: | |
166: | $this->startAttributeStack = []; |
167: | $this->endAttributeStack = []; |
168: | $this->semStack = []; |
169: | $this->semValue = null; |
170: | |
171: | return $result; |
172: | } |
173: | |
174: | protected function doParse() { |
175: | |
176: | $symbol = self::SYMBOL_NONE; |
177: | |
178: | |
179: | |
180: | |
181: | $startAttributes = []; |
182: | $endAttributes = []; |
183: | $this->endAttributes = $endAttributes; |
184: | |
185: | |
186: | $this->startAttributeStack = []; |
187: | $this->endAttributeStack = [$endAttributes]; |
188: | |
189: | |
190: | $state = 0; |
191: | $stateStack = [$state]; |
192: | |
193: | |
194: | $this->semStack = []; |
195: | |
196: | |
197: | $stackPos = 0; |
198: | |
199: | $this->errorState = 0; |
200: | |
201: | for (;;) { |
202: | |
203: | |
204: | if ($this->actionBase[$state] === 0) { |
205: | $rule = $this->actionDefault[$state]; |
206: | } else { |
207: | if ($symbol === self::SYMBOL_NONE) { |
208: | |
209: | |
210: | |
211: | |
212: | $tokenId = $this->lexer->getNextToken($tokenValue, $startAttributes, $endAttributes); |
213: | |
214: | |
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: | |
227: | $this->lookaheadStartAttributes = $startAttributes; |
228: | |
229: | |
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: | |
240: | |
241: | |
242: | |
243: | |
244: | |
245: | if ($action > 0) { |
246: | |
247: | |
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: | |
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: | |
278: | |
279: | return $this->semValue; |
280: | } elseif ($rule !== $this->unexpectedTokenRule) { |
281: | |
282: | |
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: | |
293: | return null; |
294: | } |
295: | |
296: | |
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: | |
314: | $this->startAttributeStack[$stackPos] = $this->lookaheadStartAttributes; |
315: | } |
316: | } else { |
317: | |
318: | switch ($this->errorState) { |
319: | case 0: |
320: | $msg = $this->getErrorMessage($symbol, $state); |
321: | $this->emitError(new Error($msg, $startAttributes + $endAttributes)); |
322: | |
323: | case 1: |
324: | case 2: |
325: | $this->errorState = 3; |
326: | |
327: | |
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) { |
335: | if ($stackPos <= 0) { |
336: | |
337: | return null; |
338: | } |
339: | $state = $stateStack[--$stackPos]; |
340: | |
341: | } |
342: | |
343: | |
344: | ++$stackPos; |
345: | $stateStack[$stackPos] = $state = $action; |
346: | |
347: | |
348: | |
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: | |
357: | return null; |
358: | } |
359: | |
360: | |
361: | $symbol = self::SYMBOL_NONE; |
362: | break 2; |
363: | } |
364: | } |
365: | |
366: | if ($state < $this->numNonLeafStates) { |
367: | break; |
368: | } |
369: | |
370: | |
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: | |
384: | |
385: | |
386: | |
387: | |
388: | |
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: | |
401: | |
402: | |
403: | |
404: | |
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: | |
423: | return []; |
424: | } |
425: | |
426: | $expected[] = $name; |
427: | } |
428: | } |
429: | } |
430: | |
431: | return $expected; |
432: | } |
433: | |
434: | |
435: | |
436: | |
437: | |
438: | |
439: | |
440: | |
441: | |
442: | |
443: | |
444: | |
445: | |
446: | |
447: | |
448: | |
449: | |
450: | |
451: | |
452: | |
453: | |
454: | |
455: | |
456: | |
457: | |
458: | |
459: | |
460: | |
461: | |
462: | |
463: | |
464: | |
465: | |
466: | |
467: | |
468: | |
469: | |
470: | |
471: | |
472: | |
473: | |
474: | |
475: | |
476: | |
477: | |
478: | |
479: | protected function handleNamespaces(array $stmts) : array { |
480: | $hasErrored = false; |
481: | $style = $this->getNamespacingStyle($stmts); |
482: | if (null === $style) { |
483: | |
484: | return $stmts; |
485: | } elseif ('brace' === $style) { |
486: | |
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; |
497: | } |
498: | } |
499: | return $stmts; |
500: | } else { |
501: | |
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: | |
516: | $resultStmts[] = $stmt; |
517: | $targetStmts =& $resultStmts; |
518: | } |
519: | $lastNs = $stmt; |
520: | } elseif ($stmt instanceof Node\Stmt\HaltCompiler) { |
521: | |
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: | |
536: | |
537: | if (empty($stmt->stmts)) { |
538: | return; |
539: | } |
540: | |
541: | |
542: | |
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: | |
554: | |
555: | |
556: | |
557: | |
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() |
571: | )); |
572: | } |
573: | } elseif ($style !== $currentStyle) { |
574: | $this->emitError(new Error( |
575: | 'Cannot mix bracketed namespace declarations with unbracketed namespace declarations', |
576: | $stmt->getLine() |
577: | )); |
578: | |
579: | return 'semicolon'; |
580: | } |
581: | continue; |
582: | } |
583: | |
584: | |
585: | if ($stmt instanceof Node\Stmt\Declare_ |
586: | || $stmt instanceof Node\Stmt\HaltCompiler |
587: | || $stmt instanceof Node\Stmt\Nop) { |
588: | continue; |
589: | } |
590: | |
591: | |
592: | if ($i === 0 && $stmt instanceof Node\Stmt\InlineHTML && preg_match('/\A#!.*\r?\n\z/', $stmt->value)) { |
593: | continue; |
594: | } |
595: | |
596: | |
597: | $hasNotAllowedStmts = true; |
598: | } |
599: | return $style; |
600: | } |
601: | |
602: | |
603: | |
604: | |
605: | |
606: | |
607: | |
608: | |
609: | |
610: | |
611: | |
612: | |
613: | |
614: | |
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: | |
629: | $staticProp = $tmp->var; |
630: | |
631: | |
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: | |
687: | |
688: | |
689: | |
690: | |
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: | |
716: | return new LNumber(0, $attributes); |
717: | } |
718: | } |
719: | |
720: | |
721: | |
722: | |
723: | |
724: | |
725: | |
726: | |
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: | |
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: | |
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: | |
856: | |
857: | |
858: | |
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: | |
883: | protected function fixupAlternativeElse($node) { |
884: | |
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: | |
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: | |