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