Composer Workspace global packages

This commit is contained in:
Clemens Schwaighofer
2023-08-02 14:52:33 +09:00
parent c383a7b7b7
commit 1fc144e178
239 changed files with 5659 additions and 2712 deletions

View File

@@ -245,22 +245,14 @@ class ConstExprParser
*/
private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
{
$endLine = $tokens->currentTokenLine();
$endIndex = $tokens->currentTokenIndex();
if ($this->useLinesAttributes) {
$node->setAttribute(Ast\Attribute::START_LINE, $startLine);
$node->setAttribute(Ast\Attribute::END_LINE, $endLine);
$node->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
}
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);
$node->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
}
return $node;

View File

@@ -2,15 +2,25 @@
namespace PHPStan\PhpDocParser\Parser;
use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\ShouldNotHappenException;
use function array_key_exists;
use function array_values;
use function count;
use function rtrim;
use function str_replace;
use function trim;
/**
* @phpstan-import-type ValueType from Doctrine\DoctrineArgument as DoctrineValueType
*/
class PhpDocParser
{
@@ -31,12 +41,18 @@ class PhpDocParser
/** @var bool */
private $preserveTypeAliasesWithInvalidTypes;
/** @var bool */
private $parseDoctrineAnnotations;
/** @var bool */
private $useLinesAttributes;
/** @var bool */
private $useIndexAttributes;
/** @var bool */
private $textBetweenTagsBelongsToDescription;
/**
* @param array{lines?: bool, indexes?: bool} $usedAttributes
*/
@@ -45,15 +61,19 @@ class PhpDocParser
ConstExprParser $constantExprParser,
bool $requireWhitespaceBeforeDescription = false,
bool $preserveTypeAliasesWithInvalidTypes = false,
array $usedAttributes = []
array $usedAttributes = [],
bool $parseDoctrineAnnotations = false,
bool $textBetweenTagsBelongsToDescription = false
)
{
$this->typeParser = $typeParser;
$this->constantExprParser = $constantExprParser;
$this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription;
$this->preserveTypeAliasesWithInvalidTypes = $preserveTypeAliasesWithInvalidTypes;
$this->parseDoctrineAnnotations = $parseDoctrineAnnotations;
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
$this->textBetweenTagsBelongsToDescription = $textBetweenTagsBelongsToDescription;
}
@@ -64,10 +84,44 @@ class PhpDocParser
$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)) {
if ($this->parseDoctrineAnnotations) {
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
$lastChild = $this->parseChild($tokens);
$children[] = $lastChild;
while (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
if (
$lastChild instanceof Ast\PhpDoc\PhpDocTagNode
&& (
$lastChild->value instanceof Doctrine\DoctrineTagValueNode
|| $lastChild->value instanceof Ast\PhpDoc\GenericTagValueNode
)
) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
break;
}
$lastChild = $this->parseChild($tokens);
$children[] = $lastChild;
continue;
}
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
break;
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
break;
}
$lastChild = $this->parseChild($tokens);
$children[] = $lastChild;
}
}
} else {
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
$children[] = $this->parseChild($tokens);
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL) && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
$children[] = $this->parseChild($tokens);
}
}
}
@@ -105,6 +159,7 @@ class PhpDocParser
}
/** @phpstan-impure */
private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
@@ -113,6 +168,26 @@ class PhpDocParser
return $this->enrichWithAttributes($tokens, $this->parseTag($tokens), $startLine, $startIndex);
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_TAG)) {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$tag = $tokens->currentTokenValue();
$tokens->next();
$tagStartLine = $tokens->currentTokenLine();
$tagStartIndex = $tokens->currentTokenIndex();
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocTagNode(
$tag,
$this->enrichWithAttributes(
$tokens,
$this->parseDoctrineTagValue($tokens, $tag),
$tagStartLine,
$tagStartIndex
)
), $startLine, $startIndex);
}
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$text = $this->parseText($tokens);
@@ -127,27 +202,14 @@ class PhpDocParser
*/
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);
$tag->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
}
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);
$tag->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
}
return $tag;
@@ -158,29 +220,144 @@ class PhpDocParser
{
$text = '';
while (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
$text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END);
$endTokens = [Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
if ($this->textBetweenTagsBelongsToDescription) {
$endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
}
$savepoint = false;
// if the next token is EOL, everything below is skipped and empty string is returned
while ($this->textBetweenTagsBelongsToDescription || !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
$tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
$text .= $tmpText;
// stop if we're not at EOL - meaning it's the end of PHPDoc
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
break;
}
if ($this->textBetweenTagsBelongsToDescription) {
if (!$savepoint) {
$tokens->pushSavePoint();
$savepoint = true;
} elseif ($tmpText !== '') {
$tokens->dropSavePoint();
$tokens->pushSavePoint();
}
}
$tokens->pushSavePoint();
$tokens->next();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END)) {
// if we're at EOL, check what's next
// if next is a PHPDoc tag, EOL, or end of PHPDoc, stop
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) {
$tokens->rollback();
break;
}
// otherwise if the next is text, continue building the description string
$tokens->dropSavePoint();
$text .= "\n";
$text .= $tokens->getDetectedNewline() ?? "\n";
}
if ($savepoint) {
$tokens->rollback();
$text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
}
return new Ast\PhpDoc\PhpDocTextNode(trim($text, " \t"));
}
private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens): string
{
$text = '';
$endTokens = [Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
if ($this->textBetweenTagsBelongsToDescription) {
$endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
}
$savepoint = false;
// if the next token is EOL, everything below is skipped and empty string is returned
while ($this->textBetweenTagsBelongsToDescription || !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
$tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
$text .= $tmpText;
// stop if we're not at EOL - meaning it's the end of PHPDoc
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
if (!$tokens->isPrecededByHorizontalWhitespace()) {
return trim($text . $this->parseText($tokens)->text, " \t");
}
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG)) {
$tokens->pushSavePoint();
$child = $this->parseChild($tokens);
if ($child instanceof Ast\PhpDoc\PhpDocTagNode) {
if (
$child->value instanceof Ast\PhpDoc\GenericTagValueNode
|| $child->value instanceof Doctrine\DoctrineTagValueNode
) {
$tokens->rollback();
break;
}
if ($child->value instanceof Ast\PhpDoc\InvalidTagValueNode) {
$tokens->rollback();
$tokens->pushSavePoint();
$tokens->next();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tokens->rollback();
break;
}
$tokens->rollback();
return trim($text . $this->parseText($tokens)->text, " \t");
}
}
$tokens->rollback();
return trim($text . $this->parseText($tokens)->text, " \t");
}
break;
}
if ($this->textBetweenTagsBelongsToDescription) {
if (!$savepoint) {
$tokens->pushSavePoint();
$savepoint = true;
} elseif ($tmpText !== '') {
$tokens->dropSavePoint();
$tokens->pushSavePoint();
}
}
$tokens->pushSavePoint();
$tokens->next();
// if we're at EOL, check what's next
// if next is a PHPDoc tag, EOL, or end of PHPDoc, stop
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, ...$endTokens)) {
$tokens->rollback();
break;
}
// otherwise if the next is text, continue building the description string
$tokens->dropSavePoint();
$text .= $tokens->getDetectedNewline() ?? "\n";
}
if ($savepoint) {
$tokens->rollback();
$text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
}
return trim($text, " \t");
}
public function parseTag(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagNode
{
$tag = $tokens->currentTokenValue();
@@ -312,7 +489,17 @@ class PhpDocParser
break;
default:
if ($this->parseDoctrineAnnotations) {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$tagValue = $this->parseDoctrineTagValue($tokens, $tag);
} else {
$tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescriptionAfterDoctrineTag($tokens));
}
break;
}
$tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens));
break;
}
@@ -327,6 +514,297 @@ class PhpDocParser
}
private function parseDoctrineTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\PhpDocTagValueNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
return new Doctrine\DoctrineTagValueNode(
$this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineAnnotation($tag, $this->parseDoctrineArguments($tokens, false)),
$startLine,
$startIndex
),
$this->parseOptionalDescriptionAfterDoctrineTag($tokens)
);
}
/**
* @return list<Doctrine\DoctrineArgument>
*/
private function parseDoctrineArguments(TokenIterator $tokens, bool $deep): array
{
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
return [];
}
if (!$deep) {
$tokens->addEndOfLineToSkippedTokens();
}
$arguments = [];
try {
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
do {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
break;
}
$arguments[] = $this->parseDoctrineArgument($tokens);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
} finally {
if (!$deep) {
$tokens->removeEndOfLineFromSkippedTokens();
}
}
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
return $arguments;
}
private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\DoctrineArgument
{
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
$startLine,
$startIndex
);
}
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$tokens->pushSavePoint();
$currentValue = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
$key = $this->enrichWithAttributes(
$tokens,
new IdentifierTypeNode($currentValue),
$startLine,
$startIndex
);
$tokens->consumeTokenType(Lexer::TOKEN_EQUAL);
$value = $this->parseDoctrineArgumentValue($tokens);
$tokens->dropSavePoint();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArgument($key, $value),
$startLine,
$startIndex
);
} catch (ParserException $e) {
$tokens->rollback();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArgument(null, $this->parseDoctrineArgumentValue($tokens)),
$startLine,
$startIndex
);
}
}
/**
* @return DoctrineValueType
*/
private function parseDoctrineArgumentValue(TokenIterator $tokens)
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG)) {
$name = $tokens->currentTokenValue();
$tokens->next();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineAnnotation($name, $this->parseDoctrineArguments($tokens, true)),
$startLine,
$startIndex
);
}
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET)) {
$items = [];
do {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
break;
}
$items[] = $this->parseDoctrineArrayItem($tokens);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArray($items),
$startLine,
$startIndex
);
}
$currentTokenValue = $tokens->currentTokenValue();
$tokens->pushSavePoint(); // because of ConstFetchNode
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$identifier = $this->enrichWithAttributes(
$tokens,
new Ast\Type\IdentifierTypeNode($currentTokenValue),
$startLine,
$startIndex
);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$tokens->dropSavePoint();
return $identifier;
}
$tokens->rollback(); // because of ConstFetchNode
} else {
$tokens->dropSavePoint(); // because of ConstFetchNode
}
$exception = new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine()
);
try {
$constExpr = $this->constantExprParser->parse($tokens, true);
if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
throw $exception;
}
return $constExpr;
} catch (LogicException $e) {
throw $exception;
}
}
private function parseDoctrineArrayItem(TokenIterator $tokens): Doctrine\DoctrineArrayItem
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
try {
$tokens->pushSavePoint();
$key = $this->parseDoctrineArrayKey($tokens);
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL)) {
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COLON)) {
$tokens->consumeTokenType(Lexer::TOKEN_EQUAL); // will throw exception
}
}
$value = $this->parseDoctrineArgumentValue($tokens);
$tokens->dropSavePoint();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArrayItem($key, $value),
$startLine,
$startIndex
);
} catch (ParserException $e) {
$tokens->rollback();
return $this->enrichWithAttributes(
$tokens,
new Doctrine\DoctrineArrayItem(null, $this->parseDoctrineArgumentValue($tokens)),
$startLine,
$startIndex
);
}
}
/**
* @return ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|ConstFetchNode
*/
private function parseDoctrineArrayKey(TokenIterator $tokens)
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
$tokens->next();
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
$tokens->next();
} else {
$currentTokenValue = $tokens->currentTokenValue();
$tokens->pushSavePoint(); // because of ConstFetchNode
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
$tokens->dropSavePoint();
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine()
);
}
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$tokens->dropSavePoint();
return $this->enrichWithAttributes(
$tokens,
new IdentifierTypeNode($currentTokenValue),
$startLine,
$startIndex
);
}
$tokens->rollback();
$constExpr = $this->constantExprParser->parse($tokens, true);
if (!$constExpr instanceof Ast\ConstExpr\ConstFetchNode) {
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_IDENTIFIER,
null,
$tokens->currentTokenLine()
);
}
return $constExpr;
}
return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
}
/**
* @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode
*/

