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