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