diff --git a/src/Ast/PhpDoc/ExtendsTagValueNode.php b/src/Ast/PhpDoc/ExtendsTagValueNode.php new file mode 100644 index 0000000..513f297 --- /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 0000000..7691d93 --- /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 a706311..8a3b513 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -84,6 +84,48 @@ class PhpDocNode implements Node } + /** + * @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 0000000..20b1939 --- /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 ae93095..950fefe 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -145,6 +145,12 @@ class PhpDocParser $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 @@ class PhpDocParser 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 5834517..d42e067 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -105,7 +105,7 @@ class TypeParser } - 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 8c6c951..b29a49c 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -5,7 +5,9 @@ namespace PHPStan\PhpDocParser\Parser; 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\PropertyTagValueNode; 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 @@ class PhpDocParserTest extends \PHPUnit\Framework\TestCase * @dataProvider provideSingleLinePhpDocData * @dataProvider provideMultiLinePhpDocData * @dataProvider provideTemplateTagsData + * @dataProvider provideExtendsTagsData * @dataProvider provideRealWorldExampleData * @param string $label * @param string $input @@ -2368,6 +2373,143 @@ some text in the middle' ]; } + 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 = '/**