Composer updates
This commit is contained in:
@@ -4,10 +4,6 @@ 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;
|
||||
@@ -15,49 +11,93 @@ 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)
|
||||
/** @var bool */
|
||||
private $quoteAwareConstExprString;
|
||||
|
||||
/** @var bool */
|
||||
private $useLinesAttributes;
|
||||
|
||||
/** @var bool */
|
||||
private $useIndexAttributes;
|
||||
|
||||
/**
|
||||
* @param array{lines?: bool, indexes?: bool} $usedAttributes
|
||||
*/
|
||||
public function __construct(
|
||||
bool $unescapeStrings = false,
|
||||
bool $quoteAwareConstExprString = false,
|
||||
array $usedAttributes = []
|
||||
)
|
||||
{
|
||||
$this->unescapeStrings = $unescapeStrings;
|
||||
$this->quoteAwareConstExprString = $quoteAwareConstExprString;
|
||||
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
|
||||
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
|
||||
}
|
||||
|
||||
public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\ConstExpr\ConstExprNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) {
|
||||
$value = $tokens->currentTokenValue();
|
||||
$tokens->next();
|
||||
return new Ast\ConstExpr\ConstExprFloatNode($value);
|
||||
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstExprFloatNode(str_replace('_', '', $value)),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
|
||||
$value = $tokens->currentTokenValue();
|
||||
$tokens->next();
|
||||
return new Ast\ConstExpr\ConstExprIntegerNode($value);
|
||||
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
|
||||
$value = $tokens->currentTokenValue();
|
||||
$type = $tokens->currentTokenType();
|
||||
if ($trimStrings) {
|
||||
if ($this->unescapeStrings) {
|
||||
$value = self::unescapeString($value);
|
||||
$value = StringUnescaper::unescapeString($value);
|
||||
} else {
|
||||
$value = substr($value, 1, -1);
|
||||
}
|
||||
}
|
||||
$tokens->next();
|
||||
return new Ast\ConstExpr\ConstExprStringNode($value);
|
||||
|
||||
if ($this->quoteAwareConstExprString) {
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\QuoteAwareConstExprStringNode(
|
||||
$value,
|
||||
$type === Lexer::TOKEN_SINGLE_QUOTED_STRING
|
||||
? Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED
|
||||
: Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED
|
||||
),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstExprStringNode($value),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
|
||||
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
|
||||
$identifier = $tokens->currentTokenValue();
|
||||
@@ -65,14 +105,29 @@ class ConstExprParser
|
||||
|
||||
switch (strtolower($identifier)) {
|
||||
case 'true':
|
||||
return new Ast\ConstExpr\ConstExprTrueNode();
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstExprTrueNode(),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
case 'false':
|
||||
return new Ast\ConstExpr\ConstExprFalseNode();
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstExprFalseNode(),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
case 'null':
|
||||
return new Ast\ConstExpr\ConstExprNullNode();
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstExprNullNode(),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
case 'array':
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
|
||||
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES);
|
||||
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
|
||||
}
|
||||
|
||||
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
|
||||
@@ -106,29 +161,43 @@ class ConstExprParser
|
||||
break;
|
||||
}
|
||||
|
||||
return new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName);
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return new Ast\ConstExpr\ConstFetchNode('', $identifier);
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstFetchNode('', $identifier),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
|
||||
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
|
||||
return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex);
|
||||
}
|
||||
|
||||
throw new ParserException(
|
||||
$tokens->currentTokenValue(),
|
||||
$tokens->currentTokenType(),
|
||||
$tokens->currentTokenOffset(),
|
||||
Lexer::TOKEN_IDENTIFIER
|
||||
Lexer::TOKEN_IDENTIFIER,
|
||||
null,
|
||||
$tokens->currentTokenLine()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr\ConstExprArrayNode
|
||||
private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
|
||||
{
|
||||
$items = [];
|
||||
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
|
||||
if (!$tokens->tryConsumeTokenType($endToken)) {
|
||||
do {
|
||||
$items[] = $this->parseArrayItem($tokens);
|
||||
@@ -136,12 +205,20 @@ class ConstExprParser
|
||||
$tokens->consumeTokenType($endToken);
|
||||
}
|
||||
|
||||
return new Ast\ConstExpr\ConstExprArrayNode($items);
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstExprArrayNode($items),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
|
||||
$expr = $this->parse($tokens);
|
||||
|
||||
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) {
|
||||
@@ -153,78 +230,40 @@ class ConstExprParser
|
||||
$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
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\ConstExpr\ConstExprArrayItemNode($key, $value),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154
|
||||
* @template T of Ast\ConstExpr\ConstExprNode
|
||||
* @param T $node
|
||||
* @return T
|
||||
*/
|
||||
private static function codePointToUtf8(int $num): string
|
||||
private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
|
||||
{
|
||||
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);
|
||||
$endLine = $tokens->currentTokenLine();
|
||||
$endIndex = $tokens->currentTokenIndex();
|
||||
if ($this->useLinesAttributes) {
|
||||
$node->setAttribute(Ast\Attribute::START_LINE, $startLine);
|
||||
$node->setAttribute(Ast\Attribute::END_LINE, $endLine);
|
||||
}
|
||||
|
||||
// Invalid UTF-8 codepoint escape sequence: Codepoint too large
|
||||
return "\xef\xbf\xbd";
|
||||
if ($this->useIndexAttributes) {
|
||||
$tokensArray = $tokens->getTokens();
|
||||
$endIndex--;
|
||||
if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
|
||||
$endIndex--;
|
||||
}
|
||||
|
||||
$node->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
|
||||
$node->setAttribute(Ast\Attribute::END_INDEX, $endIndex);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Lexer\Lexer;
|
||||
use function assert;
|
||||
use function json_encode;
|
||||
use function sprintf;
|
||||
use const JSON_INVALID_UTF8_SUBSTITUTE;
|
||||
use const JSON_UNESCAPED_SLASHES;
|
||||
use const JSON_UNESCAPED_UNICODE;
|
||||
|
||||
@@ -28,12 +29,16 @@ class ParserException extends Exception
|
||||
/** @var string|null */
|
||||
private $expectedTokenValue;
|
||||
|
||||
/** @var int|null */
|
||||
private $currentTokenLine;
|
||||
|
||||
public function __construct(
|
||||
string $currentTokenValue,
|
||||
int $currentTokenType,
|
||||
int $currentOffset,
|
||||
int $expectedTokenType,
|
||||
?string $expectedTokenValue = null
|
||||
?string $expectedTokenValue = null,
|
||||
?int $currentTokenLine = null
|
||||
)
|
||||
{
|
||||
$this->currentTokenValue = $currentTokenValue;
|
||||
@@ -41,13 +46,15 @@ class ParserException extends Exception
|
||||
$this->currentOffset = $currentOffset;
|
||||
$this->expectedTokenType = $expectedTokenType;
|
||||
$this->expectedTokenValue = $expectedTokenValue;
|
||||
$this->currentTokenLine = $currentTokenLine;
|
||||
|
||||
parent::__construct(sprintf(
|
||||
'Unexpected token %s, expected %s%s at offset %d',
|
||||
'Unexpected token %s, expected %s%s at offset %d%s',
|
||||
$this->formatValue($currentTokenValue),
|
||||
Lexer::TOKEN_LABELS[$expectedTokenType],
|
||||
$expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '',
|
||||
$currentOffset
|
||||
$currentOffset,
|
||||
$currentTokenLine === null ? '' : sprintf(' on line %d', $currentTokenLine)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -82,9 +89,15 @@ class ParserException extends Exception
|
||||
}
|
||||
|
||||
|
||||
public function getCurrentTokenLine(): ?int
|
||||
{
|
||||
return $this->currentTokenLine;
|
||||
}
|
||||
|
||||
|
||||
private function formatValue(string $value): string
|
||||
{
|
||||
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
assert($json !== false);
|
||||
|
||||
return $json;
|
||||
|
||||
@@ -28,11 +28,32 @@ class PhpDocParser
|
||||
/** @var bool */
|
||||
private $requireWhitespaceBeforeDescription;
|
||||
|
||||
public function __construct(TypeParser $typeParser, ConstExprParser $constantExprParser, bool $requireWhitespaceBeforeDescription = false)
|
||||
/** @var bool */
|
||||
private $preserveTypeAliasesWithInvalidTypes;
|
||||
|
||||
/** @var bool */
|
||||
private $useLinesAttributes;
|
||||
|
||||
/** @var bool */
|
||||
private $useIndexAttributes;
|
||||
|
||||
/**
|
||||
* @param array{lines?: bool, indexes?: bool} $usedAttributes
|
||||
*/
|
||||
public function __construct(
|
||||
TypeParser $typeParser,
|
||||
ConstExprParser $constantExprParser,
|
||||
bool $requireWhitespaceBeforeDescription = false,
|
||||
bool $preserveTypeAliasesWithInvalidTypes = false,
|
||||
array $usedAttributes = []
|
||||
)
|
||||
{
|
||||
$this->typeParser = $typeParser;
|
||||
$this->constantExprParser = $constantExprParser;
|
||||
$this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription;
|
||||
$this->preserveTypeAliasesWithInvalidTypes = $preserveTypeAliasesWithInvalidTypes;
|
||||
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
|
||||
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,30 +75,82 @@ class PhpDocParser
|
||||
$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 new Ast\PhpDoc\PhpDocNode([
|
||||
new Ast\PhpDoc\PhpDocTagNode($name, new Ast\PhpDoc\InvalidTagValueNode($e->getMessage(), $e)),
|
||||
]);
|
||||
|
||||
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]), 1, 0);
|
||||
}
|
||||
|
||||
return new Ast\PhpDoc\PhpDocNode(array_values($children));
|
||||
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode(array_values($children)), 1, 0);
|
||||
}
|
||||
|
||||
|
||||
private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode
|
||||
{
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
|
||||
return $this->parseTag($tokens);
|
||||
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
return $this->enrichWithAttributes($tokens, $this->parseTag($tokens), $startLine, $startIndex);
|
||||
}
|
||||
|
||||
return $this->parseText($tokens);
|
||||
$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
|
||||
{
|
||||
$endLine = $tokens->currentTokenLine();
|
||||
$endIndex = $tokens->currentTokenIndex();
|
||||
|
||||
if ($this->useLinesAttributes) {
|
||||
$tag->setAttribute(Ast\Attribute::START_LINE, $startLine);
|
||||
$tag->setAttribute(Ast\Attribute::END_LINE, $endLine);
|
||||
}
|
||||
|
||||
if ($this->useIndexAttributes) {
|
||||
$tokensArray = $tokens->getTokens();
|
||||
if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_CLOSE_PHPDOC) {
|
||||
$endIndex--;
|
||||
if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
|
||||
$endIndex--;
|
||||
}
|
||||
} elseif ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) {
|
||||
$endIndex--;
|
||||
}
|
||||
|
||||
$tag->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
|
||||
$tag->setAttribute(Ast\Attribute::END_INDEX, $endIndex);
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +193,9 @@ class PhpDocParser
|
||||
|
||||
public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
|
||||
try {
|
||||
$tokens->pushSavePoint();
|
||||
|
||||
@@ -247,7 +323,7 @@ class PhpDocParser
|
||||
$tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens), $e);
|
||||
}
|
||||
|
||||
return $tagValue;
|
||||
return $this->enrichWithAttributes($tokens, $tagValue, $startLine, $startIndex);
|
||||
}
|
||||
|
||||
|
||||
@@ -257,9 +333,7 @@ class PhpDocParser
|
||||
private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
|
||||
{
|
||||
if (
|
||||
$tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE)
|
||||
|| $tokens->isCurrentTokenType(Lexer::TOKEN_VARIADIC)
|
||||
|| $tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)
|
||||
$tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE, Lexer::TOKEN_VARIADIC, Lexer::TOKEN_VARIABLE)
|
||||
) {
|
||||
$type = null;
|
||||
} else {
|
||||
@@ -329,6 +403,8 @@ class PhpDocParser
|
||||
private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueNode
|
||||
{
|
||||
$isStatic = $tokens->tryConsumeTokenValue('static');
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
$returnTypeOrMethodName = $this->typeParser->parse($tokens);
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
|
||||
@@ -337,7 +413,9 @@ class PhpDocParser
|
||||
$tokens->next();
|
||||
|
||||
} elseif ($returnTypeOrMethodName instanceof Ast\Type\IdentifierTypeNode) {
|
||||
$returnType = $isStatic ? new Ast\Type\IdentifierTypeNode('static') : null;
|
||||
$returnType = $isStatic
|
||||
? $this->typeParser->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('static'), $startLine, $startIndex)
|
||||
: null;
|
||||
$methodName = $returnTypeOrMethodName->name;
|
||||
$isStatic = false;
|
||||
|
||||
@@ -347,9 +425,12 @@ class PhpDocParser
|
||||
}
|
||||
|
||||
$templateTypes = [];
|
||||
|
||||
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
|
||||
do {
|
||||
$templateTypes[] = $this->parseTemplateTagValue($tokens, false);
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
$templateTypes[] = $this->enrichWithAttributes($tokens, $this->parseTemplateTagValue($tokens, false), $startLine, $startIndex);
|
||||
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
|
||||
}
|
||||
@@ -370,6 +451,9 @@ class PhpDocParser
|
||||
|
||||
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:
|
||||
@@ -394,7 +478,12 @@ class PhpDocParser
|
||||
$defaultValue = null;
|
||||
}
|
||||
|
||||
return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue);
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
|
||||
private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode
|
||||
@@ -426,10 +515,15 @@ class PhpDocParser
|
||||
|
||||
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, $baseType);
|
||||
$type = $this->typeParser->parseGeneric(
|
||||
$tokens,
|
||||
$this->typeParser->enrichWithAttributes($tokens, $baseType, $startLine, $startIndex)
|
||||
);
|
||||
|
||||
$description = $this->parseOptionalDescription($tokens);
|
||||
|
||||
@@ -453,6 +547,34 @@ class PhpDocParser
|
||||
// support psalm-type syntax
|
||||
$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
|
||||
|
||||
if ($this->preserveTypeAliasesWithInvalidTypes) {
|
||||
$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);
|
||||
return new Ast\PhpDoc\TypeAliasTagValueNode(
|
||||
$alias,
|
||||
$this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$type = $this->typeParser->parse($tokens);
|
||||
|
||||
return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
|
||||
@@ -465,8 +587,16 @@ class PhpDocParser
|
||||
|
||||
$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')) {
|
||||
@@ -474,7 +604,7 @@ class PhpDocParser
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
|
||||
}
|
||||
|
||||
return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, new IdentifierTypeNode($importedFrom), $importedAs);
|
||||
return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, $importedFromType, $importedAs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
96
vendor/phpstan/phpdoc-parser/src/Parser/StringUnescaper.php
vendored
Normal file
96
vendor/phpstan/phpdoc-parser/src/Parser/StringUnescaper.php
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace PHPStan\PhpDocParser\Parser;
|
||||
|
||||
use function chr;
|
||||
use function hexdec;
|
||||
use function octdec;
|
||||
use function preg_replace_callback;
|
||||
use function str_replace;
|
||||
use function substr;
|
||||
|
||||
class StringUnescaper
|
||||
{
|
||||
|
||||
private const REPLACEMENTS = [
|
||||
'\\' => '\\',
|
||||
'n' => "\n",
|
||||
'r' => "\r",
|
||||
't' => "\t",
|
||||
'f' => "\f",
|
||||
'v' => "\v",
|
||||
'e' => "\x1B",
|
||||
];
|
||||
|
||||
public 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((int) hexdec(substr($str, 1)));
|
||||
}
|
||||
if ($str[0] === 'u') {
|
||||
return self::codePointToUtf8((int) hexdec($matches[2]));
|
||||
}
|
||||
|
||||
return chr((int) 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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PHPStan\PhpDocParser\Parser;
|
||||
|
||||
use LogicException;
|
||||
use PHPStan\PhpDocParser\Lexer\Lexer;
|
||||
use function array_pop;
|
||||
use function assert;
|
||||
@@ -12,7 +13,7 @@ use function strlen;
|
||||
class TokenIterator
|
||||
{
|
||||
|
||||
/** @var mixed[][] */
|
||||
/** @var list<array{string, int, int}> */
|
||||
private $tokens;
|
||||
|
||||
/** @var int */
|
||||
@@ -21,6 +22,9 @@ class TokenIterator
|
||||
/** @var int[] */
|
||||
private $savePoints = [];
|
||||
|
||||
/**
|
||||
* @param list<array{string, int, int}> $tokens
|
||||
*/
|
||||
public function __construct(array $tokens, int $index = 0)
|
||||
{
|
||||
$this->tokens = $tokens;
|
||||
@@ -34,6 +38,36 @@ class TokenIterator
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return list<array{string, int, int}>
|
||||
*/
|
||||
public function getTokens(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
|
||||
public function getContentBetween(int $startPos, int $endPos): string
|
||||
{
|
||||
if ($startPos < 0 || $endPos > count($this->tokens)) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$content = '';
|
||||
for ($i = $startPos; $i < $endPos; $i++) {
|
||||
$content .= $this->tokens[$i][Lexer::VALUE_OFFSET];
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
||||
public function getTokenCount(): int
|
||||
{
|
||||
return count($this->tokens);
|
||||
}
|
||||
|
||||
|
||||
public function currentTokenValue(): string
|
||||
{
|
||||
return $this->tokens[$this->index][Lexer::VALUE_OFFSET];
|
||||
@@ -57,6 +91,18 @@ class TokenIterator
|
||||
}
|
||||
|
||||
|
||||
public function currentTokenLine(): int
|
||||
{
|
||||
return $this->tokens[$this->index][Lexer::LINE_OFFSET];
|
||||
}
|
||||
|
||||
|
||||
public function currentTokenIndex(): int
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
|
||||
public function isCurrentTokenValue(string $tokenValue): bool
|
||||
{
|
||||
return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue;
|
||||
@@ -217,8 +263,69 @@ class TokenIterator
|
||||
$this->currentTokenType(),
|
||||
$this->currentTokenOffset(),
|
||||
$expectedTokenType,
|
||||
$expectedTokenValue
|
||||
$expectedTokenValue,
|
||||
$this->currentTokenLine()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the position is directly preceded by a certain token type.
|
||||
*
|
||||
* During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
|
||||
*/
|
||||
public function hasTokenImmediatelyBefore(int $pos, int $expectedTokenType): bool
|
||||
{
|
||||
$tokens = $this->tokens;
|
||||
$pos--;
|
||||
for (; $pos >= 0; $pos--) {
|
||||
$token = $tokens[$pos];
|
||||
$type = $token[Lexer::TYPE_OFFSET];
|
||||
if ($type === $expectedTokenType) {
|
||||
return true;
|
||||
}
|
||||
if (!in_array($type, [
|
||||
Lexer::TOKEN_HORIZONTAL_WS,
|
||||
Lexer::TOKEN_PHPDOC_EOL,
|
||||
], true)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the position is directly followed by a certain token type.
|
||||
*
|
||||
* During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped
|
||||
*/
|
||||
public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool
|
||||
{
|
||||
$tokens = $this->tokens;
|
||||
$pos++;
|
||||
for ($c = count($tokens); $pos < $c; $pos++) {
|
||||
$token = $tokens[$pos];
|
||||
$type = $token[Lexer::TYPE_OFFSET];
|
||||
if ($type === $expectedTokenType) {
|
||||
return true;
|
||||
}
|
||||
if (!in_array($type, [
|
||||
Lexer::TOKEN_HORIZONTAL_WS,
|
||||
Lexer::TOKEN_PHPDOC_EOL,
|
||||
], true)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given position is immediately surrounded by parenthesis.
|
||||
*/
|
||||
public function hasParentheses(int $startPos, int $endPos): bool
|
||||
{
|
||||
return $this->hasTokenImmediatelyBefore($startPos, Lexer::TOKEN_OPEN_PARENTHESES)
|
||||
&& $this->hasTokenImmediatelyAfter($endPos, Lexer::TOKEN_CLOSE_PARENTHESES);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use LogicException;
|
||||
use PHPStan\PhpDocParser\Ast;
|
||||
use PHPStan\PhpDocParser\Lexer\Lexer;
|
||||
use function in_array;
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
use function trim;
|
||||
|
||||
@@ -15,14 +16,35 @@ class TypeParser
|
||||
/** @var ConstExprParser|null */
|
||||
private $constExprParser;
|
||||
|
||||
public function __construct(?ConstExprParser $constExprParser = null)
|
||||
/** @var bool */
|
||||
private $quoteAwareConstExprString;
|
||||
|
||||
/** @var bool */
|
||||
private $useLinesAttributes;
|
||||
|
||||
/** @var bool */
|
||||
private $useIndexAttributes;
|
||||
|
||||
/**
|
||||
* @param array{lines?: bool, indexes?: bool} $usedAttributes
|
||||
*/
|
||||
public function __construct(
|
||||
?ConstExprParser $constExprParser = null,
|
||||
bool $quoteAwareConstExprString = false,
|
||||
array $usedAttributes = []
|
||||
)
|
||||
{
|
||||
$this->constExprParser = $constExprParser;
|
||||
$this->quoteAwareConstExprString = $quoteAwareConstExprString;
|
||||
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
|
||||
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
|
||||
}
|
||||
|
||||
/** @phpstan-impure */
|
||||
public function parse(TokenIterator $tokens): Ast\Type\TypeNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
|
||||
$type = $this->parseNullable($tokens);
|
||||
|
||||
@@ -37,12 +59,45 @@ class TypeParser
|
||||
}
|
||||
}
|
||||
|
||||
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @template T of Ast\Node
|
||||
* @param T $type
|
||||
* @return T
|
||||
*/
|
||||
public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node
|
||||
{
|
||||
$endLine = $tokens->currentTokenLine();
|
||||
$endIndex = $tokens->currentTokenIndex();
|
||||
|
||||
if ($this->useLinesAttributes) {
|
||||
$type->setAttribute(Ast\Attribute::START_LINE, $startLine);
|
||||
$type->setAttribute(Ast\Attribute::END_LINE, $endLine);
|
||||
}
|
||||
|
||||
if ($this->useIndexAttributes) {
|
||||
$tokensArray = $tokens->getTokens();
|
||||
$endIndex--;
|
||||
if ($tokensArray[$endIndex][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
|
||||
$endIndex--;
|
||||
}
|
||||
|
||||
$type->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
|
||||
$type->setAttribute(Ast\Attribute::END_INDEX, $endIndex);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/** @phpstan-impure */
|
||||
private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
|
||||
$type = $this->parseNullable($tokens);
|
||||
|
||||
@@ -66,13 +121,16 @@ class TypeParser
|
||||
}
|
||||
}
|
||||
|
||||
return $type;
|
||||
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
|
||||
}
|
||||
|
||||
|
||||
/** @phpstan-impure */
|
||||
private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
|
||||
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
|
||||
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
|
||||
$type = $this->subParse($tokens);
|
||||
@@ -81,26 +139,26 @@ class TypeParser
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
return $this->tryParseArrayOrOffsetAccess($tokens, $type);
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
|
||||
}
|
||||
|
||||
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
|
||||
$type = new Ast\Type\ThisTypeNode();
|
||||
$type = $this->enrichWithAttributes($tokens, new Ast\Type\ThisTypeNode(), $startLine, $startIndex);
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
return $this->tryParseArrayOrOffsetAccess($tokens, $type);
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
|
||||
}
|
||||
|
||||
$currentTokenValue = $tokens->currentTokenValue();
|
||||
$tokens->pushSavePoint(); // because of ConstFetchNode
|
||||
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
|
||||
$type = new Ast\Type\IdentifierTypeNode($currentTokenValue);
|
||||
$type = $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode($currentTokenValue), $startLine, $startIndex);
|
||||
|
||||
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
|
||||
$tokens->dropSavePoint(); // because of ConstFetchNode
|
||||
@@ -124,15 +182,22 @@ class TypeParser
|
||||
} 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);
|
||||
} elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
|
||||
if ($type->name === 'object') {
|
||||
$type = $this->parseObjectShape($tokens);
|
||||
} else {
|
||||
$type = $this->parseArrayShape($tokens, $type, $type->name);
|
||||
}
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
|
||||
$type = $this->tryParseArrayOrOffsetAccess(
|
||||
$tokens,
|
||||
$this->enrichWithAttributes($tokens, $type, $startLine, $startIndex)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $type;
|
||||
return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
|
||||
} else {
|
||||
$tokens->rollback(); // because of ConstFetchNode
|
||||
}
|
||||
@@ -144,7 +209,9 @@ class TypeParser
|
||||
$tokens->currentTokenValue(),
|
||||
$tokens->currentTokenType(),
|
||||
$tokens->currentTokenOffset(),
|
||||
Lexer::TOKEN_IDENTIFIER
|
||||
Lexer::TOKEN_IDENTIFIER,
|
||||
null,
|
||||
$tokens->currentTokenLine()
|
||||
);
|
||||
|
||||
if ($this->constExprParser === null) {
|
||||
@@ -157,7 +224,7 @@ class TypeParser
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return new Ast\Type\ConstTypeNode($constExpr);
|
||||
return $this->enrichWithAttributes($tokens, new Ast\Type\ConstTypeNode($constExpr), $startLine, $startIndex);
|
||||
} catch (LogicException $e) {
|
||||
throw $exception;
|
||||
}
|
||||
@@ -336,7 +403,14 @@ class TypeParser
|
||||
$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);
|
||||
$type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
|
||||
$startLine = $baseType->getAttribute(Ast\Attribute::START_LINE);
|
||||
$startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX);
|
||||
if ($startLine !== null && $startIndex !== null) {
|
||||
$type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
[$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens);
|
||||
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
|
||||
@@ -345,7 +419,14 @@ class TypeParser
|
||||
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
|
||||
|
||||
return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
|
||||
$type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
|
||||
$startLine = $baseType->getAttribute(Ast\Attribute::START_LINE);
|
||||
$startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX);
|
||||
if ($startLine !== null && $startIndex !== null) {
|
||||
$type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
|
||||
@@ -355,9 +436,11 @@ class TypeParser
|
||||
*/
|
||||
public function parseGenericTypeArgument(TokenIterator $tokens): array
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
|
||||
return [
|
||||
new Ast\Type\IdentifierTypeNode('mixed'),
|
||||
$this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('mixed'), $startLine, $startIndex),
|
||||
Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT,
|
||||
];
|
||||
}
|
||||
@@ -397,7 +480,10 @@ class TypeParser
|
||||
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
|
||||
$returnType = $this->parseCallableReturnType($tokens);
|
||||
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
$returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex);
|
||||
|
||||
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType);
|
||||
}
|
||||
@@ -406,6 +492,8 @@ class TypeParser
|
||||
/** @phpstan-impure */
|
||||
private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
$type = $this->parse($tokens);
|
||||
$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
|
||||
$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
|
||||
@@ -419,37 +507,141 @@ class TypeParser
|
||||
}
|
||||
|
||||
$isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
|
||||
return new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional);
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/** @phpstan-impure */
|
||||
private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
|
||||
$type = $this->parseNullable($tokens);
|
||||
return $this->parseNullable($tokens);
|
||||
|
||||
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
|
||||
$type = $this->parse($tokens);
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
|
||||
$type = new Ast\Type\ThisTypeNode();
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
));
|
||||
}
|
||||
|
||||
return $type;
|
||||
} else {
|
||||
$type = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
|
||||
$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_OPEN_ANGLE_BRACKET)) {
|
||||
$type = $this->parseGeneric($tokens, $type);
|
||||
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
|
||||
$type = $this->parseGeneric(
|
||||
$tokens,
|
||||
$this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
)
|
||||
);
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
));
|
||||
}
|
||||
|
||||
} elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
|
||||
$type = $this->parseArrayShape($tokens, $type, $type->name);
|
||||
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
));
|
||||
|
||||
} elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
|
||||
if ($type->name === 'object') {
|
||||
$type = $this->parseObjectShape($tokens);
|
||||
} else {
|
||||
$type = $this->parseArrayShape($tokens, $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
), $type->name);
|
||||
}
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $type;
|
||||
} else {
|
||||
$tokens->rollback(); // because of ConstFetchNode
|
||||
}
|
||||
} else {
|
||||
$tokens->dropSavePoint(); // because of ConstFetchNode
|
||||
}
|
||||
}
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
|
||||
$exception = new ParserException(
|
||||
$tokens->currentTokenValue(),
|
||||
$tokens->currentTokenType(),
|
||||
$tokens->currentTokenOffset(),
|
||||
Lexer::TOKEN_IDENTIFIER,
|
||||
null,
|
||||
$tokens->currentTokenLine()
|
||||
);
|
||||
|
||||
if ($this->constExprParser === null) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return $type;
|
||||
try {
|
||||
$constExpr = $this->constExprParser->parse($tokens, true);
|
||||
if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$type = new Ast\Type\ConstTypeNode($constExpr);
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
));
|
||||
}
|
||||
|
||||
return $type;
|
||||
} catch (LogicException $e) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +665,8 @@ class TypeParser
|
||||
/** @phpstan-impure */
|
||||
private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
|
||||
{
|
||||
$startLine = $type->getAttribute(Ast\Attribute::START_LINE);
|
||||
$startIndex = $type->getAttribute(Ast\Attribute::START_INDEX);
|
||||
try {
|
||||
while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
|
||||
$tokens->pushSavePoint();
|
||||
@@ -485,10 +679,28 @@ class TypeParser
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
|
||||
$tokens->dropSavePoint();
|
||||
$type = new Ast\Type\OffsetAccessTypeNode($type, $offset);
|
||||
|
||||
if ($startLine !== null && $startIndex !== null) {
|
||||
$type = $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
|
||||
$tokens->dropSavePoint();
|
||||
$type = new Ast\Type\ArrayTypeNode($type);
|
||||
|
||||
if ($startLine !== null && $startIndex !== null) {
|
||||
$type = $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$type,
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,6 +751,8 @@ class TypeParser
|
||||
/** @phpstan-impure */
|
||||
private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
try {
|
||||
$tokens->pushSavePoint();
|
||||
$key = $this->parseArrayShapeKey($tokens);
|
||||
@@ -547,12 +761,22 @@ class TypeParser
|
||||
$value = $this->parse($tokens);
|
||||
$tokens->dropSavePoint();
|
||||
|
||||
return new Ast\Type\ArrayShapeItemNode($key, $optional, $value);
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\Type\ArrayShapeItemNode($key, $optional, $value),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
} catch (ParserException $e) {
|
||||
$tokens->rollback();
|
||||
$value = $this->parse($tokens);
|
||||
|
||||
return new Ast\Type\ArrayShapeItemNode(null, false, $value);
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
new Ast\Type\ArrayShapeItemNode(null, false, $value),
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,16 +786,28 @@ class TypeParser
|
||||
*/
|
||||
private function parseArrayShapeKey(TokenIterator $tokens)
|
||||
{
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
|
||||
$key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue());
|
||||
$key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
|
||||
$tokens->next();
|
||||
|
||||
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
|
||||
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
|
||||
if ($this->quoteAwareConstExprString) {
|
||||
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
|
||||
} else {
|
||||
$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(), '"'));
|
||||
if ($this->quoteAwareConstExprString) {
|
||||
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
|
||||
} else {
|
||||
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
|
||||
}
|
||||
|
||||
$tokens->next();
|
||||
|
||||
} else {
|
||||
@@ -579,7 +815,86 @@ class TypeParser
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
|
||||
}
|
||||
|
||||
return $key;
|
||||
return $this->enrichWithAttributes(
|
||||
$tokens,
|
||||
$key,
|
||||
$startLine,
|
||||
$startIndex
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-impure
|
||||
*/
|
||||
private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode
|
||||
{
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
|
||||
|
||||
$items = [];
|
||||
|
||||
do {
|
||||
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
|
||||
|
||||
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
|
||||
return new Ast\Type\ObjectShapeNode($items);
|
||||
}
|
||||
|
||||
$items[] = $this->parseObjectShapeItem($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\ObjectShapeNode($items);
|
||||
}
|
||||
|
||||
/** @phpstan-impure */
|
||||
private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
|
||||
$key = $this->parseObjectShapeKey($tokens);
|
||||
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
|
||||
$value = $this->parse($tokens);
|
||||
|
||||
return $this->enrichWithAttributes($tokens, new Ast\Type\ObjectShapeItemNode($key, $optional, $value), $startLine, $startIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-impure
|
||||
* @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
|
||||
*/
|
||||
private function parseObjectShapeKey(TokenIterator $tokens)
|
||||
{
|
||||
$startLine = $tokens->currentTokenLine();
|
||||
$startIndex = $tokens->currentTokenIndex();
|
||||
|
||||
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
|
||||
if ($this->quoteAwareConstExprString) {
|
||||
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
|
||||
} else {
|
||||
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
|
||||
}
|
||||
$tokens->next();
|
||||
|
||||
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
|
||||
if ($this->quoteAwareConstExprString) {
|
||||
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
|
||||
} else {
|
||||
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
|
||||
}
|
||||
$tokens->next();
|
||||
|
||||
} else {
|
||||
$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
|
||||
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
|
||||
}
|
||||
|
||||
return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user