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