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 '@psalm-inheritors':
407: case '@phpstan-sealed':
408: $tagValue = $this->parseSealedTagValue($tokens);
409: break;
410:
411: case '@deprecated':
412: $tagValue = $this->parseDeprecatedTagValue($tokens);
413: break;
414:
415: case '@property':
416: case '@property-read':
417: case '@property-write':
418: case '@phpstan-property':
419: case '@phpstan-property-read':
420: case '@phpstan-property-write':
421: case '@psalm-property':
422: case '@psalm-property-read':
423: case '@psalm-property-write':
424: case '@phan-property':
425: case '@phan-property-read':
426: case '@phan-property-write':
427: $tagValue = $this->parsePropertyTagValue($tokens);
428: break;
429:
430: case '@method':
431: case '@phpstan-method':
432: case '@psalm-method':
433: case '@phan-method':
434: $tagValue = $this->parseMethodTagValue($tokens);
435: break;
436:
437: case '@template':
438: case '@phpstan-template':
439: case '@psalm-template':
440: case '@phan-template':
441: case '@template-covariant':
442: case '@phpstan-template-covariant':
443: case '@psalm-template-covariant':
444: case '@template-contravariant':
445: case '@phpstan-template-contravariant':
446: case '@psalm-template-contravariant':
447: $tagValue = $this->typeParser->parseTemplateTagValue(
448: $tokens,
449: fn ($tokens) => $this->parseOptionalDescription($tokens, true),
450: );
451: break;
452:
453: case '@extends':
454: case '@phpstan-extends':
455: case '@phan-extends':
456: case '@phan-inherits':
457: case '@template-extends':
458: $tagValue = $this->parseExtendsTagValue('@extends', $tokens);
459: break;
460:
461: case '@implements':
462: case '@phpstan-implements':
463: case '@template-implements':
464: $tagValue = $this->parseExtendsTagValue('@implements', $tokens);
465: break;
466:
467: case '@use':
468: case '@phpstan-use':
469: case '@template-use':
470: $tagValue = $this->parseExtendsTagValue('@use', $tokens);
471: break;
472:
473: case '@phpstan-type':
474: case '@psalm-type':
475: case '@phan-type':
476: $tagValue = $this->parseTypeAliasTagValue($tokens);
477: break;
478:
479: case '@phpstan-import-type':
480: case '@psalm-import-type':
481: $tagValue = $this->parseTypeAliasImportTagValue($tokens);
482: break;
483:
484: case '@phpstan-assert':
485: case '@phpstan-assert-if-true':
486: case '@phpstan-assert-if-false':
487: case '@psalm-assert':
488: case '@psalm-assert-if-true':
489: case '@psalm-assert-if-false':
490: case '@phan-assert':
491: case '@phan-assert-if-true':
492: case '@phan-assert-if-false':
493: $tagValue = $this->parseAssertTagValue($tokens);
494: break;
495:
496: case '@phpstan-this-out':
497: case '@phpstan-self-out':
498: case '@psalm-this-out':
499: case '@psalm-self-out':
500: $tagValue = $this->parseSelfOutTagValue($tokens);
501: break;
502:
503: case '@param-out':
504: case '@phpstan-param-out':
505: case '@psalm-param-out':
506: $tagValue = $this->parseParamOutTagValue($tokens);
507: break;
508:
509: default:
510: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
511: $tagValue = $this->parseDoctrineTagValue($tokens, $tag);
512: } else {
513: $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescriptionAfterDoctrineTag($tokens));
514: }
515: break;
516: }
517:
518: $tokens->dropSavePoint();
519:
520: } catch (ParserException $e) {
521: $tokens->rollback();
522: $tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens, false), $e);
523: }
524:
525: return $this->enrichWithAttributes($tokens, $tagValue, $startLine, $startIndex);
526: }
527:
528:
529: private function parseDoctrineTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode
530: {
531: $startLine = $tokens->currentTokenLine();
532: $startIndex = $tokens->currentTokenIndex();
533:
534: return new Doctrine\DoctrineTagValueNode(
535: $this->enrichWithAttributes(
536: $tokens,
537: new Doctrine\DoctrineAnnotation($tag, $this->parseDoctrineArguments($tokens, false)),
538: $startLine,
539: $startIndex,
540: ),
541: $this->parseOptionalDescriptionAfterDoctrineTag($tokens),
542: );
543: }
544:
545:
546: /**
547: * @return list<Doctrine\DoctrineArgument>
548: */
549: private function parseDoctrineArguments(TokenIterator $tokens, bool $deep): array
550: {
551: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
552: return [];
553: }
554:
555: if (!$deep) {
556: $tokens->addEndOfLineToSkippedTokens();
557: }
558:
559: $arguments = [];
560:
561: try {
562: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
563:
564: do {
565: if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
566: break;
567: }
568: $arguments[] = $this->parseDoctrineArgument($tokens);
569: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
570: } finally {
571: if (!$deep) {
572: $tokens->removeEndOfLineFromSkippedTokens();
573: }
574: }
575:
576: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
577:
578: return $arguments;
579: }
580:
581:
582: private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\DoctrineArgument
583: {
584: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
585: $startLine = $tokens->currentTokenLine();
586: $startIndex = $tokens->currentTokenIndex();
587:
588: return $this->enrichWithAttributes(
589: $tokens,
590: new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
591: $startLine,
592: $startIndex,
593: );
594: }
595:
596: $startLine = $tokens->currentTokenLine();
597: $startIndex = $tokens->currentTokenIndex();
598:
599: try {
600: $tokens->pushSavePoint();
601: $currentValue = $tokens->currentTokenValue();
602: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
603:
604: $key = $this->enrichWithAttributes(
605: $tokens,
606: new IdentifierTypeNode($currentValue),
607: $startLine,
608: $startIndex,
609: );
610: $tokens->consumeTokenType(Lexer::TOKEN_EQUAL);
611:
612: $value = $this->parseDoctrineArgumentValue($tokens);
613:
614: $tokens->dropSavePoint();
615:
616: return $this->enrichWithAttributes(
617: $tokens,
618: new Doctrine\DoctrineArgument($key, $value),
619: $startLine,
620: $startIndex,
621: );
622: } catch (ParserException $e) {
623: $tokens->rollback();
624:
625: return $this->enrichWithAttributes(
626: $tokens,
627: new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
628: $startLine,
629: $startIndex,
630: );
631: }
632: }
633:
634:
635: /**
636: * @return DoctrineValueType
637: */
638: private function parseDoctrineArgumentValue(TokenIterator $tokens)
639: {
640: $startLine = $tokens->currentTokenLine();
641: $startIndex = $tokens->currentTokenIndex();
642:
643: if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG)) {
644: $name = $tokens->currentTokenValue();
645: $tokens->next();
646:
647: return $this->enrichWithAttributes(
648: $tokens,
649: new Doctrine\DoctrineAnnotation($name, $this->parseDoctrineArguments($tokens, true)),
650: $startLine,
651: $startIndex,
652: );
653: }
654:
655: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
656: $items = [];
657: do {
658: if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
659: break;
660: }
661: $items[] = $this->parseDoctrineArrayItem($tokens);
662: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
663:
664: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
665:
666: return $this->enrichWithAttributes(
667: $tokens,
668: new Doctrine\DoctrineArray($items),
669: $startLine,
670: $startIndex,
671: );
672: }
673:
674: $currentTokenValue = $tokens->currentTokenValue();
675: $tokens->pushSavePoint(); // because of ConstFetchNode
676: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
677: $identifier = $this->enrichWithAttributes(
678: $tokens,
679: new Ast\Type\IdentifierTypeNode($currentTokenValue),
680: $startLine,
681: $startIndex,
682: );
683: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
684: $tokens->dropSavePoint();
685: return $identifier;
686: }
687:
688: $tokens->rollback(); // because of ConstFetchNode
689: } else {
690: $tokens->dropSavePoint(); // because of ConstFetchNode
691: }
692:
693: $currentTokenValue = $tokens->currentTokenValue();
694: $currentTokenType = $tokens->currentTokenType();
695: $currentTokenOffset = $tokens->currentTokenOffset();
696: $currentTokenLine = $tokens->currentTokenLine();
697:
698: try {
699: $constExpr = $this->doctrineConstantExprParser->parse($tokens);
700: if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
701: throw new ParserException(
702: $currentTokenValue,
703: $currentTokenType,
704: $currentTokenOffset,
705: Lexer::TOKEN_IDENTIFIER,
706: null,
707: $currentTokenLine,
708: );
709: }
710:
711: return $constExpr;
712: } catch (LogicException $e) {
713: throw new ParserException(
714: $currentTokenValue,
715: $currentTokenType,
716: $currentTokenOffset,
717: Lexer::TOKEN_IDENTIFIER,
718: null,
719: $currentTokenLine,
720: );
721: }
722: }
723:
724:
725: private function parseDoctrineArrayItem(TokenIterator $tokens): Doctrine\DoctrineArrayItem
726: {
727: $startLine = $tokens->currentTokenLine();
728: $startIndex = $tokens->currentTokenIndex();
729:
730: try {
731: $tokens->pushSavePoint();
732:
733: $key = $this->parseDoctrineArrayKey($tokens);
734: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
735: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COLON)) {
736: $tokens->consumeTokenType(Lexer::TOKEN_EQUAL); // will throw exception
737: }
738: }
739:
740: $value = $this->parseDoctrineArgumentValue($tokens);
741:
742: $tokens->dropSavePoint();
743:
744: return $this->enrichWithAttributes(
745: $tokens,
746: new Doctrine\DoctrineArrayItem($key, $value),
747: $startLine,
748: $startIndex,
749: );
750: } catch (ParserException $e) {
751: $tokens->rollback();
752:
753: return $this->enrichWithAttributes(
754: $tokens,
755: new Doctrine\DoctrineArrayItem(null, $this->parseDoctrineArgumentValue($tokens)),
756: $startLine,
757: $startIndex,
758: );
759: }
760: }
761:
762:
763: /**
764: * @return ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|ConstFetchNode
765: */
766: private function parseDoctrineArrayKey(TokenIterator $tokens)
767: {
768: $startLine = $tokens->currentTokenLine();
769: $startIndex = $tokens->currentTokenIndex();
770:
771: if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
772: $key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
773: $tokens->next();
774:
775: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
776: $key = $this->doctrineConstantExprParser->parseDoctrineString($tokens->currentTokenValue(), $tokens);
777:
778: $tokens->next();
779:
780: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
781: $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED);
782: $tokens->next();
783:
784: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
785: $value = $tokens->currentTokenValue();
786: $tokens->next();
787: $key = $this->doctrineConstantExprParser->parseDoctrineString($value, $tokens);
788:
789: } else {
790: $currentTokenValue = $tokens->currentTokenValue();
791: $tokens->pushSavePoint(); // because of ConstFetchNode
792: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
793: $tokens->dropSavePoint();
794: throw new ParserException(
795: $tokens->currentTokenValue(),
796: $tokens->currentTokenType(),
797: $tokens->currentTokenOffset(),
798: Lexer::TOKEN_IDENTIFIER,
799: null,
800: $tokens->currentTokenLine(),
801: );
802: }
803:
804: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
805: $tokens->dropSavePoint();
806:
807: return $this->enrichWithAttributes(
808: $tokens,
809: new IdentifierTypeNode($currentTokenValue),
810: $startLine,
811: $startIndex,
812: );
813: }
814:
815: $tokens->rollback();
816: $constExpr = $this->doctrineConstantExprParser->parse($tokens);
817: if (!$constExpr instanceof Ast\ConstExpr\ConstFetchNode) {
818: throw new ParserException(
819: $tokens->currentTokenValue(),
820: $tokens->currentTokenType(),
821: $tokens->currentTokenOffset(),
822: Lexer::TOKEN_IDENTIFIER,
823: null,
824: $tokens->currentTokenLine(),
825: );
826: }
827:
828: return $constExpr;
829: }
830:
831: return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
832: }
833:
834:
835: /**
836: * @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode
837: */
838: private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
839: {
840: if (
841: $tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE, Lexer::TOKEN_VARIADIC, Lexer::TOKEN_VARIABLE)
842: ) {
843: $type = null;
844: } else {
845: $type = $this->typeParser->parse($tokens);
846: }
847:
848: $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
849: $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
850: $parameterName = $this->parseRequiredVariableName($tokens);
851: $description = $this->parseOptionalDescription($tokens, false);
852:
853: if ($type !== null) {
854: return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference);
855: }
856:
857: return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference);
858: }
859:
860:
861: private function parseParamImmediatelyInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode
862: {
863: $parameterName = $this->parseRequiredVariableName($tokens);
864: $description = $this->parseOptionalDescription($tokens, false);
865:
866: return new Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode($parameterName, $description);
867: }
868:
869:
870: private function parseParamLaterInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode
871: {
872: $parameterName = $this->parseRequiredVariableName($tokens);
873: $description = $this->parseOptionalDescription($tokens, false);
874:
875: return new Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode($parameterName, $description);
876: }
877:
878:
879: private function parseParamClosureThisTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamClosureThisTagValueNode
880: {
881: $type = $this->typeParser->parse($tokens);
882: $parameterName = $this->parseRequiredVariableName($tokens);
883: $description = $this->parseOptionalDescription($tokens, false);
884:
885: return new Ast\PhpDoc\ParamClosureThisTagValueNode($type, $parameterName, $description);
886: }
887:
888: private function parsePureUnlessCallableIsImpureTagValue(TokenIterator $tokens): Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode
889: {
890: $parameterName = $this->parseRequiredVariableName($tokens);
891: $description = $this->parseOptionalDescription($tokens, false);
892:
893: return new Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode($parameterName, $description);
894: }
895:
896: private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode
897: {
898: $type = $this->typeParser->parse($tokens);
899: $variableName = $this->parseOptionalVariableName($tokens);
900: $description = $this->parseOptionalDescription($tokens, $variableName === '');
901: return new Ast\PhpDoc\VarTagValueNode($type, $variableName, $description);
902: }
903:
904:
905: private function parseReturnTagValue(TokenIterator $tokens): Ast\PhpDoc\ReturnTagValueNode
906: {
907: $type = $this->typeParser->parse($tokens);
908: $description = $this->parseOptionalDescription($tokens, true);
909: return new Ast\PhpDoc\ReturnTagValueNode($type, $description);
910: }
911:
912:
913: private function parseThrowsTagValue(TokenIterator $tokens): Ast\PhpDoc\ThrowsTagValueNode
914: {
915: $type = $this->typeParser->parse($tokens);
916: $description = $this->parseOptionalDescription($tokens, true);
917: return new Ast\PhpDoc\ThrowsTagValueNode($type, $description);
918: }
919:
920: private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagValueNode
921: {
922: $type = $this->typeParser->parse($tokens);
923: $description = $this->parseOptionalDescription($tokens, true);
924: return new Ast\PhpDoc\MixinTagValueNode($type, $description);
925: }
926:
927: private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode
928: {
929: $type = $this->typeParser->parse($tokens);
930: $description = $this->parseOptionalDescription($tokens, true);
931: return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description);
932: }
933:
934: private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode
935: {
936: $type = $this->typeParser->parse($tokens);
937: $description = $this->parseOptionalDescription($tokens, true);
938: return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description);
939: }
940:
941: private function parseSealedTagValue(TokenIterator $tokens): Ast\PhpDoc\SealedTagValueNode
942: {
943: $type = $this->typeParser->parse($tokens);
944: $description = $this->parseOptionalDescription($tokens, true);
945: return new Ast\PhpDoc\SealedTagValueNode($type, $description);
946: }
947:
948: private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
949: {
950: $description = $this->parseOptionalDescription($tokens, false);
951: return new Ast\PhpDoc\DeprecatedTagValueNode($description);
952: }
953:
954:
955: private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\PropertyTagValueNode
956: {
957: $type = $this->typeParser->parse($tokens);
958: $parameterName = $this->parseRequiredVariableName($tokens);
959: $description = $this->parseOptionalDescription($tokens, false);
960: return new Ast\PhpDoc\PropertyTagValueNode($type, $parameterName, $description);
961: }
962:
963:
964: private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode
965: {
966: $staticKeywordOrReturnTypeOrMethodName = $this->typeParser->parse($tokens);
967:
968: if ($staticKeywordOrReturnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode && $staticKeywordOrReturnTypeOrMethodName->name === 'static') {
969: $isStatic = true;
970: $returnTypeOrMethodName = $this->typeParser->parse($tokens);
971:
972: } else {
973: $isStatic = false;
974: $returnTypeOrMethodName = $staticKeywordOrReturnTypeOrMethodName;
975: }
976:
977: if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
978: $returnType = $returnTypeOrMethodName;
979: $methodName = $tokens->currentTokenValue();
980: $tokens->next();
981:
982: } elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) {
983: $returnType = $isStatic ? $staticKeywordOrReturnTypeOrMethodName : null;
984: $methodName = $returnTypeOrMethodName->name;
985: $isStatic = false;
986:
987: } else {
988: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); // will throw exception
989: exit;
990: }
991:
992: $templateTypes = [];
993:
994: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
995: do {
996: $startLine = $tokens->currentTokenLine();
997: $startIndex = $tokens->currentTokenIndex();
998: $templateTypes[] = $this->enrichWithAttributes(
999: $tokens,
1000: $this->typeParser->parseTemplateTagValue($tokens),
1001: $startLine,
1002: $startIndex,
1003: );
1004: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
1005: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
1006: }
1007:
1008: $parameters = [];
1009: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
1010: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
1011: $parameters[] = $this->parseMethodTagValueParameter($tokens);
1012: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
1013: $parameters[] = $this->parseMethodTagValueParameter($tokens);
1014: }
1015: }
1016: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
1017:
1018: $description = $this->parseOptionalDescription($tokens, false);
1019: return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes);
1020: }
1021:
1022: private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
1023: {
1024: $startLine = $tokens->currentTokenLine();
1025: $startIndex = $tokens->currentTokenIndex();
1026:
1027: switch ($tokens->currentTokenType()) {
1028: case Lexer::TOKEN_IDENTIFIER:
1029: case Lexer::TOKEN_OPEN_PARENTHESES:
1030: case Lexer::TOKEN_NULLABLE:
1031: $parameterType = $this->typeParser->parse($tokens);
1032: break;
1033:
1034: default:
1035: $parameterType = null;
1036: }
1037:
1038: $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
1039: $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
1040:
1041: $parameterName = $tokens->currentTokenValue();
1042: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
1043:
1044: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
1045: $defaultValue = $this->constantExprParser->parse($tokens);
1046:
1047: } else {
1048: $defaultValue = null;
1049: }
1050:
1051: return $this->enrichWithAttributes(
1052: $tokens,
1053: new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue),
1054: $startLine,
1055: $startIndex,
1056: );
1057: }
1058:
1059: private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
1060: {
1061: $startLine = $tokens->currentTokenLine();
1062: $startIndex = $tokens->currentTokenIndex();
1063: $baseType = new IdentifierTypeNode($tokens->currentTokenValue());
1064: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1065:
1066: $type = $this->typeParser->parseGeneric(
1067: $tokens,
1068: $this->typeParser->enrichWithAttributes($tokens, $baseType, $startLine, $startIndex),
1069: );
1070:
1071: $description = $this->parseOptionalDescription($tokens, true);
1072:
1073: switch ($tagName) {
1074: case '@extends':
1075: return new Ast\PhpDoc\ExtendsTagValueNode($type, $description);
1076: case '@implements':
1077: return new Ast\PhpDoc\ImplementsTagValueNode($type, $description);
1078: case '@use':
1079: return new Ast\PhpDoc\UsesTagValueNode($type, $description);
1080: }
1081:
1082: throw new ShouldNotHappenException();
1083: }
1084:
1085: private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasTagValueNode
1086: {
1087: $alias = $tokens->currentTokenValue();
1088: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1089:
1090: // support phan-type/psalm-type syntax
1091: $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
1092:
1093: $startLine = $tokens->currentTokenLine();
1094: $startIndex = $tokens->currentTokenIndex();
1095: try {
1096: $type = $this->typeParser->parse($tokens);
1097: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
1098: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
1099: throw new ParserException(
1100: $tokens->currentTokenValue(),
1101: $tokens->currentTokenType(),
1102: $tokens->currentTokenOffset(),
1103: Lexer::TOKEN_PHPDOC_EOL,
1104: null,
1105: $tokens->currentTokenLine(),
1106: );
1107: }
1108: }
1109:
1110: return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
1111: } catch (ParserException $e) {
1112: $this->parseOptionalDescription($tokens, false);
1113: return new Ast\PhpDoc\TypeAliasTagValueNode(
1114: $alias,
1115: $this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex),
1116: );
1117: }
1118: }
1119:
1120: private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode
1121: {
1122: $importedAlias = $tokens->currentTokenValue();
1123: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1124:
1125: $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from');
1126:
1127: $identifierStartLine = $tokens->currentTokenLine();
1128: $identifierStartIndex = $tokens->currentTokenIndex();
1129: $importedFrom = $tokens->currentTokenValue();
1130: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1131: $importedFromType = $this->enrichWithAttributes(
1132: $tokens,
1133: new IdentifierTypeNode($importedFrom),
1134: $identifierStartLine,
1135: $identifierStartIndex,
1136: );
1137:
1138: $importedAs = null;
1139: if ($tokens->tryConsumeTokenValue('as')) {
1140: $importedAs = $tokens->currentTokenValue();
1141: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1142: }
1143:
1144: return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, $importedFromType, $importedAs);
1145: }
1146:
1147: /**
1148: * @return Ast\PhpDoc\AssertTagValueNode|Ast\PhpDoc\AssertTagPropertyValueNode|Ast\PhpDoc\AssertTagMethodValueNode
1149: */
1150: private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
1151: {
1152: $isNegated = $tokens->tryConsumeTokenType(Lexer::TOKEN_NEGATED);
1153: $isEquality = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
1154: $type = $this->typeParser->parse($tokens);
1155: $parameter = $this->parseAssertParameter($tokens);
1156: $description = $this->parseOptionalDescription($tokens, false);
1157:
1158: if (array_key_exists('method', $parameter)) {
1159: return new Ast\PhpDoc\AssertTagMethodValueNode($type, $parameter['parameter'], $parameter['method'], $isNegated, $description, $isEquality);
1160: } elseif (array_key_exists('property', $parameter)) {
1161: return new Ast\PhpDoc\AssertTagPropertyValueNode($type, $parameter['parameter'], $parameter['property'], $isNegated, $description, $isEquality);
1162: }
1163:
1164: return new Ast\PhpDoc\AssertTagValueNode($type, $parameter['parameter'], $isNegated, $description, $isEquality);
1165: }
1166:
1167: /**
1168: * @return array{parameter: string}|array{parameter: string, property: string}|array{parameter: string, method: string}
1169: */
1170: private function parseAssertParameter(TokenIterator $tokens): array
1171: {
1172: if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
1173: $parameter = '$this';
1174: $tokens->next();
1175: } else {
1176: $parameter = $tokens->currentTokenValue();
1177: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
1178: }
1179:
1180: if ($tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) {
1181: $tokens->consumeTokenType(Lexer::TOKEN_ARROW);
1182:
1183: $propertyOrMethod = $tokens->currentTokenValue();
1184: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1185:
1186: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
1187: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
1188:
1189: return ['parameter' => $parameter, 'method' => $propertyOrMethod];
1190: }
1191:
1192: return ['parameter' => $parameter, 'property' => $propertyOrMethod];
1193: }
1194:
1195: return ['parameter' => $parameter];
1196: }
1197:
1198: private function parseSelfOutTagValue(TokenIterator $tokens): Ast\PhpDoc\SelfOutTagValueNode
1199: {
1200: $type = $this->typeParser->parse($tokens);
1201: $description = $this->parseOptionalDescription($tokens, true);
1202:
1203: return new Ast\PhpDoc\SelfOutTagValueNode($type, $description);
1204: }
1205:
1206: private function parseParamOutTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamOutTagValueNode
1207: {
1208: $type = $this->typeParser->parse($tokens);
1209: $parameterName = $this->parseRequiredVariableName($tokens);
1210: $description = $this->parseOptionalDescription($tokens, false);
1211:
1212: return new Ast\PhpDoc\ParamOutTagValueNode($type, $parameterName, $description);
1213: }
1214:
1215: private function parseOptionalVariableName(TokenIterator $tokens): string
1216: {
1217: if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
1218: $parameterName = $tokens->currentTokenValue();
1219: $tokens->next();
1220: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
1221: $parameterName = '$this';
1222: $tokens->next();
1223:
1224: } else {
1225: $parameterName = '';
1226: }
1227:
1228: return $parameterName;
1229: }
1230:
1231:
1232: private function parseRequiredVariableName(TokenIterator $tokens): string
1233: {
1234: $parameterName = $tokens->currentTokenValue();
1235: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
1236:
1237: return $parameterName;
1238: }
1239:
1240: /**
1241: * @param bool $limitStartToken true should be used when the description immediately follows a parsed type
1242: */
1243: private function parseOptionalDescription(TokenIterator $tokens, bool $limitStartToken): string
1244: {
1245: if ($limitStartToken) {
1246: foreach (self::DISALLOWED_DESCRIPTION_START_TOKENS as $disallowedStartToken) {
1247: if (!$tokens->isCurrentTokenType($disallowedStartToken)) {
1248: continue;
1249: }
1250:
1251: $tokens->consumeTokenType(Lexer::TOKEN_OTHER); // will throw exception
1252: }
1253:
1254: if (
1255: !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)
1256: && !$tokens->isPrecededByHorizontalWhitespace()
1257: ) {
1258: $tokens->consumeTokenType(Lexer::TOKEN_HORIZONTAL_WS); // will throw exception
1259: }
1260: }
1261:
1262: return $this->parseText($tokens)->text;
1263: }
1264:
1265: }
1266: