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\Enum_; |
20: | use PhpParser\Node\Stmt\Interface_; |
21: | use PhpParser\Node\Stmt\Namespace_; |
22: | use PhpParser\Node\Stmt\Property; |
23: | use PhpParser\Node\Stmt\TryCatch; |
24: | use PhpParser\Node\Stmt\UseUse; |
25: | use PhpParser\Node\VarLikeIdentifier; |
26: | |
27: | abstract class ParserAbstract implements Parser |
28: | { |
29: | const SYMBOL_NONE = -1; |
30: | |
31: | |
32: | |
33: | |
34: | |
35: | |
36: | protected $tokenToSymbolMapSize; |
37: | |
38: | protected $actionTableSize; |
39: | |
40: | protected $gotoTableSize; |
41: | |
42: | |
43: | protected $invalidSymbol; |
44: | |
45: | protected $errorSymbol; |
46: | |
47: | protected $defaultAction; |
48: | |
49: | protected $unexpectedTokenRule; |
50: | |
51: | protected $YY2TBLSTATE; |
52: | |
53: | protected $numNonLeafStates; |
54: | |
55: | |
56: | protected $tokenToSymbol; |
57: | |
58: | protected $symbolToName; |
59: | |
60: | protected $productions; |
61: | |
62: | |
63: | |
64: | |
65: | protected $actionBase; |
66: | |
67: | protected $action; |
68: | |
69: | |
70: | protected $actionCheck; |
71: | |
72: | protected $actionDefault; |
73: | |
74: | protected $reduceCallbacks; |
75: | |
76: | |
77: | |
78: | protected $gotoBase; |
79: | |
80: | protected $goto; |
81: | |
82: | |
83: | protected $gotoCheck; |
84: | |
85: | protected $gotoDefault; |
86: | |
87: | |
88: | |
89: | protected $ruleToNonTerminal; |
90: | |
91: | |
92: | protected $ruleToLength; |
93: | |
94: | |
95: | |
96: | |
97: | |
98: | |
99: | protected $lexer; |
100: | |
101: | protected $semValue; |
102: | |
103: | protected $semStack; |
104: | |
105: | protected $startAttributeStack; |
106: | |
107: | protected $endAttributeStack; |
108: | |
109: | protected $endAttributes; |
110: | |
111: | protected $lookaheadStartAttributes; |
112: | |
113: | |
114: | protected $errorHandler; |
115: | |
116: | protected $errorState; |
117: | |
118: | |
119: | |
120: | |
121: | abstract protected function initReduceCallbacks(); |
122: | |
123: | |
124: | |
125: | |
126: | |
127: | |
128: | |
129: | |
130: | |
131: | public function __construct(Lexer $lexer, array $options = []) { |
132: | $this->lexer = $lexer; |
133: | |
134: | if (isset($options['throwOnError'])) { |
135: | throw new \LogicException( |
136: | '"throwOnError" is no longer supported, use "errorHandler" instead'); |
137: | } |
138: | |
139: | $this->initReduceCallbacks(); |
140: | } |
141: | |
142: | |
143: | |
144: | |
145: | |
146: | |
147: | |
148: | |
149: | |
150: | |
151: | |
152: | |
153: | |
154: | |
155: | public function parse(string $code, ErrorHandler $errorHandler = null) { |
156: | $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing; |
157: | |
158: | $this->lexer->startLexing($code, $this->errorHandler); |
159: | $result = $this->doParse(); |
160: | |
161: | |
162: | |
163: | $this->startAttributeStack = []; |
164: | $this->endAttributeStack = []; |
165: | $this->semStack = []; |
166: | $this->semValue = null; |
167: | |
168: | return $result; |
169: | } |
170: | |
171: | protected function doParse() { |
172: | |
173: | $symbol = self::SYMBOL_NONE; |
174: | |
175: | |
176: | |
177: | |
178: | $startAttributes = []; |
179: | $endAttributes = []; |
180: | $this->endAttributes = $endAttributes; |
181: | |
182: | |
183: | $this->startAttributeStack = []; |
184: | $this->endAttributeStack = [$endAttributes]; |
185: | |
186: | |
187: | $state = 0; |
188: | $stateStack = [$state]; |
189: | |
190: | |
191: | $this->semStack = []; |
192: | |
193: | |
194: | $stackPos = 0; |
195: | |
196: | $this->errorState = 0; |
197: | |
198: | for (;;) { |
199: | |
200: | |
201: | if ($this->actionBase[$state] === 0) { |
202: | $rule = $this->actionDefault[$state]; |
203: | } else { |
204: | if ($symbol === self::SYMBOL_NONE) { |
205: | |
206: | |
207: | |
208: | |
209: | $tokenId = $this->lexer->getNextToken($tokenValue, $startAttributes, $endAttributes); |
210: | |
211: | |
212: | $symbol = $tokenId >= 0 && $tokenId < $this->tokenToSymbolMapSize |
213: | ? $this->tokenToSymbol[$tokenId] |
214: | : $this->invalidSymbol; |
215: | |
216: | if ($symbol === $this->invalidSymbol) { |
217: | throw new \RangeException(sprintf( |
218: | 'The lexer returned an invalid token (id=%d, value=%s)', |
219: | $tokenId, $tokenValue |
220: | )); |
221: | } |
222: | |
223: | |
224: | $this->lookaheadStartAttributes = $startAttributes; |
225: | |
226: | |
227: | } |
228: | |
229: | $idx = $this->actionBase[$state] + $symbol; |
230: | if ((($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol) |
231: | || ($state < $this->YY2TBLSTATE |
232: | && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0 |
233: | && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol)) |
234: | && ($action = $this->action[$idx]) !== $this->defaultAction) { |
235: | |
236: | |
237: | |
238: | |
239: | |
240: | |
241: | |
242: | if ($action > 0) { |
243: | |
244: | |
245: | |
246: | ++$stackPos; |
247: | $stateStack[$stackPos] = $state = $action; |
248: | $this->semStack[$stackPos] = $tokenValue; |
249: | $this->startAttributeStack[$stackPos] = $startAttributes; |
250: | $this->endAttributeStack[$stackPos] = $endAttributes; |
251: | $this->endAttributes = $endAttributes; |
252: | $symbol = self::SYMBOL_NONE; |
253: | |
254: | if ($this->errorState) { |
255: | --$this->errorState; |
256: | } |
257: | |
258: | if ($action < $this->numNonLeafStates) { |
259: | continue; |
260: | } |
261: | |
262: | |
263: | $rule = $action - $this->numNonLeafStates; |
264: | } else { |
265: | $rule = -$action; |
266: | } |
267: | } else { |
268: | $rule = $this->actionDefault[$state]; |
269: | } |
270: | } |
271: | |
272: | for (;;) { |
273: | if ($rule === 0) { |
274: | |
275: | |
276: | return $this->semValue; |
277: | } elseif ($rule !== $this->unexpectedTokenRule) { |
278: | |
279: | |
280: | |
281: | try { |
282: | $this->reduceCallbacks[$rule]($stackPos); |
283: | } catch (Error $e) { |
284: | if (-1 === $e->getStartLine() && isset($startAttributes['startLine'])) { |
285: | $e->setStartLine($startAttributes['startLine']); |
286: | } |
287: | |
288: | $this->emitError($e); |
289: | |
290: | return null; |
291: | } |
292: | |
293: | |
294: | $lastEndAttributes = $this->endAttributeStack[$stackPos]; |
295: | $ruleLength = $this->ruleToLength[$rule]; |
296: | $stackPos -= $ruleLength; |
297: | $nonTerminal = $this->ruleToNonTerminal[$rule]; |
298: | $idx = $this->gotoBase[$nonTerminal] + $stateStack[$stackPos]; |
299: | if ($idx >= 0 && $idx < $this->gotoTableSize && $this->gotoCheck[$idx] === $nonTerminal) { |
300: | $state = $this->goto[$idx]; |
301: | } else { |
302: | $state = $this->gotoDefault[$nonTerminal]; |
303: | } |
304: | |
305: | ++$stackPos; |
306: | $stateStack[$stackPos] = $state; |
307: | $this->semStack[$stackPos] = $this->semValue; |
308: | $this->endAttributeStack[$stackPos] = $lastEndAttributes; |
309: | if ($ruleLength === 0) { |
310: | |
311: | $this->startAttributeStack[$stackPos] = $this->lookaheadStartAttributes; |
312: | } |
313: | } else { |
314: | |
315: | switch ($this->errorState) { |
316: | case 0: |
317: | $msg = $this->getErrorMessage($symbol, $state); |
318: | $this->emitError(new Error($msg, $startAttributes + $endAttributes)); |
319: | |
320: | case 1: |
321: | case 2: |
322: | $this->errorState = 3; |
323: | |
324: | |
325: | while (!( |
326: | (($idx = $this->actionBase[$state] + $this->errorSymbol) >= 0 |
327: | && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol) |
328: | || ($state < $this->YY2TBLSTATE |
329: | && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $this->errorSymbol) >= 0 |
330: | && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $this->errorSymbol) |
331: | ) || ($action = $this->action[$idx]) === $this->defaultAction) { |
332: | if ($stackPos <= 0) { |
333: | |
334: | return null; |
335: | } |
336: | $state = $stateStack[--$stackPos]; |
337: | |
338: | } |
339: | |
340: | |
341: | ++$stackPos; |
342: | $stateStack[$stackPos] = $state = $action; |
343: | |
344: | |
345: | |
346: | $this->startAttributeStack[$stackPos] = $this->lookaheadStartAttributes; |
347: | $this->endAttributeStack[$stackPos] = $this->endAttributeStack[$stackPos - 1]; |
348: | $this->endAttributes = $this->endAttributeStack[$stackPos - 1]; |
349: | break; |
350: | |
351: | case 3: |
352: | if ($symbol === 0) { |
353: | |
354: | return null; |
355: | } |
356: | |
357: | |
358: | $symbol = self::SYMBOL_NONE; |
359: | break 2; |
360: | } |
361: | } |
362: | |
363: | if ($state < $this->numNonLeafStates) { |
364: | break; |
365: | } |
366: | |
367: | |
368: | $rule = $state - $this->numNonLeafStates; |
369: | } |
370: | } |
371: | |
372: | throw new \RuntimeException('Reached end of parser loop'); |
373: | } |
374: | |
375: | protected function emitError(Error $error) { |
376: | $this->errorHandler->handleError($error); |
377: | } |
378: | |
379: | |
380: | |
381: | |
382: | |
383: | |
384: | |
385: | |
386: | |
387: | protected function getErrorMessage(int $symbol, int $state) : string { |
388: | $expectedString = ''; |
389: | if ($expected = $this->getExpectedTokens($state)) { |
390: | $expectedString = ', expecting ' . implode(' or ', $expected); |
391: | } |
392: | |
393: | return 'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString; |
394: | } |
395: | |
396: | |
397: | |
398: | |
399: | |
400: | |
401: | |
402: | |
403: | protected function getExpectedTokens(int $state) : array { |
404: | $expected = []; |
405: | |
406: | $base = $this->actionBase[$state]; |
407: | foreach ($this->symbolToName as $symbol => $name) { |
408: | $idx = $base + $symbol; |
409: | if ($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol |
410: | || $state < $this->YY2TBLSTATE |
411: | && ($idx = $this->actionBase[$state + $this->numNonLeafStates] + $symbol) >= 0 |
412: | && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol |
413: | ) { |
414: | if ($this->action[$idx] !== $this->unexpectedTokenRule |
415: | && $this->action[$idx] !== $this->defaultAction |
416: | && $symbol !== $this->errorSymbol |
417: | ) { |
418: | if (count($expected) === 4) { |
419: | |
420: | return []; |
421: | } |
422: | |
423: | $expected[] = $name; |
424: | } |
425: | } |
426: | } |
427: | |
428: | return $expected; |
429: | } |
430: | |
431: | |
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: | protected function handleNamespaces(array $stmts) : array { |
477: | $hasErrored = false; |
478: | $style = $this->getNamespacingStyle($stmts); |
479: | if (null === $style) { |
480: | |
481: | return $stmts; |
482: | } elseif ('brace' === $style) { |
483: | |
484: | $afterFirstNamespace = false; |
485: | foreach ($stmts as $stmt) { |
486: | if ($stmt instanceof Node\Stmt\Namespace_) { |
487: | $afterFirstNamespace = true; |
488: | } elseif (!$stmt instanceof Node\Stmt\HaltCompiler |
489: | && !$stmt instanceof Node\Stmt\Nop |
490: | && $afterFirstNamespace && !$hasErrored) { |
491: | $this->emitError(new Error( |
492: | 'No code may exist outside of namespace {}', $stmt->getAttributes())); |
493: | $hasErrored = true; |
494: | } |
495: | } |
496: | return $stmts; |
497: | } else { |
498: | |
499: | $resultStmts = []; |
500: | $targetStmts =& $resultStmts; |
501: | $lastNs = null; |
502: | foreach ($stmts as $stmt) { |
503: | if ($stmt instanceof Node\Stmt\Namespace_) { |
504: | if ($lastNs !== null) { |
505: | $this->fixupNamespaceAttributes($lastNs); |
506: | } |
507: | if ($stmt->stmts === null) { |
508: | $stmt->stmts = []; |
509: | $targetStmts =& $stmt->stmts; |
510: | $resultStmts[] = $stmt; |
511: | } else { |
512: | |
513: | $resultStmts[] = $stmt; |
514: | $targetStmts =& $resultStmts; |
515: | } |
516: | $lastNs = $stmt; |
517: | } elseif ($stmt instanceof Node\Stmt\HaltCompiler) { |
518: | |
519: | $resultStmts[] = $stmt; |
520: | } else { |
521: | $targetStmts[] = $stmt; |
522: | } |
523: | } |
524: | if ($lastNs !== null) { |
525: | $this->fixupNamespaceAttributes($lastNs); |
526: | } |
527: | return $resultStmts; |
528: | } |
529: | } |
530: | |
531: | private function fixupNamespaceAttributes(Node\Stmt\Namespace_ $stmt) { |
532: | |
533: | |
534: | if (empty($stmt->stmts)) { |
535: | return; |
536: | } |
537: | |
538: | |
539: | |
540: | $endAttributes = ['endLine', 'endFilePos', 'endTokenPos']; |
541: | $lastStmt = $stmt->stmts[count($stmt->stmts) - 1]; |
542: | foreach ($endAttributes as $endAttribute) { |
543: | if ($lastStmt->hasAttribute($endAttribute)) { |
544: | $stmt->setAttribute($endAttribute, $lastStmt->getAttribute($endAttribute)); |
545: | } |
546: | } |
547: | } |
548: | |
549: | |
550: | |
551: | |
552: | |
553: | |
554: | |
555: | |
556: | private function getNamespacingStyle(array $stmts) { |
557: | $style = null; |
558: | $hasNotAllowedStmts = false; |
559: | foreach ($stmts as $i => $stmt) { |
560: | if ($stmt instanceof Node\Stmt\Namespace_) { |
561: | $currentStyle = null === $stmt->stmts ? 'semicolon' : 'brace'; |
562: | if (null === $style) { |
563: | $style = $currentStyle; |
564: | if ($hasNotAllowedStmts) { |
565: | $this->emitError(new Error( |
566: | 'Namespace declaration statement has to be the very first statement in the script', |
567: | $stmt->getLine() |
568: | )); |
569: | } |
570: | } elseif ($style !== $currentStyle) { |
571: | $this->emitError(new Error( |
572: | 'Cannot mix bracketed namespace declarations with unbracketed namespace declarations', |
573: | $stmt->getLine() |
574: | )); |
575: | |
576: | return 'semicolon'; |
577: | } |
578: | continue; |
579: | } |
580: | |
581: | |
582: | if ($stmt instanceof Node\Stmt\Declare_ |
583: | || $stmt instanceof Node\Stmt\HaltCompiler |
584: | || $stmt instanceof Node\Stmt\Nop) { |
585: | continue; |
586: | } |
587: | |
588: | |
589: | if ($i === 0 && $stmt instanceof Node\Stmt\InlineHTML && preg_match('/\A#!.*\r?\n\z/', $stmt->value)) { |
590: | continue; |
591: | } |
592: | |
593: | |
594: | $hasNotAllowedStmts = true; |
595: | } |
596: | return $style; |
597: | } |
598: | |
599: | |
600: | |
601: | |
602: | |
603: | |
604: | |
605: | |
606: | |
607: | |
608: | |
609: | |
610: | |
611: | |
612: | |
613: | protected function fixupPhp5StaticPropCall($prop, array $args, array $attributes) : Expr\StaticCall { |
614: | if ($prop instanceof Node\Expr\StaticPropertyFetch) { |
615: | $name = $prop->name instanceof VarLikeIdentifier |
616: | ? $prop->name->toString() : $prop->name; |
617: | $var = new Expr\Variable($name, $prop->name->getAttributes()); |
618: | return new Expr\StaticCall($prop->class, $var, $args, $attributes); |
619: | } elseif ($prop instanceof Node\Expr\ArrayDimFetch) { |
620: | $tmp = $prop; |
621: | while ($tmp->var instanceof Node\Expr\ArrayDimFetch) { |
622: | $tmp = $tmp->var; |
623: | } |
624: | |
625: | |
626: | $staticProp = $tmp->var; |
627: | |
628: | |
629: | $tmp = $prop; |
630: | $this->fixupStartAttributes($tmp, $staticProp->name); |
631: | while ($tmp->var instanceof Node\Expr\ArrayDimFetch) { |
632: | $tmp = $tmp->var; |
633: | $this->fixupStartAttributes($tmp, $staticProp->name); |
634: | } |
635: | |
636: | $name = $staticProp->name instanceof VarLikeIdentifier |
637: | ? $staticProp->name->toString() : $staticProp->name; |
638: | $tmp->var = new Expr\Variable($name, $staticProp->name->getAttributes()); |
639: | return new Expr\StaticCall($staticProp->class, $prop, $args, $attributes); |
640: | } else { |
641: | throw new \Exception; |
642: | } |
643: | } |
644: | |
645: | protected function fixupStartAttributes(Node $to, Node $from) { |
646: | $startAttributes = ['startLine', 'startFilePos', 'startTokenPos']; |
647: | foreach ($startAttributes as $startAttribute) { |
648: | if ($from->hasAttribute($startAttribute)) { |
649: | $to->setAttribute($startAttribute, $from->getAttribute($startAttribute)); |
650: | } |
651: | } |
652: | } |
653: | |
654: | protected function handleBuiltinTypes(Name $name) { |
655: | $builtinTypes = [ |
656: | 'bool' => true, |
657: | 'int' => true, |
658: | 'float' => true, |
659: | 'string' => true, |
660: | 'iterable' => true, |
661: | 'void' => true, |
662: | 'object' => true, |
663: | 'null' => true, |
664: | 'false' => true, |
665: | 'mixed' => true, |
666: | 'never' => true, |
667: | 'true' => true, |
668: | ]; |
669: | |
670: | if (!$name->isUnqualified()) { |
671: | return $name; |
672: | } |
673: | |
674: | $lowerName = $name->toLowerString(); |
675: | if (!isset($builtinTypes[$lowerName])) { |
676: | return $name; |
677: | } |
678: | |
679: | return new Node\Identifier($lowerName, $name->getAttributes()); |
680: | } |
681: | |
682: | |
683: | |
684: | |
685: | |
686: | |
687: | |
688: | |
689: | protected function getAttributesAt(int $pos) : array { |
690: | return $this->startAttributeStack[$pos] + $this->endAttributeStack[$pos]; |
691: | } |
692: | |
693: | protected function getFloatCastKind(string $cast): int |
694: | { |
695: | $cast = strtolower($cast); |
696: | if (strpos($cast, 'float') !== false) { |
697: | return Double::KIND_FLOAT; |
698: | } |
699: | |
700: | if (strpos($cast, 'real') !== false) { |
701: | return Double::KIND_REAL; |
702: | } |
703: | |
704: | return Double::KIND_DOUBLE; |
705: | } |
706: | |
707: | protected function parseLNumber($str, $attributes, $allowInvalidOctal = false) { |
708: | try { |
709: | return LNumber::fromString($str, $attributes, $allowInvalidOctal); |
710: | } catch (Error $error) { |
711: | $this->emitError($error); |
712: | |
713: | return new LNumber(0, $attributes); |
714: | } |
715: | } |
716: | |
717: | |
718: | |
719: | |
720: | |
721: | |
722: | |
723: | |
724: | |
725: | protected function parseNumString(string $str, array $attributes) { |
726: | if (!preg_match('/^(?:0|-?[1-9][0-9]*)$/', $str)) { |
727: | return new String_($str, $attributes); |
728: | } |
729: | |
730: | $num = +$str; |
731: | if (!is_int($num)) { |
732: | return new String_($str, $attributes); |
733: | } |
734: | |
735: | return new LNumber($num, $attributes); |
736: | } |
737: | |
738: | protected function stripIndentation( |
739: | string $string, int $indentLen, string $indentChar, |
740: | bool $newlineAtStart, bool $newlineAtEnd, array $attributes |
741: | ) { |
742: | if ($indentLen === 0) { |
743: | return $string; |
744: | } |
745: | |
746: | $start = $newlineAtStart ? '(?:(?<=\n)|\A)' : '(?<=\n)'; |
747: | $end = $newlineAtEnd ? '(?:(?=[\r\n])|\z)' : '(?=[\r\n])'; |
748: | $regex = '/' . $start . '([ \t]*)(' . $end . ')?/'; |
749: | return preg_replace_callback( |
750: | $regex, |
751: | function ($matches) use ($indentLen, $indentChar, $attributes) { |
752: | $prefix = substr($matches[1], 0, $indentLen); |
753: | if (false !== strpos($prefix, $indentChar === " " ? "\t" : " ")) { |
754: | $this->emitError(new Error( |
755: | 'Invalid indentation - tabs and spaces cannot be mixed', $attributes |
756: | )); |
757: | } elseif (strlen($prefix) < $indentLen && !isset($matches[2])) { |
758: | $this->emitError(new Error( |
759: | 'Invalid body indentation level ' . |
760: | '(expecting an indentation level of at least ' . $indentLen . ')', |
761: | $attributes |
762: | )); |
763: | } |
764: | return substr($matches[0], strlen($prefix)); |
765: | }, |
766: | $string |
767: | ); |
768: | } |
769: | |
770: | protected function parseDocString( |
771: | string $startToken, $contents, string $endToken, |
772: | array $attributes, array $endTokenAttributes, bool $parseUnicodeEscape |
773: | ) { |
774: | $kind = strpos($startToken, "'") === false |
775: | ? String_::KIND_HEREDOC : String_::KIND_NOWDOC; |
776: | |
777: | $regex = '/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/'; |
778: | $result = preg_match($regex, $startToken, $matches); |
779: | assert($result === 1); |
780: | $label = $matches[1]; |
781: | |
782: | $result = preg_match('/\A[ \t]*/', $endToken, $matches); |
783: | assert($result === 1); |
784: | $indentation = $matches[0]; |
785: | |
786: | $attributes['kind'] = $kind; |
787: | $attributes['docLabel'] = $label; |
788: | $attributes['docIndentation'] = $indentation; |
789: | |
790: | $indentHasSpaces = false !== strpos($indentation, " "); |
791: | $indentHasTabs = false !== strpos($indentation, "\t"); |
792: | if ($indentHasSpaces && $indentHasTabs) { |
793: | $this->emitError(new Error( |
794: | 'Invalid indentation - tabs and spaces cannot be mixed', |
795: | $endTokenAttributes |
796: | )); |
797: | |
798: | |
799: | $indentation = ''; |
800: | } |
801: | |
802: | $indentLen = \strlen($indentation); |
803: | $indentChar = $indentHasSpaces ? " " : "\t"; |
804: | |
805: | if (\is_string($contents)) { |
806: | if ($contents === '') { |
807: | return new String_('', $attributes); |
808: | } |
809: | |
810: | $contents = $this->stripIndentation( |
811: | $contents, $indentLen, $indentChar, true, true, $attributes |
812: | ); |
813: | $contents = preg_replace('~(\r\n|\n|\r)\z~', '', $contents); |
814: | |
815: | if ($kind === String_::KIND_HEREDOC) { |
816: | $contents = String_::parseEscapeSequences($contents, null, $parseUnicodeEscape); |
817: | } |
818: | |
819: | return new String_($contents, $attributes); |
820: | } else { |
821: | assert(count($contents) > 0); |
822: | if (!$contents[0] instanceof Node\Scalar\EncapsedStringPart) { |
823: | |
824: | $this->stripIndentation( |
825: | '', $indentLen, $indentChar, true, false, $contents[0]->getAttributes() |
826: | ); |
827: | } |
828: | |
829: | $newContents = []; |
830: | foreach ($contents as $i => $part) { |
831: | if ($part instanceof Node\Scalar\EncapsedStringPart) { |
832: | $isLast = $i === \count($contents) - 1; |
833: | $part->value = $this->stripIndentation( |
834: | $part->value, $indentLen, $indentChar, |
835: | $i === 0, $isLast, $part->getAttributes() |
836: | ); |
837: | $part->value = String_::parseEscapeSequences($part->value, null, $parseUnicodeEscape); |
838: | if ($isLast) { |
839: | $part->value = preg_replace('~(\r\n|\n|\r)\z~', '', $part->value); |
840: | } |
841: | if ('' === $part->value) { |
842: | continue; |
843: | } |
844: | } |
845: | $newContents[] = $part; |
846: | } |
847: | return new Encapsed($newContents, $attributes); |
848: | } |
849: | } |
850: | |
851: | |
852: | |
853: | |
854: | |
855: | |
856: | |
857: | protected function createCommentNopAttributes(array $comments) { |
858: | $comment = $comments[count($comments) - 1]; |
859: | $commentEndLine = $comment->getEndLine(); |
860: | $commentEndFilePos = $comment->getEndFilePos(); |
861: | $commentEndTokenPos = $comment->getEndTokenPos(); |
862: | |
863: | $attributes = ['comments' => $comments]; |
864: | if (-1 !== $commentEndLine) { |
865: | $attributes['startLine'] = $commentEndLine; |
866: | $attributes['endLine'] = $commentEndLine; |
867: | } |
868: | if (-1 !== $commentEndFilePos) { |
869: | $attributes['startFilePos'] = $commentEndFilePos + 1; |
870: | $attributes['endFilePos'] = $commentEndFilePos; |
871: | } |
872: | if (-1 !== $commentEndTokenPos) { |
873: | $attributes['startTokenPos'] = $commentEndTokenPos + 1; |
874: | $attributes['endTokenPos'] = $commentEndTokenPos; |
875: | } |
876: | return $attributes; |
877: | } |
878: | |
879: | protected function checkClassModifier($a, $b, $modifierPos) { |
880: | try { |
881: | Class_::verifyClassModifier($a, $b); |
882: | } catch (Error $error) { |
883: | $error->setAttributes($this->getAttributesAt($modifierPos)); |
884: | $this->emitError($error); |
885: | } |
886: | } |
887: | |
888: | protected function checkModifier($a, $b, $modifierPos) { |
889: | |
890: | try { |
891: | Class_::verifyModifier($a, $b); |
892: | } catch (Error $error) { |
893: | $error->setAttributes($this->getAttributesAt($modifierPos)); |
894: | $this->emitError($error); |
895: | } |
896: | } |
897: | |
898: | protected function checkParam(Param $node) { |
899: | if ($node->variadic && null !== $node->default) { |
900: | $this->emitError(new Error( |
901: | 'Variadic parameter cannot have a default value', |
902: | $node->default->getAttributes() |
903: | )); |
904: | } |
905: | } |
906: | |
907: | protected function checkTryCatch(TryCatch $node) { |
908: | if (empty($node->catches) && null === $node->finally) { |
909: | $this->emitError(new Error( |
910: | 'Cannot use try without catch or finally', $node->getAttributes() |
911: | )); |
912: | } |
913: | } |
914: | |
915: | protected function checkNamespace(Namespace_ $node) { |
916: | if (null !== $node->stmts) { |
917: | foreach ($node->stmts as $stmt) { |
918: | if ($stmt instanceof Namespace_) { |
919: | $this->emitError(new Error( |
920: | 'Namespace declarations cannot be nested', $stmt->getAttributes() |
921: | )); |
922: | } |
923: | } |
924: | } |
925: | } |
926: | |
927: | private function checkClassName($name, $namePos) { |
928: | if (null !== $name && $name->isSpecialClassName()) { |
929: | $this->emitError(new Error( |
930: | sprintf('Cannot use \'%s\' as class name as it is reserved', $name), |
931: | $this->getAttributesAt($namePos) |
932: | )); |
933: | } |
934: | } |
935: | |
936: | private function checkImplementedInterfaces(array $interfaces) { |
937: | foreach ($interfaces as $interface) { |
938: | if ($interface->isSpecialClassName()) { |
939: | $this->emitError(new Error( |
940: | sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface), |
941: | $interface->getAttributes() |
942: | )); |
943: | } |
944: | } |
945: | } |
946: | |
947: | protected function checkClass(Class_ $node, $namePos) { |
948: | $this->checkClassName($node->name, $namePos); |
949: | |
950: | if ($node->extends && $node->extends->isSpecialClassName()) { |
951: | $this->emitError(new Error( |
952: | sprintf('Cannot use \'%s\' as class name as it is reserved', $node->extends), |
953: | $node->extends->getAttributes() |
954: | )); |
955: | } |
956: | |
957: | $this->checkImplementedInterfaces($node->implements); |
958: | } |
959: | |
960: | protected function checkInterface(Interface_ $node, $namePos) { |
961: | $this->checkClassName($node->name, $namePos); |
962: | $this->checkImplementedInterfaces($node->extends); |
963: | } |
964: | |
965: | protected function checkEnum(Enum_ $node, $namePos) { |
966: | $this->checkClassName($node->name, $namePos); |
967: | $this->checkImplementedInterfaces($node->implements); |
968: | } |
969: | |
970: | protected function checkClassMethod(ClassMethod $node, $modifierPos) { |
971: | if ($node->flags & Class_::MODIFIER_STATIC) { |
972: | switch ($node->name->toLowerString()) { |
973: | case '__construct': |
974: | $this->emitError(new Error( |
975: | sprintf('Constructor %s() cannot be static', $node->name), |
976: | $this->getAttributesAt($modifierPos))); |
977: | break; |
978: | case '__destruct': |
979: | $this->emitError(new Error( |
980: | sprintf('Destructor %s() cannot be static', $node->name), |
981: | $this->getAttributesAt($modifierPos))); |
982: | break; |
983: | case '__clone': |
984: | $this->emitError(new Error( |
985: | sprintf('Clone method %s() cannot be static', $node->name), |
986: | $this->getAttributesAt($modifierPos))); |
987: | break; |
988: | } |
989: | } |
990: | |
991: | if ($node->flags & Class_::MODIFIER_READONLY) { |
992: | $this->emitError(new Error( |
993: | sprintf('Method %s() cannot be readonly', $node->name), |
994: | $this->getAttributesAt($modifierPos))); |
995: | } |
996: | } |
997: | |
998: | protected function checkClassConst(ClassConst $node, $modifierPos) { |
999: | if ($node->flags & Class_::MODIFIER_STATIC) { |
1000: | $this->emitError(new Error( |
1001: | "Cannot use 'static' as constant modifier", |
1002: | $this->getAttributesAt($modifierPos))); |
1003: | } |
1004: | if ($node->flags & Class_::MODIFIER_ABSTRACT) { |
1005: | $this->emitError(new Error( |
1006: | "Cannot use 'abstract' as constant modifier", |
1007: | $this->getAttributesAt($modifierPos))); |
1008: | } |
1009: | if ($node->flags & Class_::MODIFIER_READONLY) { |
1010: | $this->emitError(new Error( |
1011: | "Cannot use 'readonly' as constant modifier", |
1012: | $this->getAttributesAt($modifierPos))); |
1013: | } |
1014: | } |
1015: | |
1016: | protected function checkProperty(Property $node, $modifierPos) { |
1017: | if ($node->flags & Class_::MODIFIER_ABSTRACT) { |
1018: | $this->emitError(new Error('Properties cannot be declared abstract', |
1019: | $this->getAttributesAt($modifierPos))); |
1020: | } |
1021: | |
1022: | if ($node->flags & Class_::MODIFIER_FINAL) { |
1023: | $this->emitError(new Error('Properties cannot be declared final', |
1024: | $this->getAttributesAt($modifierPos))); |
1025: | } |
1026: | } |
1027: | |
1028: | protected function checkUseUse(UseUse $node, $namePos) { |
1029: | if ($node->alias && $node->alias->isSpecialClassName()) { |
1030: | $this->emitError(new Error( |
1031: | sprintf( |
1032: | 'Cannot use %s as %s because \'%2$s\' is a special class name', |
1033: | $node->name, $node->alias |
1034: | ), |
1035: | $this->getAttributesAt($namePos) |
1036: | )); |
1037: | } |
1038: | } |
1039: | } |
1040: | |