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