View File

@@ -9,6 +9,7 @@ use function assert;
use function count;
use function in_array;
use function strlen;
use function substr;
class TokenIterator
{
@@ -22,6 +23,12 @@ class TokenIterator
/** @var int[] */
private $savePoints = [];
/** @var list<int> */
private $skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
/** @var string|null */
private $newline = null;
/**
* @param list<array{string, int, int}> $tokens
*/
@@ -30,11 +37,7 @@ class TokenIterator
$this->tokens = $tokens;
$this->index = $index;
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== Lexer::TOKEN_HORIZONTAL_WS) {
return;
}
$this->index++;
$this->skipIrrelevantTokens();
}
@@ -103,6 +106,21 @@ class TokenIterator
}
public function endIndexOfLastRelevantToken(): int
{
$endIndex = $this->currentTokenIndex();
$endIndex--;
while (in_array($this->tokens[$endIndex][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
if (!isset($this->tokens[$endIndex - 1])) {
break;
}
$endIndex--;
}
return $endIndex;
}
public function isCurrentTokenValue(string $tokenValue): bool
{
return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue;
@@ -130,13 +148,14 @@ class TokenIterator
$this->throwError($tokenType);
}
$this->index++;
if (($this->tokens[$this->index][Lexer::TYPE_OFFSET] ?? -1) !== Lexer::TOKEN_HORIZONTAL_WS) {
return;
if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
if ($this->newline === null) {
$this->detectNewline();
}
}
$this->index++;
$this->skipIrrelevantTokens();
}
@@ -150,12 +169,7 @@ class TokenIterator
}
$this->index++;
if (($this->tokens[$this->index][Lexer::TYPE_OFFSET] ?? -1) !== Lexer::TOKEN_HORIZONTAL_WS) {
return;
}
$this->index++;
$this->skipIrrelevantTokens();
}
@@ -167,10 +181,7 @@ class TokenIterator
}
$this->index++;
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
$this->index++;
}
$this->skipIrrelevantTokens();
return true;
}
@@ -183,16 +194,30 @@ class TokenIterator
return false;
}
$this->index++;
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
$this->index++;
if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) {
if ($this->newline === null) {
$this->detectNewline();
}
}
$this->index++;
$this->skipIrrelevantTokens();
return true;
}
private function detectNewline(): void
{
$value = $this->currentTokenValue();
if (substr($value, 0, 2) === "\r\n") {
$this->newline = "\r\n";
} elseif (substr($value, 0, 1) === "\n") {
$this->newline = "\n";
}
}
public function getSkippedHorizontalWhiteSpaceIfAny(): string
{
if ($this->index > 0 && $this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) {
@@ -217,12 +242,34 @@ class TokenIterator
public function next(): void
{
$this->index++;
$this->skipIrrelevantTokens();
}
if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== Lexer::TOKEN_HORIZONTAL_WS) {
private function skipIrrelevantTokens(): void
{
if (!isset($this->tokens[$this->index])) {
return;
}
$this->index++;
while (in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) {
if (!isset($this->tokens[$this->index + 1])) {
break;
}
$this->index++;
}
}
public function addEndOfLineToSkippedTokens(): void
{
$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL];
}
public function removeEndOfLineFromSkippedTokens(): void
{
$this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS];
}
/** @phpstan-impure */
@@ -319,6 +366,11 @@ class TokenIterator
return false;
}
public function getDetectedNewline(): ?string
{
return $this->newline;
}
/**
* Whether the given position is immediately surrounded by parenthesis.
*/

View File

@@ -70,23 +70,14 @@ class TypeParser
*/
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);
$type->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
}
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);
$type->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
}
return $type;