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