diff --git a/src/Parser/ConstExprParser.php b/src/Parser/ConstExprParser.php index c4767ee8..f8cfcb59 100644 --- a/src/Parser/ConstExprParser.php +++ b/src/Parser/ConstExprParser.php @@ -16,24 +16,53 @@ class ConstExprParser /** @var bool */ private $quoteAwareConstExprString; - public function __construct(bool $unescapeStrings = false, bool $quoteAwareConstExprString = false) + /** @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($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($value), + $startLine, + $startIndex + ); } if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { @@ -49,15 +78,25 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con $tokens->next(); if ($this->quoteAwareConstExprString) { - return new Ast\ConstExpr\QuoteAwareConstExprStringNode( - $value, - $type === Lexer::TOKEN_SINGLE_QUOTED_STRING - ? Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED - : Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED + 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 new Ast\ConstExpr\ConstExprStringNode($value); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprStringNode($value), + $startLine, + $startIndex + ); } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { $identifier = $tokens->currentTokenValue(); @@ -65,11 +104,26 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con 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); @@ -106,11 +160,21 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con 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); @@ -131,6 +195,9 @@ private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr { $items = []; + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + if (!$tokens->tryConsumeTokenType($endToken)) { do { $items[] = $this->parseArrayItem($tokens); @@ -138,12 +205,20 @@ private function parseArray(TokenIterator $tokens, int $endToken): Ast\ConstExpr $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)) { @@ -155,7 +230,40 @@ private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprA $value = $expr; } - return new Ast\ConstExpr\ConstExprArrayItemNode($key, $value); + return $this->enrichWithAttributes( + $tokens, + new Ast\ConstExpr\ConstExprArrayItemNode($key, $value), + $startLine, + $startIndex + ); + } + + /** + * @template T of Ast\ConstExpr\ConstExprNode + * @param T $node + * @return T + */ + 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); + } + + 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; } } diff --git a/tests/PHPStan/Parser/ConstExprParserTest.php b/tests/PHPStan/Parser/ConstExprParserTest.php index 7483a800..1fac87d9 100644 --- a/tests/PHPStan/Parser/ConstExprParserTest.php +++ b/tests/PHPStan/Parser/ConstExprParserTest.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Parser; use Iterator; +use PHPStan\PhpDocParser\Ast\Attribute; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; @@ -13,6 +14,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\NodeTraverser; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPUnit\Framework\TestCase; @@ -54,6 +56,38 @@ public function testParse(string $input, ConstExprNode $expectedExpr, int $nextT } + /** + * @dataProvider provideTrueNodeParseData + * @dataProvider provideFalseNodeParseData + * @dataProvider provideNullNodeParseData + * @dataProvider provideIntegerNodeParseData + * @dataProvider provideFloatNodeParseData + * @dataProvider provideStringNodeParseData + * @dataProvider provideArrayNodeParseData + * @dataProvider provideFetchNodeParseData + * + * @dataProvider provideWithTrimStringsStringNodeParseData + */ + public function testVerifyAttributes(string $input): void + { + $tokens = new TokenIterator($this->lexer->tokenize($input)); + $constExprParser = new ConstExprParser(true, true, [ + 'lines' => true, + 'indexes' => true, + ]); + $visitor = new NodeCollectingVisitor(); + $traverser = new NodeTraverser([$visitor]); + $traverser->traverse([$constExprParser->parse($tokens)]); + + foreach ($visitor->nodes as $node) { + $this->assertNotNull($node->getAttribute(Attribute::START_LINE), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::END_LINE), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::START_INDEX), (string) $node); + $this->assertNotNull($node->getAttribute(Attribute::END_INDEX), (string) $node); + } + } + + public function provideTrueNodeParseData(): Iterator { yield [ diff --git a/tests/PHPStan/Parser/NodeCollectingVisitor.php b/tests/PHPStan/Parser/NodeCollectingVisitor.php new file mode 100644 index 00000000..aa01e559 --- /dev/null +++ b/tests/PHPStan/Parser/NodeCollectingVisitor.php @@ -0,0 +1,21 @@ + */ + public $nodes = []; + + public function enterNode(Node $node) + { + $this->nodes[] = $node; + + return null; + } + +} diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 6d089d56..41dd549f 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5620,11 +5620,11 @@ public function dataLinesAndIndexes(): iterable public function testLinesAndIndexes(string $phpDoc, array $childrenLines): void { $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); - $constExprParser = new ConstExprParser(true, true); $usedAttributes = [ 'lines' => true, 'indexes' => true, ]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); $typeParser = new TypeParser($constExprParser, true, $usedAttributes); $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); $phpDocNode = $phpDocParser->parse($tokens); @@ -5691,11 +5691,11 @@ public function dataReturnTypeLinesAndIndexes(): iterable public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): void { $tokens = new TokenIterator($this->lexer->tokenize($phpDoc)); - $constExprParser = new ConstExprParser(true, true); $usedAttributes = [ 'lines' => true, 'indexes' => true, ]; + $constExprParser = new ConstExprParser(true, true, $usedAttributes); $typeParser = new TypeParser($constExprParser, true, $usedAttributes); $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); $phpDocNode = $phpDocParser->parse($tokens); diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 9c876048..bc16e658 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -2179,10 +2179,11 @@ public function testLinesAndIndexes(string $input, array $assertions): void { $tokensArray = $this->lexer->tokenize($input); $tokens = new TokenIterator($tokensArray); - $typeParser = new TypeParser(new ConstExprParser(true, true), true, [ + $usedAttributes = [ 'lines' => true, 'indexes' => true, - ]); + ]; + $typeParser = new TypeParser(new ConstExprParser(true, true), true, $usedAttributes); $typeNode = $typeParser->parse($tokens); foreach ($assertions as [$callable, $expectedContent, $startLine, $endLine, $startIndex, $endIndex]) {