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