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: /** @var TypeParser */
23: private $typeParser;
24:
25: /** @var ConstExprParser */
26: private $constantExprParser;
27:
28: /** @var bool */
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, true);
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: case '@phpstan-this-out':
226: case '@phpstan-self-out':
227: case '@psalm-this-out':
228: case '@psalm-self-out':
229: $tagValue = $this->parseSelfOutTagValue($tokens);
230: break;
231:
232: case '@param-out':
233: case '@phpstan-param-out':
234: case '@psalm-param-out':
235: $tagValue = $this->parseParamOutTagValue($tokens);
236: break;
237:
238: default:
239: $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens));
240: break;
241: }
242:
243: $tokens->dropSavePoint();
244:
245: } catch (ParserException $e) {
246: $tokens->rollback();
247: $tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens), $e);
248: }
249:
250: return $tagValue;
251: }
252:
253:
254: /**
255: * @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode
256: */
257: private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
258: {
259: if (
260: $tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE)
261: || $tokens->isCurrentTokenType(Lexer::TOKEN_VARIADIC)
262: || $tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)
263: ) {
264: $type = null;
265: } else {
266: $type = $this->typeParser->parse($tokens);
267: }
268:
269: $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
270: $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
271: $parameterName = $this->parseRequiredVariableName($tokens);
272: $description = $this->parseOptionalDescription($tokens);
273:
274: if ($type !== null) {
275: return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference);
276: }
277:
278: return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference);
279: }
280:
281:
282: private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode
283: {
284: $type = $this->typeParser->parse($tokens);
285: $variableName = $this->parseOptionalVariableName($tokens);
286: $description = $this->parseOptionalDescription($tokens, $variableName === '');
287: return new Ast\PhpDoc\VarTagValueNode($type, $variableName, $description);
288: }
289:
290:
291: private function parseReturnTagValue(TokenIterator $tokens): Ast\PhpDoc\ReturnTagValueNode
292: {
293: $type = $this->typeParser->parse($tokens);
294: $description = $this->parseOptionalDescription($tokens, true);
295: return new Ast\PhpDoc\ReturnTagValueNode($type, $description);
296: }
297:
298:
299: private function parseThrowsTagValue(TokenIterator $tokens): Ast\PhpDoc\ThrowsTagValueNode
300: {
301: $type = $this->typeParser->parse($tokens);
302: $description = $this->parseOptionalDescription($tokens, true);
303: return new Ast\PhpDoc\ThrowsTagValueNode($type, $description);
304: }
305:
306: private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagValueNode
307: {
308: $type = $this->typeParser->parse($tokens);
309: $description = $this->parseOptionalDescription($tokens, true);
310: return new Ast\PhpDoc\MixinTagValueNode($type, $description);
311: }
312:
313: private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
314: {
315: $description = $this->parseOptionalDescription($tokens);
316: return new Ast\PhpDoc\DeprecatedTagValueNode($description);
317: }
318:
319:
320: private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\PropertyTagValueNode
321: {
322: $type = $this->typeParser->parse($tokens);
323: $parameterName = $this->parseRequiredVariableName($tokens);
324: $description = $this->parseOptionalDescription($tokens);
325: return new Ast\PhpDoc\PropertyTagValueNode($type, $parameterName, $description);
326: }
327:
328:
329: private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode
330: {
331: $isStatic = $tokens->tryConsumeTokenValue('static');
332: $returnTypeOrMethodName = $this->typeParser->parse($tokens);
333:
334: if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
335: $returnType = $returnTypeOrMethodName;
336: $methodName = $tokens->currentTokenValue();
337: $tokens->next();
338:
339: } elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) {
340: $returnType = $isStatic ? new Ast\Type\IdentifierTypeNode('static') : null;
341: $methodName = $returnTypeOrMethodName->name;
342: $isStatic = false;
343:
344: } else {
345: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); // will throw exception
346: exit;
347: }
348:
349: $templateTypes = [];
350: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
351: do {
352: $templateTypes[] = $this->parseTemplateTagValue($tokens, false);
353: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
354: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
355: }
356:
357: $parameters = [];
358: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
359: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
360: $parameters[] = $this->parseMethodTagValueParameter($tokens);
361: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
362: $parameters[] = $this->parseMethodTagValueParameter($tokens);
363: }
364: }
365: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
366:
367: $description = $this->parseOptionalDescription($tokens);
368: return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes);
369: }
370:
371: private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
372: {
373: switch ($tokens->currentTokenType()) {
374: case Lexer::TOKEN_IDENTIFIER:
375: case Lexer::TOKEN_OPEN_PARENTHESES:
376: case Lexer::TOKEN_NULLABLE:
377: $parameterType = $this->typeParser->parse($tokens);
378: break;
379:
380: default:
381: $parameterType = null;
382: }
383:
384: $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
385: $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
386:
387: $parameterName = $tokens->currentTokenValue();
388: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
389:
390: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
391: $defaultValue = $this->constantExprParser->parse($tokens);
392:
393: } else {
394: $defaultValue = null;
395: }
396:
397: return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue);
398: }
399:
400: private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode
401: {
402: $name = $tokens->currentTokenValue();
403: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
404:
405: if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
406: $bound = $this->typeParser->parse($tokens);
407:
408: } else {
409: $bound = null;
410: }
411:
412: if ($tokens->tryConsumeTokenValue('=')) {
413: $default = $this->typeParser->parse($tokens);
414: } else {
415: $default = null;
416: }
417:
418: if ($parseDescription) {
419: $description = $this->parseOptionalDescription($tokens);
420: } else {
421: $description = '';
422: }
423:
424: return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
425: }
426:
427: private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
428: {
429: $baseType = new IdentifierTypeNode($tokens->currentTokenValue());
430: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
431:
432: $type = $this->typeParser->parseGeneric($tokens, $baseType);
433:
434: $description = $this->parseOptionalDescription($tokens);
435:
436: switch ($tagName) {
437: case '@extends':
438: return new Ast\PhpDoc\ExtendsTagValueNode($type, $description);
439: case '@implements':
440: return new Ast\PhpDoc\ImplementsTagValueNode($type, $description);
441: case '@use':
442: return new Ast\PhpDoc\UsesTagValueNode($type, $description);
443: }
444:
445: throw new ShouldNotHappenException();
446: }
447:
448: private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasTagValueNode
449: {
450: $alias = $tokens->currentTokenValue();
451: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
452:
453: // support psalm-type syntax
454: $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
455:
456: $type = $this->typeParser->parse($tokens);
457:
458: return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
459: }
460:
461: private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode
462: {
463: $importedAlias = $tokens->currentTokenValue();
464: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
465:
466: $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from');
467:
468: $importedFrom = $tokens->currentTokenValue();
469: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
470:
471: $importedAs = null;
472: if ($tokens->tryConsumeTokenValue('as')) {
473: $importedAs = $tokens->currentTokenValue();
474: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
475: }
476:
477: return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, new IdentifierTypeNode($importedFrom), $importedAs);
478: }
479:
480: /**
481: * @return Ast\PhpDoc\AssertTagValueNode|Ast\PhpDoc\AssertTagPropertyValueNode|Ast\PhpDoc\AssertTagMethodValueNode
482: */
483: private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
484: {
485: $isNegated = $tokens->tryConsumeTokenType(Lexer::TOKEN_NEGATED);
486: $isEquality = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
487: $type = $this->typeParser->parse($tokens);
488: $parameter = $this->parseAssertParameter($tokens);
489: $description = $this->parseOptionalDescription($tokens);
490:
491: if (array_key_exists('method', $parameter)) {
492: return new Ast\PhpDoc\AssertTagMethodValueNode($type, $parameter['parameter'], $parameter['method'], $isNegated, $description, $isEquality);
493: } elseif (array_key_exists('property', $parameter)) {
494: return new Ast\PhpDoc\AssertTagPropertyValueNode($type, $parameter['parameter'], $parameter['property'], $isNegated, $description, $isEquality);
495: }
496:
497: return new Ast\PhpDoc\AssertTagValueNode($type, $parameter['parameter'], $isNegated, $description, $isEquality);
498: }
499:
500: /**
501: * @return array{parameter: string}|array{parameter: string, property: string}|array{parameter: string, method: string}
502: */
503: private function parseAssertParameter(TokenIterator $tokens): array
504: {
505: if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
506: $parameter = '$this';
507: $requirePropertyOrMethod = true;
508: $tokens->next();
509: } else {
510: $parameter = $tokens->currentTokenValue();
511: $requirePropertyOrMethod = false;
512: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
513: }
514:
515: if ($requirePropertyOrMethod || $tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) {
516: $tokens->consumeTokenType(Lexer::TOKEN_ARROW);
517:
518: $propertyOrMethod = $tokens->currentTokenValue();
519: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
520:
521: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
522: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
523:
524: return ['parameter' => $parameter, 'method' => $propertyOrMethod];
525: }
526:
527: return ['parameter' => $parameter, 'property' => $propertyOrMethod];
528: }
529:
530: return ['parameter' => $parameter];
531: }
532:
533: private function parseSelfOutTagValue(TokenIterator $tokens): Ast\PhpDoc\SelfOutTagValueNode
534: {
535: $type = $this->typeParser->parse($tokens);
536: $description = $this->parseOptionalDescription($tokens);
537:
538: return new Ast\PhpDoc\SelfOutTagValueNode($type, $description);
539: }
540:
541: private function parseParamOutTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamOutTagValueNode
542: {
543: $type = $this->typeParser->parse($tokens);
544: $parameterName = $this->parseRequiredVariableName($tokens);
545: $description = $this->parseOptionalDescription($tokens);
546:
547: return new Ast\PhpDoc\ParamOutTagValueNode($type, $parameterName, $description);
548: }
549:
550: private function parseOptionalVariableName(TokenIterator $tokens): string
551: {
552: if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
553: $parameterName = $tokens->currentTokenValue();
554: $tokens->next();
555: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
556: $parameterName = '$this';
557: $tokens->next();
558:
559: } else {
560: $parameterName = '';
561: }
562:
563: return $parameterName;
564: }
565:
566:
567: private function parseRequiredVariableName(TokenIterator $tokens): string
568: {
569: $parameterName = $tokens->currentTokenValue();
570: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
571:
572: return $parameterName;
573: }
574:
575: private function parseOptionalDescription(TokenIterator $tokens, bool $limitStartToken = false): string
576: {
577: if ($limitStartToken) {
578: foreach (self::DISALLOWED_DESCRIPTION_START_TOKENS as $disallowedStartToken) {
579: if (!$tokens->isCurrentTokenType($disallowedStartToken)) {
580: continue;
581: }
582:
583: $tokens->consumeTokenType(Lexer::TOKEN_OTHER); // will throw exception
584: }
585:
586: if (
587: $this->requireWhitespaceBeforeDescription
588: && !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)
589: && !$tokens->isPrecededByHorizontalWhitespace()
590: ) {
591: $tokens->consumeTokenType(Lexer::TOKEN_HORIZONTAL_WS); // will throw exception
592: }
593: }
594:
595: return $this->parseText($tokens)->text;
596: }
597:
598: }
599: