/home/smartonegroup/public_html/system/vendor/phpstan/phpdoc-parser/src/Parser/PhpDocParser.php
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\ParserConfig;
use PHPStan\ShouldNotHappenException;
use function array_key_exists;
use function count;
use function rtrim;
use function str_replace;
use function trim;
/**
* @phpstan-import-type ValueType from Doctrine\DoctrineArgument as DoctrineValueType
*/
class PhpDocParser
{
private const DISALLOWED_DESCRIPTION_START_TOKENS = [
Lexer::TOKEN_UNION,
Lexer::TOKEN_INTERSECTION,
];
private ParserConfig $config;
private TypeParser $typeParser;
private ConstExprParser $constantExprParser;
private ConstExprParser $doctrineConstantExprParser;
public function __construct(
ParserConfig $config,
TypeParser $typeParser,
ConstExprParser $constantExprParser
)
{
$this->config = $config;
$this->typeParser = $typeParser;
$this->constantExprParser = $constantExprParser;
$this->doctrineConstantExprParser = $constantExprParser->toDoctrine();
}
public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PHPDOC);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$children = [];
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;
}
}
try {
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC);
} catch (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($children), 1, 0);
}
/** @phpstan-impure */
private function parseChild(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(TokenIterator $tokens, Ast\Node $tag, int $startLine, int $startIndex): Ast\Node
{
if ($this->config->useLinesAttributes) {
$tag->setAttribute(Ast\Attribute::START_LINE, $startLine);
$tag->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
}
if ($this->config->useIndexAttributes) {
$tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
$tag->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
}
return $tag;
}
private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode
{
$text = '';
$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 (true) {
$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 (!$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(TokenIterator $tokens): string
{
$text = '';
$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 (true) {
$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 (!$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(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(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,
fn ($tokens) => $this->parseOptionalDescription($tokens, true),
);
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 ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tagValue = $this->parseDoctrineTagValue($tokens, $tag);
} else {
$tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescriptionAfterDoctrineTag($tokens));
}
break;
}
$tokens->dropSavePoint();
} catch (ParserException $e) {
$tokens->rollback();
$tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens, false), $e);
}
return $this->enrichWithAttributes($tokens, $tagValue, $startLine, $startIndex);
}
private function parseDoctrineTagValue(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<Doctrine\DoctrineArgument>
*/
private function parseDoctrineArguments(TokenIterator $tokens, bool $deep): array
{
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
return [];
}
if (!$deep) {
$tokens->addEndOfLineToSkippedTokens();
}
$arguments = [];
try {
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
do {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
break;
}
$arguments[] = $this->parseDoctrineArgument($tokens);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
} finally {
if (!$deep) {
$tokens->removeEndOfLineFromSkippedTokens();
}
}
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
return $arguments;
}
private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\DoctrineArgument
{
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
$startLine,
$startIndex,
);
}
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$tokens->pushSavePoint();
$currentValue = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$key = $this->enrichWithAttributes(
$tokens,
new IdentifierTypeNode($currentValue),
$startLine,
$startIndex,
);
$tokens->consumeTokenType(Lexer::TOKEN_EQUAL);
$value = $this->parseDoctrineArgumentValue($tokens);
$tokens->dropSavePoint();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArgument($key, $value),
$startLine,
$startIndex,
);
} catch (ParserException $e) {
$tokens->rollback();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
$startLine,
$startIndex,
);
}
}
/**
* @return DoctrineValueType
*/
private function parseDoctrineArgumentValue(TokenIterator $tokens)
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG)) {
$name = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineAnnotation($name, $this->parseDoctrineArguments($tokens, true)),
$startLine,
$startIndex,
);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
$items = [];
do {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
break;
}
$items[] = $this->parseDoctrineArrayItem($tokens);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArray($items),
$startLine,
$startIndex,
);
}
$currentTokenValue = $tokens->currentTokenValue();
$tokens->pushSavePoint(); // because of ConstFetchNode
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$identifier = $this->enrichWithAttributes(
$tokens,
new Ast\Type\IdentifierTypeNode($currentTokenValue),
$startLine,
$startIndex,
);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$tokens->dropSavePoint();
return $identifier;
}
$tokens->rollback(); // because of ConstFetchNode
} else {
$tokens->dropSavePoint(); // because of ConstFetchNode
}
$currentTokenValue = $tokens->currentTokenValue();
$currentTokenType = $tokens->currentTokenType();
$currentTokenOffset = $tokens->currentTokenOffset();
$currentTokenLine = $tokens->currentTokenLine();
try {
$constExpr = $this->doctrineConstantExprParser->parse($tokens);
if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
throw new ParserException(
$currentTokenValue,
$currentTokenType,
$currentTokenOffset,
Lexer::TOKEN_IDENTIFIER,
null,
$currentTokenLine,
);
}
return $constExpr;
} catch (LogicException $e) {
throw new ParserException(
$currentTokenValue,
$currentTokenType,
$currentTokenOffset,
Lexer::TOKEN_IDENTIFIER,
null,
$currentTokenLine,
);
}
}
private function parseDoctrineArrayItem(TokenIterator $tokens): Doctrine\DoctrineArrayItem
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$tokens->pushSavePoint();
$key = $this->parseDoctrineArrayKey($tokens);
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COLON)) {
$tokens->consumeTokenType(Lexer::TOKEN_EQUAL); // will throw exception
}
}
$value = $this->parseDoctrineArgumentValue($tokens);
$tokens->dropSavePoint();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArrayItem($key, $value),
$startLine,
$startIndex,
);
} catch (ParserException $e) {
$tokens->rollback();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArrayItem(null, $this->parseDoctrineArgumentValue($tokens)),
$startLine,
$startIndex,
);
}
}
/**
* @return ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|ConstFetchNode
*/
private function parseDoctrineArrayKey(TokenIterator $tokens)
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$key = $this->doctrineConstantExprParser->parseDoctrineString($tokens->currentTokenValue(), $tokens);
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED);
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$value = $tokens->currentTokenValue();
$tokens->next();
$key = $this->doctrineConstantExprParser->parseDoctrineString($value, $tokens);
} else {
$currentTokenValue = $tokens->currentTokenValue();
$tokens->pushSavePoint(); // because of ConstFetchNode
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$tokens->dropSavePoint();
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine(),
);
}
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$tokens->dropSavePoint();
return $this->enrichWithAttributes(
$tokens,
new IdentifierTypeNode($currentTokenValue),
$startLine,
$startIndex,
);
}
$tokens->rollback();
$constExpr = $this->doctrineConstantExprParser->parse($tokens);
if (!$constExpr instanceof Ast\ConstExpr\ConstFetchNode) {
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine(),
);
}
return $constExpr;
}
return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
}
/**
* @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode
*/
private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
{
if (
$tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE, Lexer::TOKEN_VARIADIC, Lexer::TOKEN_VARIABLE)
) {
$type = null;
} else {
$type = $this->typeParser->parse($tokens);
}
$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
$parameterName = $this->parseRequiredVariableName($tokens);
$description = $this->parseOptionalDescription($tokens, false);
if ($type !== null) {
return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference);
}
return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference);
}
private function parseParamImmediatelyInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode
{
$parameterName = $this->parseRequiredVariableName($tokens);
$description = $this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode($parameterName, $description);
}
private function parseParamLaterInvokedCallableTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode
{
$parameterName = $this->parseRequiredVariableName($tokens);
$description = $this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode($parameterName, $description);
}
private function parseParamClosureThisTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamClosureThisTagValueNode
{
$type = $this->typeParser->parse($tokens);
$parameterName = $this->parseRequiredVariableName($tokens);
$description = $this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\ParamClosureThisTagValueNode($type, $parameterName, $description);
}
private function parsePureUnlessCallableIsImpureTagValue(TokenIterator $tokens): Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode
{
$parameterName = $this->parseRequiredVariableName($tokens);
$description = $this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode($parameterName, $description);
}
private function parseVarTagValue(TokenIterator $tokens): Ast\PhpDoc\VarTagValueNode
{
$type = $this->typeParser->parse($tokens);
$variableName = $this->parseOptionalVariableName($tokens);
$description = $this->parseOptionalDescription($tokens, $variableName === '');
return new Ast\PhpDoc\VarTagValueNode($type, $variableName, $description);
}
private function parseReturnTagValue(TokenIterator $tokens): Ast\PhpDoc\ReturnTagValueNode
{
$type = $this->typeParser->parse($tokens);
$description = $this->parseOptionalDescription($tokens, true);
return new Ast\PhpDoc\ReturnTagValueNode($type, $description);
}
private function parseThrowsTagValue(TokenIterator $tokens): Ast\PhpDoc\ThrowsTagValueNode
{
$type = $this->typeParser->parse($tokens);
$description = $this->parseOptionalDescription($tokens, true);
return new Ast\PhpDoc\ThrowsTagValueNode($type, $description);
}
private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagValueNode
{
$type = $this->typeParser->parse($tokens);
$description = $this->parseOptionalDescription($tokens, true);
return new Ast\PhpDoc\MixinTagValueNode($type, $description);
}
private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode
{
$type = $this->typeParser->parse($tokens);
$description = $this->parseOptionalDescription($tokens, true);
return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description);
}
private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode
{
$type = $this->typeParser->parse($tokens);
$description = $this->parseOptionalDescription($tokens, true);
return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description);
}
private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
{
$description = $this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\DeprecatedTagValueNode($description);
}
private function parsePropertyTagValue(TokenIterator $tokens): Ast\PhpDoc\PropertyTagValueNode
{
$type = $this->typeParser->parse($tokens);
$parameterName = $this->parseRequiredVariableName($tokens);
$description = $this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\PropertyTagValueNode($type, $parameterName, $description);
}
private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode
{
$staticKeywordOrReturnTypeOrMethodName = $this->typeParser->parse($tokens);
if ($staticKeywordOrReturnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode && $staticKeywordOrReturnTypeOrMethodName->name === 'static') {
$isStatic = true;
$returnTypeOrMethodName = $this->typeParser->parse($tokens);
} else {
$isStatic = false;
$returnTypeOrMethodName = $staticKeywordOrReturnTypeOrMethodName;
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$returnType = $returnTypeOrMethodName;
$methodName = $tokens->currentTokenValue();
$tokens->next();
} elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) {
$returnType = $isStatic ? $staticKeywordOrReturnTypeOrMethodName : null;
$methodName = $returnTypeOrMethodName->name;
$isStatic = false;
} else {
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); // will throw exception
exit;
}
$templateTypes = [];
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
do {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$templateTypes[] = $this->enrichWithAttributes(
$tokens,
$this->typeParser->parseTemplateTagValue($tokens),
$startLine,
$startIndex,
);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
}
$parameters = [];
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
$parameters[] = $this->parseMethodTagValueParameter($tokens);
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
$parameters[] = $this->parseMethodTagValueParameter($tokens);
}
}
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
$description = $this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes);
}
private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
switch ($tokens->currentTokenType()) {
case Lexer::TOKEN_IDENTIFIER:
case Lexer::TOKEN_OPEN_PARENTHESES:
case Lexer::TOKEN_NULLABLE:
$parameterType = $this->typeParser->parse($tokens);
break;
default:
$parameterType = null;
}
$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
$parameterName = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
$defaultValue = $this->constantExprParser->parse($tokens);
} else {
$defaultValue = null;
}
return $this->enrichWithAttributes(
$tokens,
new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue),
$startLine,
$startIndex,
);
}
private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$baseType = new IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$type = $this->typeParser->parseGeneric(
$tokens,
$this->typeParser->enrichWithAttributes($tokens, $baseType, $startLine, $startIndex),
);
$description = $this->parseOptionalDescription($tokens, true);
switch ($tagName) {
case '@extends':
return new Ast\PhpDoc\ExtendsTagValueNode($type, $description);
case '@implements':
return new Ast\PhpDoc\ImplementsTagValueNode($type, $description);
case '@use':
return new Ast\PhpDoc\UsesTagValueNode($type, $description);
}
throw new ShouldNotHappenException();
}
private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasTagValueNode
{
$alias = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
// support phan-type/psalm-type syntax
$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$type = $this->typeParser->parse($tokens);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_PHPDOC_EOL,
null,
$tokens->currentTokenLine(),
);
}
}
return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
} catch (ParserException $e) {
$this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\TypeAliasTagValueNode(
$alias,
$this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex),
);
}
}
private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode
{
$importedAlias = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from');
$identifierStartLine = $tokens->currentTokenLine();
$identifierStartIndex = $tokens->currentTokenIndex();
$importedFrom = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$importedFromType = $this->enrichWithAttributes(
$tokens,
new IdentifierTypeNode($importedFrom),
$identifierStartLine,
$identifierStartIndex,
);
$importedAs = null;
if ($tokens->tryConsumeTokenValue('as')) {
$importedAs = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}
return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, $importedFromType, $importedAs);
}
/**
* @return Ast\PhpDoc\AssertTagValueNode|Ast\PhpDoc\AssertTagPropertyValueNode|Ast\PhpDoc\AssertTagMethodValueNode
*/
private function parseAssertTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
{
$isNegated = $tokens->tryConsumeTokenType(Lexer::TOKEN_NEGATED);
$isEquality = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
$type = $this->typeParser->parse($tokens);
$parameter = $this->parseAssertParameter($tokens);
$description = $this->parseOptionalDescription($tokens, false);
if (array_key_exists('method', $parameter)) {
return new Ast\PhpDoc\AssertTagMethodValueNode($type, $parameter['parameter'], $parameter['method'], $isNegated, $description, $isEquality);
} elseif (array_key_exists('property', $parameter)) {
return new Ast\PhpDoc\AssertTagPropertyValueNode($type, $parameter['parameter'], $parameter['property'], $isNegated, $description, $isEquality);
}
return new Ast\PhpDoc\AssertTagValueNode($type, $parameter['parameter'], $isNegated, $description, $isEquality);
}
/**
* @return array{parameter: string}|array{parameter: string, property: string}|array{parameter: string, method: string}
*/
private function parseAssertParameter(TokenIterator $tokens): array
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
$parameter = '$this';
$tokens->next();
} else {
$parameter = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_ARROW)) {
$tokens->consumeTokenType(Lexer::TOKEN_ARROW);
$propertyOrMethod = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
return ['parameter' => $parameter, 'method' => $propertyOrMethod];
}
return ['parameter' => $parameter, 'property' => $propertyOrMethod];
}
return ['parameter' => $parameter];
}
private function parseSelfOutTagValue(TokenIterator $tokens): Ast\PhpDoc\SelfOutTagValueNode
{
$type = $this->typeParser->parse($tokens);
$description = $this->parseOptionalDescription($tokens, true);
return new Ast\PhpDoc\SelfOutTagValueNode($type, $description);
}
private function parseParamOutTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamOutTagValueNode
{
$type = $this->typeParser->parse($tokens);
$parameterName = $this->parseRequiredVariableName($tokens);
$description = $this->parseOptionalDescription($tokens, false);
return new Ast\PhpDoc\ParamOutTagValueNode($type, $parameterName, $description);
}
private function parseOptionalVariableName(TokenIterator $tokens): string
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
$parameterName = $tokens->currentTokenValue();
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
$parameterName = '$this';
$tokens->next();
} else {
$parameterName = '';
}
return $parameterName;
}
private function parseRequiredVariableName(TokenIterator $tokens): string
{
$parameterName = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
return $parameterName;
}
/**
* @param bool $limitStartToken true should be used when the description immediately follows a parsed type
*/
private function parseOptionalDescription(TokenIterator $tokens, bool $limitStartToken): string
{
if ($limitStartToken) {
foreach (self::DISALLOWED_DESCRIPTION_START_TOKENS as $disallowedStartToken) {
if (!$tokens->isCurrentTokenType($disallowedStartToken)) {
continue;
}
$tokens->consumeTokenType(Lexer::TOKEN_OTHER); // will throw exception
}
if (
!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)
&& !$tokens->isPrecededByHorizontalWhitespace()
) {
$tokens->consumeTokenType(Lexer::TOKEN_HORIZONTAL_WS); // will throw exception
}
}
return $this->parseText($tokens)->text;
}
}