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: | |
23: | |
24: | class PhpDocParser |
25: | { |
26: | |
27: | private const DISALLOWED_DESCRIPTION_START_TOKENS = [ |
28: | Lexer::TOKEN_UNION, |
29: | Lexer::TOKEN_INTERSECTION, |
30: | ]; |
31: | |
32: | |
33: | private $typeParser; |
34: | |
35: | |
36: | private $constantExprParser; |
37: | |
38: | |
39: | private $doctrineConstantExprParser; |
40: | |
41: | |
42: | private $requireWhitespaceBeforeDescription; |
43: | |
44: | |
45: | private $preserveTypeAliasesWithInvalidTypes; |
46: | |
47: | |
48: | private $parseDoctrineAnnotations; |
49: | |
50: | |
51: | private $useLinesAttributes; |
52: | |
53: | |
54: | private $useIndexAttributes; |
55: | |
56: | |
57: | private $textBetweenTagsBelongsToDescription; |
58: | |
59: | |
60: | |
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: | |
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: | |
204: | |
205: | |
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: | |
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: | |
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: | |
258: | |
259: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) { |
260: | $tokens->rollback(); |
261: | break; |
262: | } |
263: | |
264: | |
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: | |
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: | |
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: | |
344: | |
345: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) { |
346: | $tokens->rollback(); |
347: | break; |
348: | } |
349: | |
350: | |
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: | |
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: | |
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(); |
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(); |
732: | } else { |
733: | $tokens->dropSavePoint(); |
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); |
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: | |
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(); |
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: | |
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); |
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: | |
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: | |
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: | |
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); |
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); |
1295: | } |
1296: | } |
1297: | |
1298: | return $this->parseText($tokens)->text; |
1299: | } |
1300: | |
1301: | } |
1302: | |