From 2e17e4a90702d8b7ead58f4e08478a8e819ba6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Sat, 27 Feb 2021 14:47:23 +0100 Subject: [PATCH] type aliases: support @phpstan-import-type and @psalm-import-type tags --- src/Ast/PhpDoc/PhpDocNode.php | 14 +++ .../PhpDoc/TypeAliasImportTagValueNode.php | 34 +++++++ src/Parser/PhpDocParser.php | 31 ++++++ tests/PHPStan/Parser/PhpDocParserTest.php | 96 ++++++++++++++++++- 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 src/Ast/PhpDoc/TypeAliasImportTagValueNode.php diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index d0f2829..ace03b1 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -252,6 +252,20 @@ class PhpDocNode implements Node } + /** + * @return TypeAliasImportTagValueNode[] + */ + public function getTypeAliasImportTagValues(string $tagName = '@phpstan-import-type'): array + { + return array_column( + array_filter($this->getTagsByName($tagName), static function (PhpDocTagNode $tag): bool { + return $tag->value instanceof TypeAliasImportTagValueNode; + }), + 'value' + ); + } + + public function __toString(): string { return "/**\n * " . implode("\n * ", $this->children) . '*/'; diff --git a/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php b/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php new file mode 100644 index 0000000..923eb62 --- /dev/null +++ b/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php @@ -0,0 +1,34 @@ +importedAlias = $importedAlias; + $this->importedFrom = $importedFrom; + $this->importedAs = $importedAs; + } + + public function __toString(): string + { + return trim( + "{$this->importedAlias} from {$this->importedFrom}" + . ($this->importedAs !== null ? " as {$this->importedAs}" : '') + ); + } + +} diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 158b633..a22b51a 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -194,6 +194,11 @@ class PhpDocParser $tagValue = $this->parseTypeAliasTagValue($tokens); break; + case '@phpstan-import-type': + case '@psalm-import-type': + $tagValue = $this->parseTypeAliasImportTagValue($tokens); + break; + default: $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens)); break; @@ -382,6 +387,32 @@ class PhpDocParser return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type); } + private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode + { + $importedAlias = $tokens->currentTokenValue(); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + + if (!$tokens->tryConsumeTokenValue('from')) { + throw new \PHPStan\PhpDocParser\Parser\ParserException( + $tokens->currentTokenValue(), + $tokens->currentTokenType(), + $tokens->currentTokenOffset(), + Lexer::TOKEN_IDENTIFIER + ); + } + + $importedFrom = $this->typeParser->parse($tokens); + assert($importedFrom instanceof IdentifierTypeNode); + + $importedAs = null; + if ($tokens->tryConsumeTokenValue('as')) { + $importedAs = $tokens->currentTokenValue(); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + } + + return new Ast\PhpDoc\TypeAliasImportTagValueNode($importedAlias, $importedFrom, $importedAs); + } + private function parseOptionalVariableName(TokenIterator $tokens): string { if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 61c498a..920a41f 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -23,6 +23,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; @@ -68,6 +69,7 @@ class PhpDocParserTest extends \PHPUnit\Framework\TestCase * @dataProvider provideTemplateTagsData * @dataProvider provideExtendsTagsData * @dataProvider provideTypeAliasTagsData + * @dataProvider provideTypeAliasImportTagsData * @dataProvider provideRealWorldExampleData * @dataProvider provideDescriptionWithOrWithoutHtml * @param string $label @@ -2904,7 +2906,7 @@ some text in the middle' '@phpstan-type', new InvalidTagValueNode( 'TypeAlias', - new ParserException( + new \PHPStan\PhpDocParser\Parser\ParserException( '*/', Lexer::TOKEN_CLOSE_PHPDOC, 28, @@ -2923,7 +2925,7 @@ some text in the middle' '@phpstan-type', new InvalidTagValueNode( '', - new ParserException( + new \PHPStan\PhpDocParser\Parser\ParserException( '*/', Lexer::TOKEN_CLOSE_PHPDOC, 18, @@ -2935,6 +2937,96 @@ some text in the middle' ]; } + public function provideTypeAliasImportTagsData(): \Iterator + { + yield [ + 'OK', + '/** @phpstan-import-type TypeAlias from AnotherClass */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-import-type', + new TypeAliasImportTagValueNode( + 'TypeAlias', + new IdentifierTypeNode('AnotherClass'), + null + ) + ), + ]), + ]; + + yield [ + 'OK with alias', + '/** @phpstan-import-type TypeAlias from AnotherClass as DifferentAlias */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-import-type', + new TypeAliasImportTagValueNode( + 'TypeAlias', + new IdentifierTypeNode('AnotherClass'), + 'DifferentAlias' + ) + ), + ]), + ]; + + yield [ + 'invalid missing from', + '/** @phpstan-import-type TypeAlias */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-import-type', + new InvalidTagValueNode( + 'TypeAlias', + new \PHPStan\PhpDocParser\Parser\ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 35, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + ]; + + yield [ + 'invalid missing from with alias', + '/** @phpstan-import-type TypeAlias as DifferentAlias */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-import-type', + new InvalidTagValueNode( + 'TypeAlias as DifferentAlias', + new \PHPStan\PhpDocParser\Parser\ParserException( + 'as', + Lexer::TOKEN_IDENTIFIER, + 35, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + ]; + + yield [ + 'invalid empty', + '/** @phpstan-import-type */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-import-type', + new InvalidTagValueNode( + '', + new \PHPStan\PhpDocParser\Parser\ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 25, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + ]; + } + public function providerDebug(): \Iterator { $sample = '/**