Support @extends, @implements, @uses

This commit is contained in:
Arnaud Le Blanc 2019-06-07 18:35:22 +02:00 committed by Ondřej Mirtes
parent 8c4ef2aefd
commit 14346cb8b0
7 changed files with 294 additions and 1 deletions

View File

@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
class ExtendsTagValueNode implements PhpDocTagValueNode
{
/** @var GenericTypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
class ImplementsTagValueNode implements PhpDocTagValueNode
{
/** @var GenericTypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -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[]
*/

View File

@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\PhpDoc;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
class UsesTagValueNode implements PhpDocTagValueNode
{
/** @var GenericTypeNode */
public $type;
/** @var string (may be empty) */
public $description;
public function __construct(GenericTypeNode $type, string $description)
{
$this->type = $type;
$this->description = $description;
}
public function __toString(): string
{
return trim("{$this->type} {$this->description}");
}
}

View File

@ -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)) {

View File

@ -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);

View File

@ -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<A> */',
new PhpDocNode([
new PhpDocTagNode(
'@extends',
new ExtendsTagValueNode(
new GenericTypeNode(
new IdentifierTypeNode('Foo'),
[
new IdentifierTypeNode('A'),
]
),
''
)
),
]),
];
yield [
'OK with two arguments',
'/** @extends Foo<A,B> */',
new PhpDocNode([
new PhpDocTagNode(
'@extends',
new ExtendsTagValueNode(
new GenericTypeNode(
new IdentifierTypeNode('Foo'),
[
new IdentifierTypeNode('A'),
new IdentifierTypeNode('B'),
]
),
''
)
),
]),
];
yield [
'OK @implements',
'/** @implements Foo<A,B> */',
new PhpDocNode([
new PhpDocTagNode(
'@implements',
new ImplementsTagValueNode(
new GenericTypeNode(
new IdentifierTypeNode('Foo'),
[
new IdentifierTypeNode('A'),
new IdentifierTypeNode('B'),
]
),
''
)
),
]),
];
yield [
'OK @uses',
'/** @uses Foo<A,B> */',
new PhpDocNode([
new PhpDocTagNode(
'@uses',
new UsesTagValueNode(
new GenericTypeNode(
new IdentifierTypeNode('Foo'),
[
new IdentifierTypeNode('A'),
new IdentifierTypeNode('B'),
]
),
''
)
),
]),
];
yield [
'OK with description',
'/** @extends Foo<A> 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 = '/**