composer base packages updates

This commit is contained in:
Clemens Schwaighofer
2023-03-28 11:51:54 +09:00
parent c3b29ad0d7
commit 28909fdc03
174 changed files with 7527 additions and 5286 deletions

View File

@@ -0,0 +1,230 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function chr;
use function hexdec;
use function octdec;
use function preg_replace_callback;
use function str_replace;
use function strtolower;
use function substr;
class ConstExprParser
{
private const REPLACEMENTS = [
'\\' => '\\',
'n' => "\n",
'r' => "\r",
't' => "\t",
'f' => "\f",
'v' => "\v",
'e' => "\x1B",
];
/** @var bool */
private $unescapeStrings;
public function __construct(bool $unescapeStrings = false)
{
$this->unescapeStrings = $unescapeStrings;
}
public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\ConstExpr\ConstExprNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return new Ast\ConstExpr\ConstExprFloatNode($value);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$value = $tokens->currentTokenValue();
$tokens->next();
return new Ast\ConstExpr\ConstExprIntegerNode($value);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$value = $tokens->currentTokenValue();
if ($trimStrings) {
if ($this->unescapeStrings) {
$value = self::unescapeString($value);
} else {
$value = substr($value, 1, -1);
}
}
$tokens->next();
return new Ast\ConstExpr\ConstExprStringNode($value);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$identifier = $tokens->currentTokenValue();
$tokens->next();
switch (strtolower($identifier)) {
case 'true':
return new Ast\ConstExpr\ConstExprTrueNode();
case 'false':
return new Ast\ConstExpr\ConstExprFalseNode();
case 'null':
return new Ast\ConstExpr\ConstExprNullNode();
case 'array':
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$classConstantName = '';
$lastType = null;
while (true) {
if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) {
$classConstantName .= $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$lastType = Lexer::TOKEN_IDENTIFIER;
continue;
}
if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
$classConstantName .= '*';
$lastType = Lexer::TOKEN_WILDCARD;
if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') {
break;
}
continue;
}
if ($lastType === null) {
// trigger parse error if nothing valid was consumed
$tokens->consumeTokenType(Lexer::TOKEN_WILDCARD);
}
break;
}
return new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName);
}
return new Ast\ConstExpr\ConstFetchNode('', $identifier);
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
}
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER
);
}
private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr\ConstExprArrayNode
{
$items = [];
if (!$tokens->tryConsumeTokenType($endToken)) {
do {
$items[] = $this->parseArrayItem($tokens);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) && !$tokens->isCurrentTokenType($endToken));
$tokens->consumeTokenType($endToken);
}
return new Ast\ConstExpr\ConstExprArrayNode($items);
}
private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
{
$expr = $this->parse($tokens);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) {
$key = $expr;
$value = $this->parse($tokens);
} else {
$key = null;
$value = $expr;
}
return new Ast\ConstExpr\ConstExprArrayItemNode($key, $value);
}
private static function unescapeString(string $string): string
{
$quote = $string[0];
if ($quote === '\'') {
return str_replace(
['\\\\', '\\\''],
['\\', '\''],
substr($string, 1, -1)
);
}
return self::parseEscapeSequences(substr($string, 1, -1), '"');
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130
*/
private static function parseEscapeSequences(string $str, string $quote): string
{
$str = str_replace('\\' . $quote, $quote, $str);
return preg_replace_callback(
'~\\\\([\\\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~',
static function ($matches) {
$str = $matches[1];
if (isset(self::REPLACEMENTS[$str])) {
return self::REPLACEMENTS[$str];
}
if ($str[0] === 'x' || $str[0] === 'X') {
return chr(hexdec(substr($str, 1)));
}
if ($str[0] === 'u') {
return self::codePointToUtf8(hexdec($matches[2]));
}
return chr(octdec($str));
},
$str
);
}
/**
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154
*/
private static function codePointToUtf8(int $num): string
{
if ($num <= 0x7F) {
return chr($num);
}
if ($num <= 0x7FF) {
return chr(($num >> 6) + 0xC0)
. chr(($num & 0x3F) + 0x80);
}
if ($num <= 0xFFFF) {
return chr(($num >> 12) + 0xE0)
. chr((($num >> 6) & 0x3F) + 0x80)
. chr(($num & 0x3F) + 0x80);
}
if ($num <= 0x1FFFFF) {
return chr(($num >> 18) + 0xF0)
. chr((($num >> 12) & 0x3F) + 0x80)
. chr((($num >> 6) & 0x3F) + 0x80)
. chr(($num & 0x3F) + 0x80);
}
// Invalid UTF-8 codepoint escape sequence: Codepoint too large
return "\xef\xbf\xbd";
}
}

