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 = '/**