|    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:  |  |