View File

@@ -0,0 +1,93 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use Exception;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function assert;
use function json_encode;
use function sprintf;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
class ParserException extends Exception
{
/** @var string */
private $currentTokenValue;
/** @var int */
private $currentTokenType;
/** @var int */
private $currentOffset;
/** @var int */
private $expectedTokenType;
/** @var string|null */
private $expectedTokenValue;
public function __construct(
string $currentTokenValue,
int $currentTokenType,
int $currentOffset,
int $expectedTokenType,
?string $expectedTokenValue = null
)
{
$this->currentTokenValue = $currentTokenValue;
$this->currentTokenType = $currentTokenType;
$this->currentOffset = $currentOffset;
$this->expectedTokenType = $expectedTokenType;
$this->expectedTokenValue = $expectedTokenValue;
parent::__construct(sprintf(
'Unexpected token %s, expected %s%s at offset %d',
$this->formatValue($currentTokenValue),
Lexer::TOKEN_LABELS[$expectedTokenType],
$expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '',
$currentOffset
));
}
public function getCurrentTokenValue(): string
{
return $this->currentTokenValue;
}
public function getCurrentTokenType(): int
{
return $this->currentTokenType;
}
public function getCurrentOffset(): int
{
return $this->currentOffset;
}
public function getExpectedTokenType(): int
{
return $this->expectedTokenType;
}
public function getExpectedTokenValue(): ?string
{
return $this->expectedTokenValue;
}
private function formatValue(string $value): string
{
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
assert($json !== false);
return $json;
}
}

View File

@@ -0,0 +1,598 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\ShouldNotHappenException;
use function array_key_exists;
use function array_values;
use function count;
use function trim;
class PhpDocParser
{
private const DISALLOWED_DESCRIPTION_START_TOKENS = [
Lexer::TOKEN_UNION,
Lexer::TOKEN_INTERSECTION,
];
/** @var TypeParser */
private $typeParser;
/** @var ConstExprParser */
private $constantExprParser;
/** @var bool */
private $requireWhitespaceBeforeDescription;
public function __construct(TypeParser $typeParser, ConstExprParser $constantExprParser, bool $requireWhitespaceBeforeDescription = false)
{
$this->typeParser = $typeParser;
$this->constantExprParser = $constantExprParser;
$this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription;
}
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)) {
$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 (ParserException $e) {
$name = '';
if (count($children) > 0) {
$lastChild = $children[count($children) - 1];
if ($lastChild instanceof Ast\PhpDoc\PhpDocTagNode) {
$name = $lastChild->name;
}
}
$tokens->forwardToTheEnd();
return new Ast\PhpDoc\PhpDocNode([
new Ast\PhpDoc\PhpDocTagNode($name, new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e)),
]);
}
return new Ast\PhpDoc\PhpDocNode(array_values($children));
}
private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
return $this->parseTag($tokens);
}
return $this->parseText($tokens);
}
private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode
{
$text = '';
while (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
$text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
break;
}
$tokens->pushSavePoint();
$tokens->next();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)) {
$tokens->rollback();
break;
}
$tokens->dropSavePoint();
$text .= "\n";
}
return new Ast\PhpDoc\PhpDocTextNode(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
{
try {
$tokens->pushSavePoint();
switch ($tag) {
case '@param':
case '@phpstan-param':
case '@psalm-param':
$tagValue = $this->parseParamTagValue($tokens);
break;
case '@var':
case '@phpstan-var':
case '@psalm-var':
$tagValue = $this->parseVarTagValue($tokens);
break;
case '@return':
case '@phpstan-return':
case '@psalm-return':
$tagValue = $this->parseReturnTagValue($tokens);
break;
case '@throws':
case '@phpstan-throws':
$tagValue = $this->parseThrowsTagValue($tokens);
break;
case '@mixin':
$tagValue = $this->parseMixinTagValue($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':
$tagValue = $this->parsePropertyTagValue($tokens);
break;
case '@method':
case '@phpstan-method':
case '@psalm-method':
$tagValue = $this->parseMethodTagValue($tokens);
break;
case '@template':
case '@phpstan-template':
case '@psalm-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->parseTemplateTagValue($tokens, true);
break;
case '@extends':
case '@phpstan-extends':
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':
$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':
$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:
$tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens));
break;
}
$tokens->dropSavePoint();
} catch (ParserException $e) {
$tokens->rollback();
$tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens), $e);
}
return $tagValue;
}
/**
* @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode
*/
private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
{
if (
$tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE)
|| $tokens->isCurrentTokenType(Lexer::TOKEN_VARIADIC)
|| $tokens->isCurrentTokenType(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);
if ($type !== null) {
return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference);
}
return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference);
}
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 parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode
{
$description = $this->parseOptionalDescription($tokens);
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);
return new Ast\PhpDoc\PropertyTagValueNode($type, $parameterName, $description);
}
private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode
{
$isStatic = $tokens->tryConsumeTokenValue('static');
$returnTypeOrMethodName = $this->typeParser->parse($tokens);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$returnType = $returnTypeOrMethodName;
$methodName = $tokens->currentTokenValue();
$tokens->next();
} elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) {
$returnType = $isStatic ? new Ast\Type\IdentifierTypeNode('static') : 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 {
$templateTypes[] = $this->parseTemplateTagValue($tokens, false);
} 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);
return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes);
}
private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
{
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 new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue);
}
private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode
{
$name = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
$bound = $this->typeParser->parse($tokens);
} else {
$bound = null;
}
if ($tokens->tryConsumeTokenValue('=')) {
$default = $this->typeParser->parse($tokens);
} else {
$default = null;
}
if ($parseDescription) {
$description = $this->parseOptionalDescription($tokens);
} else {
$description = '';
}
return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
}
private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
{
$baseType = new IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$type = $this->typeParser->parseGeneric($tokens, $baseType);
$description = $this->parseOptionalDescription($tokens);
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 psalm-type syntax
$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
$type = $this->typeParser->parse($tokens);
return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
}
private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode
{
$importedAlias = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'from');
$importedFrom = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$importedAs = null;
if ($tokens->tryConsumeTokenValue('as')) {
$importedAs = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}
return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, new IdentifierTypeNode($importedFrom), $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);
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';
$requirePropertyOrMethod = true;
$tokens->next();
} else {
$parameter = $tokens->currentTokenValue();
$requirePropertyOrMethod = false;
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
}
if ($requirePropertyOrMethod || $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);
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);
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;
}
private function parseOptionalDescription(TokenIterator $tokens, bool $limitStartToken = false): 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 (
$this->requireWhitespaceBeforeDescription
&& !$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;
}
}

View File

@@ -0,0 +1,224 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function array_pop;
use function assert;
use function count;
use function in_array;
use function strlen;
class TokenIterator
{
/** @var mixed[][] */
private $tokens;
/** @var int */
private $index;
/** @var int[] */
private $savePoints = [];
public function __construct(array $tokens, int $index = 0)
{
$this->tokens = $tokens;
$this->index = $index;
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== Lexer::TOKEN_HORIZONTAL_WS) {
return;
}
$this->index++;
}
public function currentTokenValue(): string
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET];
}
public function currentTokenType(): int
{
return $this->tokens[$this->index][Lexer::TYPE_OFFSET];
}
public function currentTokenOffset(): int
{
$offset = 0;
for ($i = 0; $i < $this->index; $i++) {
$offset += strlen($this->tokens[$i][Lexer::VALUE_OFFSET]);
}
return $offset;
}
public function isCurrentTokenValue(string $tokenValue): bool
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue;
}
public function isCurrentTokenType(int ...$tokenType): bool
{
return in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true);
}
public function isPrecededByHorizontalWhitespace(): bool
{
return ($this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] ?? -1) === Lexer::TOKEN_HORIZONTAL_WS;
}
/**
* @throws ParserException
*/
public function consumeTokenType(int $tokenType): void
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
$this->throwError($tokenType);
}
$this->index++;
if (($this->tokens[$this->index][Lexer::TYPE_OFFSET] ?? -1) !== Lexer::TOKEN_HORIZONTAL_WS) {
return;
}
$this->index++;
}
/**
* @throws ParserException
*/
public function consumeTokenValue(int $tokenType, string $tokenValue): void
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType || $this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
$this->throwError($tokenType, $tokenValue);
}
$this->index++;
if (($this->tokens[$this->index][Lexer::TYPE_OFFSET] ?? -1) !== Lexer::TOKEN_HORIZONTAL_WS) {
return;
}
$this->index++;
}
/** @phpstan-impure */
public function tryConsumeTokenValue(string $tokenValue): bool
{
if ($this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) {
return false;
}
$this->index++;
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
$this->index++;
}
return true;
}
/** @phpstan-impure */
public function tryConsumeTokenType(int $tokenType): bool
{
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) {
return false;
}
$this->index++;
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
$this->index++;
}
return true;
}
public function getSkippedHorizontalWhiteSpaceIfAny(): string
{
if ($this->index > 0 && $this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
return $this->tokens[$this->index - 1][Lexer::VALUE_OFFSET];
}
return '';
}
/** @phpstan-impure */
public function joinUntil(int ...$tokenType): string
{
$s = '';
while (!in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true)) {
$s .= $this->tokens[$this->index++][Lexer::VALUE_OFFSET];
}
return $s;
}
public function next(): void
{
$this->index++;
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== Lexer::TOKEN_HORIZONTAL_WS) {
return;
}
$this->index++;
}
/** @phpstan-impure */
public function forwardToTheEnd(): void
{
$lastToken = count($this->tokens) - 1;
$this->index = $lastToken;
}
public function pushSavePoint(): void
{
$this->savePoints[] = $this->index;
}
public function dropSavePoint(): void
{
array_pop($this->savePoints);
}
public function rollback(): void
{
$index = array_pop($this->savePoints);
assert($index !== null);
$this->index = $index;
}
/**
* @throws ParserException
*/
private function throwError(int $expectedTokenType, ?string $expectedTokenValue = null): void
{
throw new ParserException(
$this->currentTokenValue(),
$this->currentTokenType(),
$this->currentTokenOffset(),
$expectedTokenType,
$expectedTokenValue
);
}
}

View File

@@ -0,0 +1,585 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Parser;
use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function in_array;
use function strpos;
use function trim;
class TypeParser
{
/** @var ConstExprParser|null */
private $constExprParser;
public function __construct(?ConstExprParser $constExprParser = null)
{
$this->constExprParser = $constExprParser;
}
/** @phpstan-impure */
public function parse(TokenIterator $tokens): Ast\Type\TypeNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);
} else {
$type = $this->parseAtomic($tokens);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
$type = $this->parseUnion($tokens, $type);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
$type = $this->parseIntersection($tokens, $type);
}
}
return $type;
}
/** @phpstan-impure */
private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
$type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue());
} else {
$type = $this->parseAtomic($tokens);
if ($tokens->isCurrentTokenValue('is')) {
$type = $this->parseConditional($tokens, $type);
} else {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
$type = $this->subParseUnion($tokens, $type);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
$type = $this->subParseIntersection($tokens, $type);
}
}
}
return $type;
}
/** @phpstan-impure */
private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
{
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$type = $this->subParse($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
return $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
return $type;
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
$type = new Ast\Type\ThisTypeNode();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
return $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
return $type;
}
$currentTokenValue = $tokens->currentTokenValue();
$tokens->pushSavePoint(); // because of ConstFetchNode
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$type = new Ast\Type\IdentifierTypeNode($currentTokenValue);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$tokens->dropSavePoint(); // because of ConstFetchNode
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$tokens->pushSavePoint();
$isHtml = $this->isHtml($tokens);
$tokens->rollback();
if ($isHtml) {
return $type;
}
$type = $this->parseGeneric($tokens, $type);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$type = $this->tryParseCallable($tokens, $type);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
} elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
$type = $this->parseArrayShape($tokens, $type, $type->name);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
}
return $type;
} else {
$tokens->rollback(); // because of ConstFetchNode
}
} else {
$tokens->dropSavePoint(); // because of ConstFetchNode
}
$exception = new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER
);
if ($this->constExprParser === null) {
throw $exception;
}
try {
$constExpr = $this->constExprParser->parse($tokens, true);
if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
throw $exception;
}
return new Ast\Type\ConstTypeNode($constExpr);
} catch (LogicException $e) {
throw $exception;
}
}
/** @phpstan-impure */
private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
$types = [$type];
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
$types[] = $this->parseAtomic($tokens);
}
return new Ast\Type\UnionTypeNode($types);
}
/** @phpstan-impure */
private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
$types = [$type];
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$types[] = $this->parseAtomic($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
}
return new Ast\Type\UnionTypeNode($types);
}
/** @phpstan-impure */
private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
$types = [$type];
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
$types[] = $this->parseAtomic($tokens);
}
return new Ast\Type\IntersectionTypeNode($types);
}
/** @phpstan-impure */
private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
$types = [$type];
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$types[] = $this->parseAtomic($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
}
return new Ast\Type\IntersectionTypeNode($types);
}
/** @phpstan-impure */
private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$negated = false;
if ($tokens->isCurrentTokenValue('not')) {
$negated = true;
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}
$targetType = $this->parse($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$ifType = $this->parse($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$elseType = $this->subParse($tokens);
return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated);
}
/** @phpstan-impure */
private function parseConditionalForParameter(TokenIterator $tokens, string $parameterName): Ast\Type\TypeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
$tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is');
$negated = false;
if ($tokens->isCurrentTokenValue('not')) {
$negated = true;
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}
$targetType = $this->parse($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$ifType = $this->parse($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$elseType = $this->subParse($tokens);
return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated);
}
/** @phpstan-impure */
private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
$type = $this->parseAtomic($tokens);
return new Ast\Type\NullableTypeNode($type);
}
/** @phpstan-impure */
public function isHtml(TokenIterator $tokens): bool
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
return false;
}
$htmlTagName = $tokens->currentTokenValue();
$tokens->next();
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
return false;
}
while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) {
if (
$tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)
&& strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false
) {
return true;
}
$tokens->next();
}
return false;
}
/** @phpstan-impure */
public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$genericTypes = [];
$variances = [];
[$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
// trailing comma case
return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
}
[$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
}
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
}
/**
* @phpstan-impure
* @return array{Ast\Type\TypeNode, Ast\Type\GenericTypeNode::VARIANCE_*}
*/
public function parseGenericTypeArgument(TokenIterator $tokens): array
{
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
return [
new Ast\Type\IdentifierTypeNode('mixed'),
Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT,
];
}
if ($tokens->tryConsumeTokenValue('contravariant')) {
$variance = Ast\Type\GenericTypeNode::VARIANCE_CONTRAVARIANT;
} elseif ($tokens->tryConsumeTokenValue('covariant')) {
$variance = Ast\Type\GenericTypeNode::VARIANCE_COVARIANT;
} else {
$variance = Ast\Type\GenericTypeNode::VARIANCE_INVARIANT;
}
$type = $this->parse($tokens);
return [$type, $variance];
}
/** @phpstan-impure */
private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$parameters = [];
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
$parameters[] = $this->parseCallableParameter($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
break;
}
$parameters[] = $this->parseCallableParameter($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
}
}
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$returnType = $this->parseCallableReturnType($tokens);
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType);
}
/** @phpstan-impure */
private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode
{
$type = $this->parse($tokens);
$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
$parameterName = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
} else {
$parameterName = '';
}
$isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
return new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional);
}
/** @phpstan-impure */
private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$type = $this->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
} else {
$type = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$type = $this->parseGeneric($tokens, $type);
} elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
$type = $this->parseArrayShape($tokens, $type, $type->name);
}
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
return $type;
}
/** @phpstan-impure */
private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
{
try {
$tokens->pushSavePoint();
$type = $this->parseCallable($tokens, $identifier);
$tokens->dropSavePoint();
} catch (ParserException $e) {
$tokens->rollback();
$type = $identifier;
}
return $type;
}
/** @phpstan-impure */
private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
try {
while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$tokens->pushSavePoint();
$canBeOffsetAccessType = !$tokens->isPrecededByHorizontalWhitespace();
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET);
if ($canBeOffsetAccessType && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET)) {
$offset = $this->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
$tokens->dropSavePoint();
$type = new Ast\Type\OffsetAccessTypeNode($type, $offset);
} else {
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
$tokens->dropSavePoint();
$type = new Ast\Type\ArrayTypeNode($type);
}
}
} catch (ParserException $e) {
$tokens->rollback();
}
return $type;
}
/**
* @phpstan-impure
* @param Ast\Type\ArrayShapeNode::KIND_* $kind
*/
private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, string $kind): Ast\Type\ArrayShapeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
$items = [];
$sealed = true;
do {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
return new Ast\Type\ArrayShapeNode($items, true, $kind);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) {
$sealed = false;
$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA);
break;
}
$items[] = $this->parseArrayShapeItem($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
return new Ast\Type\ArrayShapeNode($items, $sealed, $kind);
}
/** @phpstan-impure */
private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode
{
try {
$tokens->pushSavePoint();
$key = $this->parseArrayShapeKey($tokens);
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$value = $this->parse($tokens);
$tokens->dropSavePoint();
return new Ast\Type\ArrayShapeItemNode($key, $optional, $value);
} catch (ParserException $e) {
$tokens->rollback();
$value = $this->parse($tokens);
return new Ast\Type\ArrayShapeItemNode(null, false, $value);
}
}
/**
* @phpstan-impure
* @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
*/
private function parseArrayShapeKey(TokenIterator $tokens)
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue());
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
$tokens->next();
} else {
$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}
return $key;
}
}