1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\PhpDocParser\Parser;
4:
5: use LogicException;
6: use PHPStan\PhpDocParser\Ast;
7: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
8: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
9: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
10: use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
11: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
12: use PHPStan\PhpDocParser\Lexer\Lexer;
13: use PHPStan\PhpDocParser\ParserConfig;
14: use PHPStan\ShouldNotHappenException;
15: use function array_key_exists;
16: use function count;
17: use function rtrim;
18: use function str_replace;
19: use function trim;
20:
21: /**
22: * @phpstan-import-type ValueType from Doctrine\DoctrineArgument as DoctrineValueType
23: */
24: class PhpDocParser
25: {
26:
27: private const DISALLOWED_DESCRIPTION_START_TOKENS = [
28: Lexer::TOKEN_UNION,
29: Lexer::TOKEN_INTERSECTION,
30: ];
31:
32: private ParserConfig $config;
33:
34: private TypeParser $typeParser;
35:
36: private ConstExprParser $constantExprParser;
37:
38: private ConstExprParser $doctrineConstantExprParser;
39:
40: public function __construct(
41: ParserConfig $config,
42: TypeParser $typeParser,
43: ConstExprParser $constantExprParser
44: )
45: {
46: $this->config = $config;
47: $this->typeParser = $typeParser;
48: $this->constantExprParser = $constantExprParser;
49: $this->doctrineConstantExprParser = $constantExprParser->toDoctrine();
50: }
51:
52:
53: public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode
54: {
55: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PHPDOC);
56: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
57:
58: $children = [];
59:
60: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
61: $lastChild = $this->parseChild($tokens);
62: $children[] = $lastChild;
63: while (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
64: if (
65: $lastChild instanceof Ast\PhpDoc\PhpDocTagNode
66: && (
67: $lastChild->value instanceof Doctrine\DoctrineTagValueNode
68: || $lastChild->value instanceof Ast\PhpDoc\GenericTagValueNode
69: )
70: ) {
71: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
72: if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
73: break;
74: }
75: $lastChild = $this->parseChild($tokens);
76: $children[] = $lastChild;
77: continue;
78: }
79:
80: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
81: break;
82: }
83: if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
84: break;
85: }
86:
87: $lastChild = $this->parseChild($tokens);
88: $children[] = $lastChild;
89: }
90: }
91:
92: try {
93: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC);
94: } catch (ParserException $e) {
95: $name = '';
96: $startLine = $tokens->currentTokenLine();
97: $startIndex = $tokens->currentTokenIndex();
98: if (count($children) > 0) {
99: $lastChild = $children[count($children) - 1];
100: if ($lastChild instanceof Ast\PhpDoc\PhpDocTagNode) {
101: $name = $lastChild->name;
102: $startLine = $tokens->currentTokenLine();
103: $startIndex = $tokens->currentTokenIndex();
104: }
105: }
106:
107: $tag = new Ast\PhpDoc\PhpDocTagNode(
108: $name,
109: $this->enrichWithAttributes(
110: $tokens,
111: new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e),
112: $startLine,
113: $startIndex,
114: ),
115: );
116:
117: $tokens->forwardToTheEnd();
118:
119: return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]), 1, 0);
120: }
121:
122: return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode($children), 1, 0);
123: }
124:
125:
126: /** @phpstan-impure */
127: private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode
128: {
129: if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
130: $startLine = $tokens->currentTokenLine();
131: $startIndex = $tokens->currentTokenIndex();
132: return $this->enrichWithAttributes($tokens, $this->parseTag($tokens), $startLine, $startIndex);
133: }
134:
135: if ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_TAG)) {
136: $startLine = $tokens->currentTokenLine();
137: $startIndex = $tokens->currentTokenIndex();
138: $tag = $tokens->currentTokenValue();
139: $tokens->next();
140:
141: $tagStartLine = $tokens->currentTokenLine();
142: $tagStartIndex = $tokens->currentTokenIndex();
143:
144: return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocTagNode(
145: $tag,
146: $this->enrichWithAttributes(
147: $tokens,
148: $this->parseDoctrineTagValue($tokens, $tag),
149: $tagStartLine,
150: $tagStartIndex,
151: ),
152: ), $startLine, $startIndex);
153: }
154:
155: $startLine = $tokens->currentTokenLine();
156: $startIndex = $tokens->currentTokenIndex();
157: $text = $this->parseText($tokens);
158:
159: return $this->enrichWithAttributes($tokens, $text, $startLine, $startIndex);
160: }
161:
162: /**
163: * @template T of Ast\Node
164: * @param T $tag
165: * @return T
166: */
167: private function enrichWithAttributes(TokenIterator $tokens, Ast\Node $tag, int $startLine, int $startIndex): Ast\Node
168: {
169: if ($this->config->useLinesAttributes) {
170: $tag->setAttribute(Ast\Attribute::START_LINE, $startLine);
171: $tag->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
172: }
173:
174: if ($this->config->useIndexAttributes) {
175: $tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
176: $tag->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
177: }
178:
179: return $tag;
180: }
181:
182:
183: private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode
184: {
185: $text = '';
186:
187: $endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
188:
189: $savepoint = false;
190:
191: // if the next token is EOL, everything below is skipped and empty string is returned
192: while (true) {
193: $tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
194: $text .= $tmpText;
195:
196: // stop if we're not at EOL - meaning it's the end of PHPDoc
197: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC)) {
198: break;
199: }
200:
201: if (!$savepoint) {
202: $tokens->pushSavePoint();
203: $savepoint = true;
204: } elseif ($tmpText !== '') {
205: $tokens->dropSavePoint();
206: $tokens->pushSavePoint();
207: }
208:
209: $tokens->pushSavePoint();
210: $tokens->next();
211:
212: // if we're at EOL, check what's next
213: // if next is a PHPDoc tag, EOL, or end of PHPDoc, stop
214: if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) {
215: $tokens->rollback();
216: break;
217: }
218:
219: // otherwise if the next is text, continue building the description string
220:
221: $tokens->dropSavePoint();
222: $text .= $tokens->getDetectedNewline() ?? "\n";
223: }
224:
225: if ($savepoint) {
226: $tokens->rollback();
227: $text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
228: }
229:
230: return new Ast\PhpDoc\PhpDocTextNode(trim($text, " \t"));
231: }
232:
233:
234: private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens): string
235: {
236: $text = '';
237:
238: $endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
239:
240: $savepoint = false;
241:
242: // if the next token is EOL, everything below is skipped and empty string is returned
243: while (true) {
244: $tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
245: $text .= $tmpText;
246:
247: // stop if we're not at EOL - meaning it's the end of PHPDoc
248: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC)) {
249: if (!$tokens->isPrecededByHorizontalWhitespace()) {
250: return trim($text . $this->parseText($tokens)->text, " \t");
251: }
252: if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
253: $tokens->pushSavePoint();
254: $child = $this->parseChild($tokens);
255: if ($child instanceof Ast\PhpDoc\PhpDocTagNode) {
256: if (
257: $child->value instanceof Ast\PhpDoc\GenericTagValueNode
258: || $child->value instanceof Doctrine\DoctrineTagValueNode
259: ) {
260: $tokens->rollback();
261: break;
262: }
263: if ($child->value instanceof Ast\PhpDoc\InvalidTagValueNode) {
264: $tokens->rollback();
265: $tokens->pushSavePoint();
266: $tokens->next();
267: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
268: $tokens->rollback();
269: break;
270: }
271: $tokens->rollback();
272: return trim($text . $this->parseText($tokens)->text, " \t");
273: }
274: }
275:
276: $tokens->rollback();
277: return trim($text . $this->parseText($tokens)->text, " \t");
278: }
279: break;
280: }
281:
282: if (!$savepoint) {
283: $tokens->pushSavePoint();
284: $savepoint = true;
285: } elseif ($tmpText !== '') {
286: $tokens->dropSavePoint();
287: $tokens->pushSavePoint();
288: }
289:
290: $tokens->pushSavePoint();
291: $tokens->next();
292:
293: // if we're at EOL, check what's next
294: // if next is a PHPDoc tag, EOL, or end of PHPDoc, stop
295: if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) {
296: $tokens->rollback();
297: break;
298: }
299:
300: // otherwise if the next is text, continue building the description string
301:
302: $tokens->dropSavePoint();
303: $text .= $tokens->getDetectedNewline() ?? "\n";
304: }
305:
306: if ($savepoint) {
307: $tokens->rollback();
308: $text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
309: }
310:
311: return trim($text, " \t");
312: }
313:
314:
315: public function parseTag(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagNode
316: {
317: $tag = $tokens->currentTokenValue();
318: $tokens->next();
319: $value = $this->parseTagValue($tokens, $tag);
320:
321: return new Ast\PhpDoc\PhpDocTagNode($tag, $value);
322: }
323:
324:
325: public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode
326: {
327: $startLine = $tokens->currentTokenLine();
328: $startIndex = $tokens->currentTokenIndex();
329:
330: try {
331: $tokens->pushSavePoint();
332:
333: switch ($tag) {
334: case '@param':
335: case '@phpstan-param':
336: case '@psalm-param':
337: case '@phan-param':
338: $tagValue = $this->parseParamTagValue($tokens);
339: break;
340:
341: case '@param-immediately-invoked-callable':
342: case '@phpstan-param-immediately-invoked-callable':
343: $tagValue = $this->parseParamImmediatelyInvokedCallableTagValue($tokens);
344: break;
345:
346: case '@param-later-invoked-callable':
347: case '@phpstan-param-later-invoked-callable':
348: $tagValue = $this->parseParamLaterInvokedCallableTagValue($tokens);
349: break;
350:
351: case '@param-closure-this':
352: case '@phpstan-param-closure-this':
353: $tagValue = $this->parseParamClosureThisTagValue($tokens);
354: break;
355:
356: case '@pure-unless-callable-is-impure':
357: case '@phpstan-pure-unless-callable-is-impure':
358: $tagValue = $this->parsePureUnlessCallableIsImpureTagValue($tokens);
359: break;
360:
361: case '@var':
362: case '@phpstan-var':
363: case '@psalm-var':
364: case '@phan-var':
365: $tagValue = $this->parseVarTagValue($tokens);
366: break;
367:
368: case '@return':
369: case '@phpstan-return':
370: case '@psalm-return':
371: case '@phan-return':
372: case '@phan-real-return':
373: $tagValue = $this->parseReturnTagValue($tokens);
374: break;
375:
376: case '@throws':
377: case '@phpstan-throws':
378: $tagValue = $this->parseThrowsTagValue($tokens);
379: break;
380:
381: case '@mixin':
382: case '@phan-mixin':
383: $tagValue = $this->parseMixinTagValue($tokens);
384: break;
385:
386: case '@psalm-require-extends':
387: case '@phpstan-require-extends':
388: $tagValue = $this->parseRequireExtendsTagValue($tokens);
389: break;
390:
391: case '@psalm-require-implements':
392: case '@phpstan-require-implements':
393: $tagValue = $this->parseRequireImplementsTagValue($tokens);
394: break;
395:
396: case '@deprecated':
397: $tagValue = $this->parseDeprecatedTagValue($tokens);
398: break;
399:
400: case '@property':
401: case '@property-read':
402: case '@property-write':
403: case '@phpstan-property':
404: case '@phpstan-property-read':
405: case '@phpstan-property-write':
406: case '@psalm-property':
407: case '@psalm-property-read':
408: case '@psalm-property-write':
409: case '@phan-property':
410: case '@phan-property-read':
411: case '@phan-property-write':
412: $tagValue = $this->parsePropertyTagValue($tokens);
413: break;
414:
415: case '@method':
416: case '@phpstan-method':
417: case '@psalm-method':
418: case '@phan-method':
419: $tagValue = $this->parseMethodTagValue($tokens);
420: break;
421:
422: case '@template':
423: case '@phpstan-template':
424: case '@psalm-template':
425: case '@phan-template':
426: case '@template-covariant':
427: case '@phpstan-template-covariant':
428: case '@psalm-template-covariant':
429: case '@template-contravariant':
430: case '@phpstan-template-contravariant':
431: case '@psalm-template-contravariant':
432: $tagValue = $this->typeParser->parseTemplateTagValue(
433: $tokens,
434: fn ($tokens) => $this->parseOptionalDescription($tokens, true),
435: );
436: break;
437:
438: case '@extends':
439: case '@phpstan-extends':
440: case '@phan-extends':
441: case '@phan-inherits':
442: case '@template-extends':
443: $tagValue = $this->parseExtendsTagValue('@extends', $tokens);
444: break;
445:
446: case '@implements':
447: case '@phpstan-implements':
448: case '@template-implements':
449: $tagValue = $this->parseExtendsTagValue('@implements', $tokens);
450: break;
451:
452: case '@use':
453: case '@phpstan-use':
454: case '@template-use':
455: $tagValue = $this->parseExtendsTagValue('@use', $tokens);
456: break;
457:
458: case '@phpstan-type':
459: case '@psalm-type':
460: case '@phan-type':
461: $tagValue = $this->parseTypeAliasTagValue($tokens);
462: break;
463:
464: case '@phpstan-import-type':
465: case '@psalm-import-type':
466: $tagValue = $this->parseTypeAliasImportTagValue($tokens);
467: break;
468:
469: case '@phpstan-assert':
470: case '@phpstan-assert-if-true':
471: case '@phpstan-assert-if-false':
472: case '@psalm-assert':
473: case '@psalm-assert-if-true':
474: case '@psalm-assert-if-false':
475: case '@phan-assert':
476: case '@phan-assert-if-true':
477: case '@phan-assert-if-false':
478: $tagValue = $this->parseAssertTagValue($tokens);
479: break;
480:
481: case '@phpstan-this-out':
482: case '@phpstan-self-out':
483: case '@psalm-this-out':
484: case '@psalm-self-out':
485: $tagValue = $this->parseSelfOutTagValue($tokens);
486: break;
487:
488: case '@param-out':
489: case '@phpstan-param-out':
490: case '@psalm-param-out':
491: $tagValue = $this->parseParamOutTagValue($tokens);
492: break;
493:
494: default:
495: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
496: $tagValue = $this->parseDoctrineTagValue($tokens, $tag);
497: } else {
498: $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescriptionAfterDoctrineTag($tokens));
499: }
500: break;
501: }
502:
503: $tokens->dropSavePoint();
504:
505: } catch (ParserException $e) {
506: $tokens->rollback();
507: $tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens, false), $e);
508: }
509:
510: return $this->enrichWithAttributes($tokens, $tagValue, $startLine, $startIndex);
511: }
512:
513:
514: private function parseDoctrineTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode
515: {
516: $startLine = $tokens->currentTokenLine();
517: $startIndex = $tokens->currentTokenIndex();
518:
519: return new Doctrine\DoctrineTagValueNode(
520: $this->enrichWithAttributes(
521: $tokens,
522: new Doctrine\DoctrineAnnotation($tag, $this->parseDoctrineArguments($tokens, false)),
523: $startLine,
524: $startIndex,
525: ),
526: $this->parseOptionalDescriptionAfterDoctrineTag($tokens),
527: );
528: }
529:
530:
531: /**
532: * @return list<Doctrine\DoctrineArgument>
533: */
534: private function parseDoctrineArguments(TokenIterator $tokens, bool $deep): array
535: {
536: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
537: return [];
538: }
539:
540: if (!$deep) {
541: $tokens->addEndOfLineToSkippedTokens();
542: }
543:
544: $arguments = [];
545:
546: try {
547: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
548:
549: do {
550: if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
551: break;
552: }
553: $arguments[] = $this->parseDoctrineArgument($tokens);
554: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
555: } finally {
556: if (!$deep) {
557: $tokens->removeEndOfLineFromSkippedTokens();
558: }
559: }
560:
561: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
562:
563: return $arguments;
564: }
565:
566:
567: private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\DoctrineArgument
568: {
569: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
570: $startLine = $tokens->currentTokenLine();
571: $startIndex = $tokens->currentTokenIndex();
572:
573: return $this->enrichWithAttributes(
574: $tokens,
575: new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
576: $startLine,
577: $startIndex,
578: );
579: }
580:
581: $startLine = $tokens->currentTokenLine();
582: $startIndex = $tokens->currentTokenIndex();
583:
584: try {
585: $tokens->pushSavePoint();
586: $currentValue = $tokens->currentTokenValue();
587: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
588:
589: $key = $this->enrichWithAttributes(
590: $tokens,
591: new IdentifierTypeNode($currentValue),
592: $startLine,
593: $startIndex,
594: );
595: $tokens->consumeTokenType(Lexer::TOKEN_EQUAL);
596:
597: $value = $this->parseDoctrineArgumentValue($tokens);
598:
599: $tokens->dropSavePoint();
600:
601: return $this->enrichWithAttributes(
602: $tokens,
603: new Doctrine\DoctrineArgument($key, $value),
604: $startLine,
605: $startIndex,
606: );
607: } catch (ParserException $e) {
608: $tokens->rollback();
609:
610: return $this->enrichWithAttributes(
611: $tokens,
612: new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
613: $startLine,
614: $startIndex,
615: );
616: }
617: }
618:
619:
620: /**
621: * @return DoctrineValueType
622: */
623: private function parseDoctrineArgumentValue(TokenIterator $tokens)
624: {
625: $startLine = $tokens->currentTokenLine();
626: $startIndex = $tokens->currentTokenIndex();
627:
628: if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG)) {
629: $name = $tokens->currentTokenValue();
630: $tokens->next();
631:
632: return $this->enrichWithAttributes(
633: $tokens,
634: new Doctrine\DoctrineAnnotation($name, $this->parseDoctrineArguments($tokens, true)),
635: $startLine,
636: $startIndex,
637: );
638: }
639:
640: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
641: $items = [];
642: do {
643: if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
644: break;
645: }
646: $items[] = $this->parseDoctrineArrayItem($tokens);
647: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
648:
649: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
650:
651: return $this->enrichWithAttributes(
652: $tokens,
653: new Doctrine\DoctrineArray($items),
654: $startLine,
655: $startIndex,
656: );
657: }
658:
659: $currentTokenValue = $tokens->currentTokenValue();
660: $tokens->pushSavePoint(); // because of ConstFetchNode
661: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
662: $identifier = $this->enrichWithAttributes(
663: $tokens,
664: new Ast\Type\IdentifierTypeNode($currentTokenValue),
665: $startLine,
666: $startIndex,
667: );
668: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
669: $tokens->dropSavePoint();
670: return $identifier;
671: }
672:
673: $tokens->rollback(); // because of ConstFetchNode
674: } else {
675: $tokens->dropSavePoint(); // because of ConstFetchNode
676: }
677:
678: $currentTokenValue = $tokens->currentTokenValue();
679: $currentTokenType = $tokens->currentTokenType();
680: $currentTokenOffset = $tokens->currentTokenOffset();
681: $currentTokenLine = $tokens->currentTokenLine();
682:
683: try {
684: $constExpr = $this->doctrineConstantExprParser->parse($tokens);
685: if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
686: throw new ParserException(
687: $currentTokenValue,
688: $currentTokenType,
689: $currentTokenOffset,
690: Lexer::TOKEN_IDENTIFIER,
691: null,
692: $currentTokenLine,
693: );
694: }
695:
696: return $constExpr;
697: } catch (LogicException $e) {
698: throw new ParserException(
699: $currentTokenValue,
700: $currentTokenType,
701: $currentTokenOffset,
702: Lexer::TOKEN_IDENTIFIER,
703: null,
704: $currentTokenLine,
705: );
706: }
707: }
708:
709:
710: private function parseDoctrineArrayItem(TokenIterator $tokens): Doctrine\DoctrineArrayItem
711: {
712: $startLine = $tokens->currentTokenLine();
713: $startIndex = $tokens->currentTokenIndex();
714:
715: try {
716: $tokens->pushSavePoint();
717:
718: $key = $this->parseDoctrineArrayKey($tokens);
719: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
720: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COLON)) {
721: $tokens->consumeTokenType(Lexer::TOKEN_EQUAL); // will throw exception
722: }
723: }
724:
725: $value = $this->parseDoctrineArgumentValue($tokens);
726:
727: $tokens->dropSavePoint();
728:
729: return $this->enrichWithAttributes(
730: $tokens,
731: new Doctrine\DoctrineArrayItem($key, $value),
732: $startLine,
733: $startIndex,
734: );
735: } catch (ParserException $e) {
736: $tokens->rollback();
737:
738: return $this->enrichWithAttributes(
739: $tokens,
740: new Doctrine\DoctrineArrayItem(null, $this->parseDoctrineArgumentValue($tokens)),
741: $startLine,
742: $startIndex,
743: );
744: }
745: }
746:
747:
748: /**
749: * @return ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|ConstFetchNode
750: */
751: private function parseDoctrineArrayKey(TokenIterator $tokens)
752: {
753: $startLine = $tokens->currentTokenLine();
754: $startIndex = $tokens->currentTokenIndex();
755:
756: if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
757: $key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
758: $tokens->next();
759:
760: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
761: $key = $this->doctrineConstantExprParser->parseDoctrineString($tokens->currentTokenValue(), $tokens);
762:
763: $tokens->next();
764:
765: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
766: $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED);
767: $tokens->next();
768:
769: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
770: $value = $tokens->currentTokenValue();
771: $tokens->next();
772: $key = $this->doctrineConstantExprParser->parseDoctrineString($value, $tokens);
773:
774: } else {
775: $currentTokenValue = $tokens->currentTokenValue();
776: $tokens->pushSavePoint(); // because of ConstFetchNode
777: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
778: $tokens->dropSavePoint();
779: throw new ParserException(
780: $tokens->currentTokenValue(),
781: $tokens->currentTokenType(),
782: $tokens->currentTokenOffset(),
783: Lexer::TOKEN_IDENTIFIER,
784: null,
785: $tokens->currentTokenLine(),
786: );
787: }
788:
789: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
790: $tokens->dropSavePoint();
791:
792: return $this->enrichWithAttributes(
793: $tokens,
794: new IdentifierTypeNode($currentTokenValue),
795: $startLine,
796: $startIndex,
797: );
798: }
799:
800: $tokens->rollback();
801: $constExpr = $this->doctrineConstantExprParser->parse($tokens);
802: if (!$constExpr instanceof Ast\ConstExpr\ConstFetchNode) {
803: throw new ParserException(
804: $tokens->currentTokenValue(),
805: $tokens->currentTokenType(),
806: $tokens->currentTokenOffset(),
807: Lexer::TOKEN_IDENTIFIER,
808: null,
809: $tokens->currentTokenLine(),
810: );
811: }
812:
813: return $constExpr;
814: }
815:
816: return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
817: }
818:
819:
820: /**
821: * @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode
822: */
823: private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
824: {
825: if (
826: $tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE, Lexer::TOKEN_VARIADIC, Lexer::TOKEN_VARIABLE)
827: ) {
828: $type = null;
829: } else {
830: $type = $this->typeParser->parse($tokens);
831: }
832:
833: $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
834: $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
835: $parameterName = $this->parseRequiredVariableName($tokens);
836: $description = $this->parseOptionalDescription($tokens, false);
837:
838: if ($type !== null) {
839: return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference);
840: }
841:
842: return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference);
843: }
844:
845:
846: private function parseParamImmediatelyInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode
847: {
848: $parameterName = $this->parseRequiredVariableName($tokens);
849: $description = $this->parseOptionalDescription($tokens, false);
850:
851: return new Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode($parameterName, $description);
852: }
853:
854:
855: private function parseParamLaterInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode
856: {
857: $parameterName = $this->parseRequiredVariableName($tokens);
858: $description = $this->parseOptionalDescription($tokens, false);
859:
860: return new Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode($parameterName, $description);
861: }
862:
863:
864: private function parseParamClosureThisTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamClosureThisTagValueNode
865: {
866: $type = $this->typeParser->parse($tokens);
867: $parameterName = $this->parseRequiredVariableName($tokens);
868: $description = $this->parseOptionalDescription($tokens, false);
869:
870: return new Ast\PhpDoc\ParamClosureThisTagValueNode($type, $parameterName, $description);
871: }
872:
873: private function parsePureUnlessCallableIsImpureTagValue(TokenIterator $tokens): Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode
874: {
875: $parameterName = $this->parseRequiredVariableName($tokens);
876: $description = $this->parseOptionalDescription($tokens, false);
877:
878: return new Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode($parameterName, $description);
879: }
880:
881: private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode
882: {
883: $type = $this->typeParser->parse($tokens);
884: $variableName = $this->parseOptionalVariableName($tokens);
885: $description = $this->parseOptionalDescription($tokens, $variableName === '');
886: return new Ast\PhpDoc\VarTagValueNode($type, $variableName, $description);
887: }
888:
889:
890: private function parseReturnTagValue(TokenIterator $tokens): Ast\PhpDoc\ReturnTagValueNode
891: {
892: $type = $this->typeParser->parse($tokens);
893: $description = $this->parseOptionalDescription($tokens, true);
894: return new Ast\PhpDoc\ReturnTagValueNode($type, $description);
895: }
896:
897:
898: private function parseThrowsTagValue(TokenIterator $tokens): Ast\PhpDoc\ThrowsTagValueNode
899: {
900: $type = $this->typeParser->parse($tokens);
901: $description = $this->parseOptionalDescription($tokens, true);
902: return new Ast\PhpDoc\ThrowsTagValueNode($type, $description);
903: }
904:
905: private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagValueNode
906: {
907: $type = $this->typeParser->parse($tokens);
908: $description = $this->parseOptionalDescription($tokens, true);
909: return new Ast\PhpDoc\MixinTagValueNode($type, $description);
910: }
911:
912: private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode
913: {
914: $type = $this->typeParser->parse($tokens);
915: $description = $this->parseOptionalDescription($tokens, true);
916: return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description);
917: }
918:
919: private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode
920: {
921: $type = $this->typeParser->parse($tokens);
922: $description = $this->parseOptionalDescription($tokens, true);
923: return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description);
924: }
925:
926: private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
927: {
928: $description = $this->parseOptionalDescription($tokens, false);
929: return new Ast\PhpDoc\DeprecatedTagValueNode($description);
930: }
931:
932:
933: private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\PropertyTagValueNode
934: {
935: $type = $this->typeParser->parse($tokens);
936: $parameterName = $this->parseRequiredVariableName($tokens);
937: $description = $this->parseOptionalDescription($tokens, false);
938: return new Ast\PhpDoc\PropertyTagValueNode($type, $parameterName, $description);
939: }
940:
941:
942: private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode
943: {
944: $staticKeywordOrReturnTypeOrMethodName = $this->typeParser->parse($tokens);
945:
946: if ($staticKeywordOrReturnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode && $staticKeywordOrReturnTypeOrMethodName->name === 'static') {
947: $isStatic = true;
948: $returnTypeOrMethodName = $this->typeParser->parse($tokens);
949:
950: } else {
951: $isStatic = false;
952: $returnTypeOrMethodName = $staticKeywordOrReturnTypeOrMethodName;
953: }
954:
955: if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
956: $returnType = $returnTypeOrMethodName;
957: $methodName = $tokens->currentTokenValue();
958: $tokens->next();
959:
960: } elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) {
961: $returnType = $isStatic ? $staticKeywordOrReturnTypeOrMethodName : null;
962: $methodName = $returnTypeOrMethodName->name;
963: $isStatic = false;
964:
965: } else {
966: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); // will throw exception
967: exit;
968: }
969:
970: $templateTypes = [];
971:
972: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
973: do {
974: $startLine = $tokens->currentTokenLine();
975: $startIndex = $tokens->currentTokenIndex();
976: $templateTypes[] = $this->enrichWithAttributes(
977: $tokens,
978: $this->typeParser->parseTemplateTagValue($tokens),
979: $startLine,
980: $startIndex,
981: );
982: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
983: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
984: }
985:
986: $parameters = [];
987: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
988: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
989: $parameters[] = $this->parseMethodTagValueParameter($tokens);
990: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
991: $parameters[] = $this->parseMethodTagValueParameter($tokens);
992: }
993: }
994: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
995:
996: $description = $this->parseOptionalDescription($tokens, false);
997: return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes);
998: }
999:
1000: private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
1001: {
1002: $startLine = $tokens->currentTokenLine();
1003: $startIndex = $tokens->currentTokenIndex();
1004:
1005: switch ($tokens->currentTokenType()) {
1006: case Lexer::TOKEN_IDENTIFIER:
1007: case Lexer::TOKEN_OPEN_PARENTHESES:
1008: case Lexer::TOKEN_NULLABLE:
1009: $parameterType = $this->typeParser->parse($tokens);
1010: break;
1011:
1012: default:
1013: $parameterType = null;
1014: }
1015:
1016: $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
1017: $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
1018:
1019: $parameterName = $tokens->currentTokenValue();
1020: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
1021:
1022: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
1023: $defaultValue = $this->constantExprParser->parse($tokens);
1024:
1025: } else {
1026: $defaultValue = null;
1027: }
1028:
1029: return $this->enrichWithAttributes(
1030: $tokens,
1031: new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue),
1032: $startLine,
1033: $startIndex,
1034: );
1035: }
1036:
1037: private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
1038: {
1039: $startLine = $tokens->currentTokenLine();
1040: $startIndex = $tokens->currentTokenIndex();
1041: $baseType = new IdentifierTypeNode($tokens->currentTokenValue());
1042: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1043:
1044: $type = $this->typeParser->parseGeneric(
1045: $tokens,
1046: $this->typeParser->enrichWithAttributes($tokens, $baseType, $startLine, $startIndex),
1047: );
1048:
1049: $description = $this->parseOptionalDescription($tokens, true);
1050:
1051: switch ($tagName) {
1052: case '@extends':
1053: return new Ast\PhpDoc\ExtendsTagValueNode($type, $description);
1054: case '@implements':
1055: return new Ast\PhpDoc\ImplementsTagValueNode($type, $description);
1056: case '@use':
1057: return new Ast\PhpDoc\UsesTagValueNode($type, $description);
1058: }
1059:
1060: throw new ShouldNotHappenException();
1061: }
1062:
1063: private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasTagValueNode
1064: {
1065: $alias = $tokens->currentTokenValue();
1066: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1067:
1068: // support phan-type/psalm-type syntax
1069: $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
1070:
1071: $startLine = $tokens->currentTokenLine();
1072: $startIndex = $tokens->currentTokenIndex();
1073: try {
1074: $type = $this->typeParser->parse($tokens);
1075: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
1076: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
1077: throw new ParserException(
1078: $tokens->currentTokenValue(),
1079: $tokens->currentTokenType(),
1080: $tokens->currentTokenOffset(),
1081: Lexer::TOKEN_PHPDOC_EOL,
1082: null,
1083: $tokens->currentTokenLine(),
1084: );
1085: }
1086: }
1087:
1088: return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
1089: } catch (ParserException $e) {
1090: $this->parseOptionalDescription($tokens, false);
1091: return new Ast\PhpDoc\TypeAliasTagValueNode(
1092: $alias,
1093: $this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex),
1094: );
1095: }
1096: }
1097:
1098: private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode
1099: {
1100: $importedAlias = $tokens->currentTokenValue();
1101: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1102:
1103: $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from');
1104:
1105: $identifierStartLine = $tokens->currentTokenLine();
1106: $identifierStartIndex = $tokens->currentTokenIndex();
1107: $importedFrom = $tokens->currentTokenValue();
1108: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1109: $importedFromType = $this->enrichWithAttributes(
1110: $tokens,
1111: new IdentifierTypeNode($importedFrom),
1112: $identifierStartLine,
1113: $identifierStartIndex,
1114: );
1115:
1116: $importedAs = null;
1117: if ($tokens->tryConsumeTokenValue('as')) {
1118: $importedAs = $tokens->currentTokenValue();
1119: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1120: }
1121:
1122: return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, $importedFromType, $importedAs);
1123: }
1124:
1125: /**
1126: * @return Ast\PhpDoc\AssertTagValueNode|Ast\PhpDoc\AssertTagPropertyValueNode|Ast\PhpDoc\AssertTagMethodValueNode
1127: */
1128: private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
1129: {
1130: $isNegated = $tokens->tryConsumeTokenType(Lexer::TOKEN_NEGATED);
1131: $isEquality = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
1132: $type = $this->typeParser->parse($tokens);
1133: $parameter = $this->parseAssertParameter($tokens);
1134: $description = $this->parseOptionalDescription($tokens, false);
1135:
1136: if (array_key_exists('method', $parameter)) {
1137: return new Ast\PhpDoc\AssertTagMethodValueNode($type, $parameter['parameter'], $parameter['method'], $isNegated, $description, $isEquality);
1138: } elseif (array_key_exists('property', $parameter)) {
1139: return new Ast\PhpDoc\AssertTagPropertyValueNode($type, $parameter['parameter'], $parameter['property'], $isNegated, $description, $isEquality);
1140: }
1141:
1142: return new Ast\PhpDoc\AssertTagValueNode($type, $parameter['parameter'], $isNegated, $description, $isEquality);
1143: }
1144:
1145: /**
1146: * @return array{parameter: string}|array{parameter: string, property: string}|array{parameter: string, method: string}
1147: */
1148: private function parseAssertParameter(TokenIterator $tokens): array
1149: {
1150: if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
1151: $parameter = '$this';
1152: $tokens->next();
1153: } else {
1154: $parameter = $tokens->currentTokenValue();
1155: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
1156: }
1157:
1158: if ($tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) {
1159: $tokens->consumeTokenType(Lexer::TOKEN_ARROW);
1160:
1161: $propertyOrMethod = $tokens->currentTokenValue();
1162: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1163:
1164: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
1165: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
1166:
1167: return ['parameter' => $parameter, 'method' => $propertyOrMethod];
1168: }
1169:
1170: return ['parameter' => $parameter, 'property' => $propertyOrMethod];
1171: }
1172:
1173: return ['parameter' => $parameter];
1174: }
1175:
1176: private function parseSelfOutTagValue(TokenIterator $tokens): Ast\PhpDoc\SelfOutTagValueNode
1177: {
1178: $type = $this->typeParser->parse($tokens);
1179: $description = $this->parseOptionalDescription($tokens, true);
1180:
1181: return new Ast\PhpDoc\SelfOutTagValueNode($type, $description);
1182: }
1183:
1184: private function parseParamOutTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamOutTagValueNode
1185: {
1186: $type = $this->typeParser->parse($tokens);
1187: $parameterName = $this->parseRequiredVariableName($tokens);
1188: $description = $this->parseOptionalDescription($tokens, false);
1189:
1190: return new Ast\PhpDoc\ParamOutTagValueNode($type, $parameterName, $description);
1191: }
1192:
1193: private function parseOptionalVariableName(TokenIterator $tokens): string
1194: {
1195: if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
1196: $parameterName = $tokens->currentTokenValue();
1197: $tokens->next();
1198: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
1199: $parameterName = '$this';
1200: $tokens->next();
1201:
1202: } else {
1203: $parameterName = '';
1204: }
1205:
1206: return $parameterName;
1207: }
1208:
1209:
1210: private function parseRequiredVariableName(TokenIterator $tokens): string
1211: {
1212: $parameterName = $tokens->currentTokenValue();
1213: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
1214:
1215: return $parameterName;
1216: }
1217:
1218: /**
1219: * @param bool $limitStartToken true should be used when the description immediately follows a parsed type
1220: */
1221: private function parseOptionalDescription(TokenIterator $tokens, bool $limitStartToken): string
1222: {
1223: if ($limitStartToken) {
1224: foreach (self::DISALLOWED_DESCRIPTION_START_TOKENS as $disallowedStartToken) {
1225: if (!$tokens->isCurrentTokenType($disallowedStartToken)) {
1226: continue;
1227: }
1228:
1229: $tokens->consumeTokenType(Lexer::TOKEN_OTHER); // will throw exception
1230: }
1231:
1232: if (
1233: !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)
1234: && !$tokens->isPrecededByHorizontalWhitespace()
1235: ) {
1236: $tokens->consumeTokenType(Lexer::TOKEN_HORIZONTAL_WS); // will throw exception
1237: }
1238: }
1239:
1240: return $this->parseText($tokens)->text;
1241: }
1242:
1243: }
1244: