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