typeParser = $typeParser;
$this->constantExprParser = $constantExprParser;
$this->doctrineConstantExprParser = $constantExprParser->toDoctrine();
$this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription;
$this->preserveTypeAliasesWithInvalidTypes = $preserveTypeAliasesWithInvalidTypes;
$this->parseDoctrineAnnotations = $parseDoctrineAnnotations;
$this->useLinesAttributes = $usedAttributes['lines'] ?? \false;
$this->useIndexAttributes = $usedAttributes['indexes'] ?? \false;
$this->textBetweenTagsBelongsToDescription = $textBetweenTagsBelongsToDescription;
}
public function parse(\PHPStan\PhpDocParser\Parser\TokenIterator $tokens) : Ast\PhpDoc\PhpDocNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PHPDOC);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$children = [];
if ($this->parseDoctrineAnnotations) {
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
$lastChild = $this->parseChild($tokens);
$children[] = $lastChild;
while (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
if ($lastChild instanceof Ast\PhpDoc\PhpDocTagNode && ($lastChild->value instanceof Doctrine\DoctrineTagValueNode || $lastChild->value instanceof Ast\PhpDoc\GenericTagValueNode)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
break;
}
$lastChild = $this->parseChild($tokens);
$children[] = $lastChild;
continue;
}
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
break;
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
break;
}
$lastChild = $this->parseChild($tokens);
$children[] = $lastChild;
}
}
} else {
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
$children[] = $this->parseChild($tokens);
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL) && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
$children[] = $this->parseChild($tokens);
}
}
}
try {
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC);
} catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
$name = '';
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if (count($children) > 0) {
$lastChild = $children[count($children) - 1];
if ($lastChild instanceof Ast\PhpDoc\PhpDocTagNode) {
$name = $lastChild->name;
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
}
}
$tag = new Ast\PhpDoc\PhpDocTagNode($name, $this->enrichWithAttributes($tokens, new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e), $startLine, $startIndex));
$tokens->forwardToTheEnd();
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]), 1, 0);
}
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode(array_values($children)), 1, 0);
}
/** @phpstan-impure */
private function parseChild(\PHPStan\PhpDocParser\Parser\TokenIterator $tokens) : Ast\PhpDoc\PhpDocChildNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
return $this->enrichWithAttributes($tokens, $this->parseTag($tokens), $startLine, $startIndex);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_TAG)) {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$tag = $tokens->currentTokenValue();
$tokens->next();
$tagStartLine = $tokens->currentTokenLine();
$tagStartIndex = $tokens->currentTokenIndex();
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocTagNode($tag, $this->enrichWithAttributes($tokens, $this->parseDoctrineTagValue($tokens, $tag), $tagStartLine, $tagStartIndex)), $startLine, $startIndex);
}
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$text = $this->parseText($tokens);
return $this->enrichWithAttributes($tokens, $text, $startLine, $startIndex);
}
/**
* @template T of Ast\Node
* @param T $tag
* @return T
*/
private function enrichWithAttributes(\PHPStan\PhpDocParser\Parser\TokenIterator $tokens, Ast\Node $tag, int $startLine, int $startIndex) : Ast\Node
{
if ($this->useLinesAttributes) {
$tag->setAttribute(Ast\Attribute::START_LINE, $startLine);
$tag->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
}
if ($this->useIndexAttributes) {
$tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
$tag->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
}
return $tag;
}
private function parseText(\PHPStan\PhpDocParser\Parser\TokenIterator $tokens) : Ast\PhpDoc\PhpDocTextNode
{
$text = '';
$endTokens = [Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
if ($this->textBetweenTagsBelongsToDescription) {
$endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
}
$savepoint = \false;
// if the next token is EOL, everything below is skipped and empty string is returned
while ($this->textBetweenTagsBelongsToDescription || !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
$tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
$text .= $tmpText;
// stop if we're not at EOL - meaning it's the end of PHPDoc
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC)) {
break;
}
if ($this->textBetweenTagsBelongsToDescription) {
if (!$savepoint) {
$tokens->pushSavePoint();
$savepoint = \true;
} elseif ($tmpText !== '') {
$tokens->dropSavePoint();
$tokens->pushSavePoint();
}
}
$tokens->pushSavePoint();
$tokens->next();
// if we're at EOL, check what's next
// if next is a PHPDoc tag, EOL, or end of PHPDoc, stop
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) {
$tokens->rollback();
break;
}
// otherwise if the next is text, continue building the description string
$tokens->dropSavePoint();
$text .= $tokens->getDetectedNewline() ?? "\n";
}
if ($savepoint) {
$tokens->rollback();
$text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
}
return new Ast\PhpDoc\PhpDocTextNode(trim($text, " \t"));
}
private function parseOptionalDescriptionAfterDoctrineTag(\PHPStan\PhpDocParser\Parser\TokenIterator $tokens) : string
{
$text = '';
$endTokens = [Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
if ($this->textBetweenTagsBelongsToDescription) {
$endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
}
$savepoint = \false;
// if the next token is EOL, everything below is skipped and empty string is returned
while ($this->textBetweenTagsBelongsToDescription || !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
$tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
$text .= $tmpText;
// stop if we're not at EOL - meaning it's the end of PHPDoc
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC)) {
if (!$tokens->isPrecededByHorizontalWhitespace()) {
return trim($text . $this->parseText($tokens)->text, " \t");
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
$tokens->pushSavePoint();
$child = $this->parseChild($tokens);
if ($child instanceof Ast\PhpDoc\PhpDocTagNode) {
if ($child->value instanceof Ast\PhpDoc\GenericTagValueNode || $child->value instanceof Doctrine\DoctrineTagValueNode) {
$tokens->rollback();
break;
}
if ($child->value instanceof Ast\PhpDoc\InvalidTagValueNode) {
$tokens->rollback();
$tokens->pushSavePoint();
$tokens->next();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tokens->rollback();
break;
}
$tokens->rollback();
return trim($text . $this->parseText($tokens)->text, " \t");
}
}
$tokens->rollback();
return trim($text . $this->parseText($tokens)->text, " \t");
}
break;
}
if ($this->textBetweenTagsBelongsToDescription) {
if (!$savepoint) {
$tokens->pushSavePoint();
$savepoint = \true;
} elseif ($tmpText !== '') {
$tokens->dropSavePoint();
$tokens->pushSavePoint();
}
}
$tokens->pushSavePoint();
$tokens->next();
// if we're at EOL, check what's next
// if next is a PHPDoc tag, EOL, or end of PHPDoc, stop
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) {
$tokens->rollback();
break;
}
// otherwise if the next is text, continue building the description string
$tokens->dropSavePoint();
$text .= $tokens->getDetectedNewline() ?? "\n";
}
if ($savepoint) {
$tokens->rollback();
$text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
}
return trim($text, " \t");
}
public function parseTag(\PHPStan\PhpDocParser\Parser\TokenIterator $tokens) : Ast\PhpDoc\PhpDocTagNode
{
$tag = $tokens->currentTokenValue();
$tokens->next();
$value = $this->parseTagValue($tokens, $tag);
return new Ast\PhpDoc\PhpDocTagNode($tag, $value);
}
public function parseTagValue(\PHPStan\PhpDocParser\Parser\TokenIterator $tokens, string $tag) : Ast\PhpDoc\PhpDocTagValueNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$tokens->pushSavePoint();
switch ($tag) {
case '@param':
case '@phpstan-param':
case '@psalm-param':
case '@phan-param':
$tagValue = $this->parseParamTagValue($tokens);
break;
case '@param-immediately-invoked-callable':
case '@phpstan-param-immediately-invoked-callable':
$tagValue = $this->parseParamImmediatelyInvokedCallableTagValue($tokens);
break;
case '@param-later-invoked-callable':
case '@phpstan-param-later-invoked-callable':
$tagValue = $this->parseParamLaterInvokedCallableTagValue($tokens);
break;
case '@param-closure-this':
case '@phpstan-param-closure-this':
$tagValue = $this->parseParamClosureThisTagValue($tokens);
break;
case '@pure-unless-callable-is-impure':
case '@phpstan-pure-unless-callable-is-impure':
$tagValue = $this->parsePureUnlessCallableIsImpureTagValue($tokens);
break;
case '@var':
case '@phpstan-var':
case '@psalm-var':
case '@phan-var':
$tagValue = $this->parseVarTagValue($tokens);
break;
case '@return':
case '@phpstan-return':
case '@psalm-return':
case '@phan-return':
case '@phan-real-return':
$tagValue = $this->parseReturnTagValue($tokens);
break;
case '@throws':
case '@phpstan-throws':
$tagValue = $this->parseThrowsTagValue($tokens);
break;
case '@mixin':
case '@phan-mixin':
$tagValue = $this->parseMixinTagValue($tokens);
break;
case '@psalm-require-extends':
case '@phpstan-require-extends':
$tagValue = $this->parseRequireExtendsTagValue($tokens);
break;
case '@psalm-require-implements':
case '@phpstan-require-implements':
$tagValue = $this->parseRequireImplementsTagValue($tokens);
break;
case '@deprecated':
$tagValue = $this->parseDeprecatedTagValue($tokens);
break;
case '@property':
case '@property-read':
case '@property-write':
case '@phpstan-property':
case '@phpstan-property-read':
case '@phpstan-property-write':
case '@psalm-property':
case '@psalm-property-read':
case '@psalm-property-write':
case '@phan-property':
case '@phan-property-read':
case '@phan-property-write':
$tagValue = $this->parsePropertyTagValue($tokens);
break;
case '@method':
case '@phpstan-method':
case '@psalm-method':
case '@phan-method':
$tagValue = $this->parseMethodTagValue($tokens);
break;
case '@template':
case '@phpstan-template':
case '@psalm-template':
case '@phan-template':
case '@template-covariant':
case '@phpstan-template-covariant':
case '@psalm-template-covariant':
case '@template-contravariant':
case '@phpstan-template-contravariant':
case '@psalm-template-contravariant':
$tagValue = $this->typeParser->parseTemplateTagValue($tokens, function ($tokens) {
return $this->parseOptionalDescription($tokens);
});
break;
case '@extends':
case '@phpstan-extends':
case '@phan-extends':
case '@phan-inherits':
case '@template-extends':
$tagValue = $this->parseExtendsTagValue('@extends', $tokens);
break;
case '@implements':
case '@phpstan-implements':
case '@template-implements':
$tagValue = $this->parseExtendsTagValue('@implements', $tokens);
break;
case '@use':
case '@phpstan-use':
case '@template-use':
$tagValue = $this->parseExtendsTagValue('@use', $tokens);
break;
case '@phpstan-type':
case '@psalm-type':
case '@phan-type':
$tagValue = $this->parseTypeAliasTagValue($tokens);
break;
case '@phpstan-import-type':
case '@psalm-import-type':
$tagValue = $this->parseTypeAliasImportTagValue($tokens);
break;
case '@phpstan-assert':
case '@phpstan-assert-if-true':
case '@phpstan-assert-if-false':
case '@psalm-assert':
case '@psalm-assert-if-true':
case '@psalm-assert-if-false':
case '@phan-assert':
case '@phan-assert-if-true':
case '@phan-assert-if-false':
$tagValue = $this->parseAssertTagValue($tokens);
break;
case '@phpstan-this-out':
case '@phpstan-self-out':
case '@psalm-this-out':
case '@psalm-self-out':
$tagValue = $this->parseSelfOutTagValue($tokens);
break;
case '@param-out':
case '@phpstan-param-out':
case '@psalm-param-out':
$tagValue = $this->parseParamOutTagValue($tokens);
break;
default:
if ($this->parseDoctrineAnnotations) {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tagValue = $this->parseDoctrineTagValue($tokens, $tag);
} else {
$tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescriptionAfterDoctrineTag($tokens));
}
break;
}
$tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens));
break;
}
$tokens->dropSavePoint();
} catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
$tokens->rollback();
$tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens), $e);
}
return $this->enrichWithAttributes($tokens, $tagValue, $startLine, $startIndex);
}
private function parseDoctrineTagValue(\PHPStan\PhpDocParser\Parser\TokenIterator $tokens, string $tag) : Ast\PhpDoc\PhpDocTagValueNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
return new Doctrine\DoctrineTagValueNode($this->enrichWithAttributes($tokens, new Doctrine\DoctrineAnnotation($tag, $this->parseDoctrineArguments($tokens, \false)), $startLine, $startIndex), $this->parseOptionalDescriptionAfterDoctrineTag($tokens));
}
/**
* @return list