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