From 00b301eba0739f33561115a83fa684054ebe7c0a Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 7 Jun 2019 18:35:22 +0200 Subject: [PATCH] Support @extends, @implements, @uses --- src/Ast/PhpDoc/ExtendsTagValueNode.php | 28 +++++ src/Ast/PhpDoc/ImplementsTagValueNode.php | 28 +++++ src/Ast/PhpDoc/PhpDocNode.php | 42 +++++++ src/Ast/PhpDoc/UsesTagValueNode.php | 28 +++++ src/Parser/PhpDocParser.php | 25 ++++ src/Parser/TypeParser.php | 2 +- tests/PHPStan/Parser/PhpDocParserTest.php | 142 ++++++++++++++++++++++ 7 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 src/Ast/PhpDoc/ExtendsTagValueNode.php create mode 100644 src/Ast/PhpDoc/ImplementsTagValueNode.php create mode 100644 src/Ast/PhpDoc/UsesTagValueNode.php diff --git a/src/Ast/PhpDoc/ExtendsTagValueNode.php b/src/Ast/PhpDoc/ExtendsTagValueNode.php new file mode 100644 index 00000000..513f2975 --- /dev/null +++ b/src/Ast/PhpDoc/ExtendsTagValueNode.php @@ -0,0 +1,28 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/ImplementsTagValueNode.php b/src/Ast/PhpDoc/ImplementsTagValueNode.php new file mode 100644 index 00000000..7691d93a --- /dev/null +++ b/src/Ast/PhpDoc/ImplementsTagValueNode.php @@ -0,0 +1,28 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index a7063119..8a3b5139 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -84,6 +84,48 @@ public function getTemplateTagValues(): array } + /** + * @return ExtendsTagValueNode[] + */ + public function getExtendsTagValues(): array + { + return array_column( + array_filter($this->getTagsByName('@extends'), static function (PhpDocTagNode $tag): bool { + return $tag->value instanceof ExtendsTagValueNode; + }), + 'value' + ); + } + + + /** + * @return ImplementsTagValueNode[] + */ + public function getImplementsTagValues(): array + { + return array_column( + array_filter($this->getTagsByName('@implements'), static function (PhpDocTagNode $tag): bool { + return $tag->value instanceof ImplementsTagValueNode; + }), + 'value' + ); + } + + + /** + * @return UsesTagValueNode[] + */ + public function getUsesTagValues(): array + { + return array_column( + array_filter($this->getTagsByName('@uses'), static function (PhpDocTagNode $tag): bool { + return $tag->value instanceof UsesTagValueNode; + }), + 'value' + ); + } + + /** * @return ReturnTagValueNode[] */ diff --git a/src/Ast/PhpDoc/UsesTagValueNode.php b/src/Ast/PhpDoc/UsesTagValueNode.php new file mode 100644 index 00000000..20b19390 --- /dev/null +++ b/src/Ast/PhpDoc/UsesTagValueNode.php @@ -0,0 +1,28 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index ae930953..950fefe8 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -145,6 +145,12 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseTemplateTagValue($tokens); break; + case '@extends': + case '@implements': + case '@uses': + $tagValue = $this->parseExtendsTagValue($tag, $tokens); + break; + default: $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens)); break; @@ -292,6 +298,25 @@ private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\Templa return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description); } + private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode + { + $baseType = new IdentifierTypeNode($tokens->currentTokenValue()); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + + $type = $this->typeParser->parseGeneric($tokens, $baseType); + + $description = $this->parseOptionalDescription($tokens); + + switch ($tagName) { + case '@extends': + return new Ast\PhpDoc\ExtendsTagValueNode($type, $description); + case '@implements': + return new Ast\PhpDoc\ImplementsTagValueNode($type, $description); + case '@uses': + return new Ast\PhpDoc\UsesTagValueNode($type, $description); + } + } + private function parseOptionalVariableName(TokenIterator $tokens): string { if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 58345175..d42e067a 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -105,7 +105,7 @@ private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode } - private function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\TypeNode + public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); $genericTypes[] = $this->parse($tokens); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 8c6c951e..b29a49c8 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5,7 +5,9 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\PhpDoc\DeprecatedTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; @@ -17,8 +19,10 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -51,6 +55,7 @@ protected function setUp(): void * @dataProvider provideSingleLinePhpDocData * @dataProvider provideMultiLinePhpDocData * @dataProvider provideTemplateTagsData + * @dataProvider provideExtendsTagsData * @dataProvider provideRealWorldExampleData * @param string $label * @param string $input @@ -2368,6 +2373,143 @@ public function provideTemplateTagsData(): \Iterator ]; } + public function provideExtendsTagsData(): \Iterator + { + yield [ + 'OK with one argument', + '/** @extends Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@extends', + new ExtendsTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [ + new IdentifierTypeNode('A'), + ] + ), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with two arguments', + '/** @extends Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@extends', + new ExtendsTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [ + new IdentifierTypeNode('A'), + new IdentifierTypeNode('B'), + ] + ), + '' + ) + ), + ]), + ]; + + yield [ + 'OK @implements', + '/** @implements Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@implements', + new ImplementsTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [ + new IdentifierTypeNode('A'), + new IdentifierTypeNode('B'), + ] + ), + '' + ) + ), + ]), + ]; + + yield [ + 'OK @uses', + '/** @uses Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@uses', + new UsesTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [ + new IdentifierTypeNode('A'), + new IdentifierTypeNode('B'), + ] + ), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @extends Foo extends foo*/', + new PhpDocNode([ + new PhpDocTagNode( + '@extends', + new ExtendsTagValueNode( + new GenericTypeNode( + new IdentifierTypeNode('Foo'), + [new IdentifierTypeNode('A')] + ), + 'extends foo' + ) + ), + ]), + ]; + + yield [ + 'invalid without type', + '/** @extends */', + new PhpDocNode([ + new PhpDocTagNode( + '@extends', + new InvalidTagValueNode( + '', + new \PHPStan\PhpDocParser\Parser\ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 13, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + ]; + + yield [ + 'invalid without arguments', + '/** @extends Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@extends', + new InvalidTagValueNode( + 'Foo', + new \PHPStan\PhpDocParser\Parser\ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 17, + Lexer::TOKEN_OPEN_ANGLE_BRACKET + ) + ) + ), + ]), + ]; + } + public function providerDebug(): \Iterator { $sample = '/**