1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace PHPStan\PhpDocParser\Parser; |
4: | |
5: | use PHPStan\PhpDocParser\Ast; |
6: | use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; |
7: | use PHPStan\PhpDocParser\Lexer\Lexer; |
8: | use PHPStan\ShouldNotHappenException; |
9: | use function array_key_exists; |
10: | use function array_values; |
11: | use function count; |
12: | use function trim; |
13: | |
14: | class PhpDocParser |
15: | { |
16: | |
17: | private const DISALLOWED_DESCRIPTION_START_TOKENS = [ |
18: | Lexer::TOKEN_UNION, |
19: | Lexer::TOKEN_INTERSECTION, |
20: | ]; |
21: | |
22: | |
23: | private $typeParser; |
24: | |
25: | |
26: | private $constantExprParser; |
27: | |
28: | |
29: | private $requireWhitespaceBeforeDescription; |
30: | |
31: | public function __construct(TypeParser $typeParser, ConstExprParser $constantExprParser, bool $requireWhitespaceBeforeDescription = false) |
32: | { |
33: | $this->typeParser = $typeParser; |
34: | $this->constantExprParser = $constantExprParser; |
35: | $this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription; |
36: | } |
37: | |
38: | |
39: | public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode |
40: | { |
41: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PHPDOC); |
42: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
43: | |
44: | $children = []; |
45: | |
46: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { |
47: | $children[] = $this->parseChild($tokens); |
48: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL) && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) { |
49: | $children[] = $this->parseChild($tokens); |
50: | } |
51: | } |
52: | |
53: | try { |
54: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC); |
55: | } catch (ParserException $e) { |
56: | $name = ''; |
57: | if (count($children) > 0) { |
58: | $lastChild = $children[count($children) - 1]; |
59: | if ($lastChild instanceof Ast\PhpDoc\PhpDocTagNode) { |
60: | $name = $lastChild->name; |
61: | } |
62: | } |
63: | $tokens->forwardToTheEnd(); |
64: | return new Ast\PhpDoc\PhpDocNode([ |
65: | new Ast\PhpDoc\PhpDocTagNode($name, new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e)), |
66: | ]); |
67: | } |
68: | |
69: | return new Ast\PhpDoc\PhpDocNode(array_values($children)); |
70: | } |
71: | |
72: | |
73: | private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode |
74: | { |
75: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) { |
76: | return $this->parseTag($tokens); |
77: | |
78: | } |
79: | |
80: | return $this->parseText($tokens); |
81: | } |
82: | |
83: | |
84: | private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode |
85: | { |
86: | $text = ''; |
87: | |
88: | while (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { |
89: | $text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END); |
90: | |
91: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { |
92: | break; |
93: | } |
94: | |
95: | $tokens->pushSavePoint(); |
96: | $tokens->next(); |
97: | |
98: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)) { |
99: | $tokens->rollback(); |
100: | break; |
101: | } |
102: | |
103: | $tokens->dropSavePoint(); |
104: | $text .= "\n"; |
105: | } |
106: | |
107: | return new Ast\PhpDoc\PhpDocTextNode(trim($text, " \t")); |
108: | } |
109: | |
110: | |
111: | public function parseTag(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagNode |
112: | { |
113: | $tag = $tokens->currentTokenValue(); |
114: | $tokens->next(); |
115: | $value = $this->parseTagValue($tokens, $tag); |
116: | |
117: | return new Ast\PhpDoc\PhpDocTagNode($tag, $value); |
118: | } |
119: | |
120: | |
121: | public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode |
122: | { |
123: | try { |
124: | $tokens->pushSavePoint(); |
125: | |
126: | switch ($tag) { |
127: | case '@param': |
128: | case '@phpstan-param': |
129: | case '@psalm-param': |
130: | $tagValue = $this->parseParamTagValue($tokens); |
131: | break; |
132: | |
133: | case '@var': |
134: | case '@phpstan-var': |
135: | case '@psalm-var': |
136: | $tagValue = $this->parseVarTagValue($tokens); |
137: | break; |
138: | |
139: | case '@return': |
140: | case '@phpstan-return': |
141: | case '@psalm-return': |
142: | $tagValue = $this->parseReturnTagValue($tokens); |
143: | break; |
144: | |
145: | case '@throws': |
146: | case '@phpstan-throws': |
147: | $tagValue = $this->parseThrowsTagValue($tokens); |
148: | break; |
149: | |
150: | case '@mixin': |
151: | $tagValue = $this->parseMixinTagValue($tokens); |
152: | break; |
153: | |
154: | case '@deprecated': |
155: | $tagValue = $this->parseDeprecatedTagValue($tokens); |
156: | break; |
157: | |
158: | case '@property': |
159: | case '@property-read': |
160: | case '@property-write': |
161: | case '@phpstan-property': |
162: | case '@phpstan-property-read': |
163: | case '@phpstan-property-write': |
164: | case '@psalm-property': |
165: | case '@psalm-property-read': |
166: | case '@psalm-property-write': |
167: | $tagValue = $this->parsePropertyTagValue($tokens); |
168: | break; |
169: | |
170: | case '@method': |
171: | case '@phpstan-method': |
172: | case '@psalm-method': |
173: | $tagValue = $this->parseMethodTagValue($tokens); |
174: | break; |
175: | |
176: | case '@template': |
177: | case '@phpstan-template': |
178: | case '@psalm-template': |
179: | case '@template-covariant': |
180: | case '@phpstan-template-covariant': |
181: | case '@psalm-template-covariant': |
182: | case '@template-contravariant': |
183: | case '@phpstan-template-contravariant': |
184: | case '@psalm-template-contravariant': |
185: | $tagValue = $this->parseTemplateTagValue($tokens); |
186: | break; |
187: | |
188: | case '@extends': |
189: | case '@phpstan-extends': |
190: | case '@template-extends': |
191: | $tagValue = $this->parseExtendsTagValue('@extends', $tokens); |
192: | break; |
193: | |
194: | case '@implements': |
195: | case '@phpstan-implements': |
196: | case '@template-implements': |
197: | $tagValue = $this->parseExtendsTagValue('@implements', $tokens); |
198: | break; |
199: | |
200: | case '@use': |
201: | case '@phpstan-use': |
202: | case '@template-use': |
203: | $tagValue = $this->parseExtendsTagValue('@use', $tokens); |
204: | break; |
205: | |
206: | case '@phpstan-type': |
207: | case '@psalm-type': |
208: | $tagValue = $this->parseTypeAliasTagValue($tokens); |
209: | break; |
210: | |
211: | case '@phpstan-import-type': |
212: | case '@psalm-import-type': |
213: | $tagValue = $this->parseTypeAliasImportTagValue($tokens); |
214: | break; |
215: | |
216: | case '@phpstan-assert': |
217: | case '@phpstan-assert-if-true': |
218: | case '@phpstan-assert-if-false': |
219: | case '@psalm-assert': |
220: | case '@psalm-assert-if-true': |
221: | case '@psalm-assert-if-false': |
222: | $tagValue = $this->parseAssertTagValue($tokens); |
223: | break; |
224: | |
225: | default: |
226: | $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens)); |
227: | break; |
228: | } |
229: | |
230: | $tokens->dropSavePoint(); |
231: | |
232: | } catch (ParserException $e) { |
233: | $tokens->rollback(); |
234: | $tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens), $e); |
235: | } |
236: | |
237: | return $tagValue; |
238: | } |
239: | |
240: | |
241: | |
242: | |
243: | |
244: | private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode |
245: | { |
246: | if ( |
247: | $tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE) |
248: | || $tokens->isCurrentTokenType(Lexer::TOKEN_VARIADIC) |
249: | || $tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE) |
250: | ) { |
251: | $type = null; |
252: | } else { |
253: | $type = $this->typeParser->parse($tokens); |
254: | } |
255: | |
256: | $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); |
257: | $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); |
258: | $parameterName = $this->parseRequiredVariableName($tokens); |
259: | $description = $this->parseOptionalDescription($tokens); |
260: | |
261: | if ($type !== null) { |
262: | return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference); |
263: | } |
264: | |
265: | return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference); |
266: | } |
267: | |
268: | |
269: | private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode |
270: | { |
271: | $type = $this->typeParser->parse($tokens); |
272: | $variableName = $this->parseOptionalVariableName($tokens); |
273: | $description = $this->parseOptionalDescription($tokens, $variableName === ''); |
274: | return new Ast\PhpDoc\VarTagValueNode($type, $variableName, $description); |
275: | } |
276: | |
277: | |
278: | private function parseReturnTagValue(TokenIterator $tokens): Ast\PhpDoc\ReturnTagValueNode |
279: | { |
280: | $type = $this->typeParser->parse($tokens); |
281: | $description = $this->parseOptionalDescription($tokens, true); |
282: | return new Ast\PhpDoc\ReturnTagValueNode($type, $description); |
283: | } |
284: | |
285: | |
286: | private function parseThrowsTagValue(TokenIterator $tokens): Ast\PhpDoc\ThrowsTagValueNode |
287: | { |
288: | $type = $this->typeParser->parse($tokens); |
289: | $description = $this->parseOptionalDescription($tokens, true); |
290: | return new Ast\PhpDoc\ThrowsTagValueNode($type, $description); |
291: | } |
292: | |
293: | private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagValueNode |
294: | { |
295: | $type = $this->typeParser->parse($tokens); |
296: | $description = $this->parseOptionalDescription($tokens, true); |
297: | return new Ast\PhpDoc\MixinTagValueNode($type, $description); |
298: | } |
299: | |
300: | private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode |
301: | { |
302: | $description = $this->parseOptionalDescription($tokens); |
303: | return new Ast\PhpDoc\DeprecatedTagValueNode($description); |
304: | } |
305: | |
306: | |
307: | private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\PropertyTagValueNode |
308: | { |
309: | $type = $this->typeParser->parse($tokens); |
310: | $parameterName = $this->parseRequiredVariableName($tokens); |
311: | $description = $this->parseOptionalDescription($tokens); |
312: | return new Ast\PhpDoc\PropertyTagValueNode($type, $parameterName, $description); |
313: | } |
314: | |
315: | |
316: | private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode |
317: | { |
318: | $isStatic = $tokens->tryConsumeTokenValue('static'); |
319: | $returnTypeOrMethodName = $this->typeParser->parse($tokens); |
320: | |
321: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { |
322: | $returnType = $returnTypeOrMethodName; |
323: | $methodName = $tokens->currentTokenValue(); |
324: | $tokens->next(); |
325: | |
326: | } elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) { |
327: | $returnType = $isStatic ? new Ast\Type\IdentifierTypeNode('static') : null; |
328: | $methodName = $returnTypeOrMethodName->name; |
329: | $isStatic = false; |
330: | |
331: | } else { |
332: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
333: | exit; |
334: | } |
335: | |
336: | $parameters = []; |
337: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); |
338: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { |
339: | $parameters[] = $this->parseMethodTagValueParameter($tokens); |
340: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { |
341: | $parameters[] = $this->parseMethodTagValueParameter($tokens); |
342: | } |
343: | } |
344: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); |
345: | |
346: | $description = $this->parseOptionalDescription($tokens); |
347: | return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description); |
348: | } |
349: | |
350: | |
351: | private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode |
352: | { |
353: | switch ($tokens->currentTokenType()) { |
354: | case Lexer::TOKEN_IDENTIFIER: |
355: | case Lexer::TOKEN_OPEN_PARENTHESES: |
356: | case Lexer::TOKEN_NULLABLE: |
357: | $parameterType = $this->typeParser->parse($tokens); |
358: | break; |
359: | |
360: | default: |
361: | $parameterType = null; |
362: | } |
363: | |
364: | $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); |
365: | $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); |
366: | |
367: | $parameterName = $tokens->currentTokenValue(); |
368: | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
369: | |
370: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) { |
371: | $defaultValue = $this->constantExprParser->parse($tokens); |
372: | |
373: | } else { |
374: | $defaultValue = null; |
375: | } |
376: | |
377: | return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue); |
378: | } |
379: | |
380: | private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode |
381: | { |
382: | $name = $tokens->currentTokenValue(); |
383: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
384: | |
385: | if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { |
386: | $bound = $this->typeParser->parse($tokens); |
387: | |
388: | } else { |
389: | $bound = null; |
390: | } |
391: | |
392: | $description = $this->parseOptionalDescription($tokens); |
393: | |
394: | return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description); |
395: | } |
396: | |
397: | private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode |
398: | { |
399: | $baseType = new IdentifierTypeNode($tokens->currentTokenValue()); |
400: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
401: | |
402: | $type = $this->typeParser->parseGeneric($tokens, $baseType); |
403: | |
404: | $description = $this->parseOptionalDescription($tokens); |
405: | |
406: | switch ($tagName) { |
407: | case '@extends': |
408: | return new Ast\PhpDoc\ExtendsTagValueNode($type, $description); |
409: | case '@implements': |
410: | return new Ast\PhpDoc\ImplementsTagValueNode($type, $description); |
411: | case '@use': |
412: | return new Ast\PhpDoc\UsesTagValueNode($type, $description); |
413: | } |
414: | |
415: | throw new ShouldNotHappenException(); |
416: | } |
417: | |
418: | private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasTagValueNode |
419: | { |
420: | $alias = $tokens->currentTokenValue(); |
421: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
422: | |
423: | |
424: | $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); |
425: | |
426: | $type = $this->typeParser->parse($tokens); |
427: | |
428: | return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type); |
429: | } |
430: | |
431: | private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode |
432: | { |
433: | $importedAlias = $tokens->currentTokenValue(); |
434: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
435: | |
436: | $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from'); |
437: | |
438: | $importedFrom = $tokens->currentTokenValue(); |
439: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
440: | |
441: | $importedAs = null; |
442: | if ($tokens->tryConsumeTokenValue('as')) { |
443: | $importedAs = $tokens->currentTokenValue(); |
444: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
445: | } |
446: | |
447: | return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, new IdentifierTypeNode($importedFrom), $importedAs); |
448: | } |
449: | |
450: | |
451: | |
452: | |
453: | private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode |
454: | { |
455: | $isNegated = $tokens->tryConsumeTokenType(Lexer::TOKEN_NEGATED); |
456: | $type = $this->typeParser->parse($tokens); |
457: | $parameter = $this->parseAssertParameter($tokens); |
458: | $description = $this->parseOptionalDescription($tokens); |
459: | |
460: | if (array_key_exists('method', $parameter)) { |
461: | return new Ast\PhpDoc\AssertTagMethodValueNode($type, $parameter['parameter'], $parameter['method'], $isNegated, $description); |
462: | } elseif (array_key_exists('property', $parameter)) { |
463: | return new Ast\PhpDoc\AssertTagPropertyValueNode($type, $parameter['parameter'], $parameter['property'], $isNegated, $description); |
464: | } |
465: | |
466: | return new Ast\PhpDoc\AssertTagValueNode($type, $parameter['parameter'], $isNegated, $description); |
467: | } |
468: | |
469: | |
470: | |
471: | |
472: | private function parseAssertParameter(TokenIterator $tokens): array |
473: | { |
474: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) { |
475: | $parameter = '$this'; |
476: | $requirePropertyOrMethod = true; |
477: | $tokens->next(); |
478: | } else { |
479: | $parameter = $tokens->currentTokenValue(); |
480: | $requirePropertyOrMethod = false; |
481: | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
482: | } |
483: | |
484: | if ($requirePropertyOrMethod || $tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) { |
485: | $tokens->consumeTokenType(Lexer::TOKEN_ARROW); |
486: | |
487: | $propertyOrMethod = $tokens->currentTokenValue(); |
488: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
489: | |
490: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { |
491: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); |
492: | |
493: | return ['parameter' => $parameter, 'method' => $propertyOrMethod]; |
494: | } |
495: | |
496: | return ['parameter' => $parameter, 'property' => $propertyOrMethod]; |
497: | } |
498: | |
499: | return ['parameter' => $parameter]; |
500: | } |
501: | |
502: | private function parseOptionalVariableName(TokenIterator $tokens): string |
503: | { |
504: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { |
505: | $parameterName = $tokens->currentTokenValue(); |
506: | $tokens->next(); |
507: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) { |
508: | $parameterName = '$this'; |
509: | $tokens->next(); |
510: | |
511: | } else { |
512: | $parameterName = ''; |
513: | } |
514: | |
515: | return $parameterName; |
516: | } |
517: | |
518: | |
519: | private function parseRequiredVariableName(TokenIterator $tokens): string |
520: | { |
521: | $parameterName = $tokens->currentTokenValue(); |
522: | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
523: | |
524: | return $parameterName; |
525: | } |
526: | |
527: | private function parseOptionalDescription(TokenIterator $tokens, bool $limitStartToken = false): string |
528: | { |
529: | if ($limitStartToken) { |
530: | foreach (self::DISALLOWED_DESCRIPTION_START_TOKENS as $disallowedStartToken) { |
531: | if (!$tokens->isCurrentTokenType($disallowedStartToken)) { |
532: | continue; |
533: | } |
534: | |
535: | $tokens->consumeTokenType(Lexer::TOKEN_OTHER); |
536: | } |
537: | |
538: | if ( |
539: | $this->requireWhitespaceBeforeDescription |
540: | && !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END) |
541: | && !$tokens->isPrecededByHorizontalWhitespace() |
542: | ) { |
543: | $tokens->consumeTokenType(Lexer::TOKEN_HORIZONTAL_WS); |
544: | } |
545: | } |
546: | |
547: | return $this->parseText($tokens)->text; |
548: | } |
549: | |
550: | } |
551: | |