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