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: /** @var bool */
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: * @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode
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); // will throw exception
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: // support psalm-type syntax
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: * @return Ast\PhpDoc\AssertTagValueNode|Ast\PhpDoc\AssertTagPropertyValueNode|Ast\PhpDoc\AssertTagMethodValueNode
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: * @return array{parameter: string}|array{parameter: string, property: string}|array{parameter: string, method: string}
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); // will throw exception
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); // will throw exception
617: }
618: }
619:
620: return $this->parseText($tokens)->text;
621: }
622:
623: }
624: