From ef1a39aca0cdb0a96f92332e3795b27934144511 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 21 May 2019 15:22:01 +0200 Subject: [PATCH] Support @template tag --- src/Ast/PhpDoc/PhpDocNode.php | 14 +++ src/Ast/PhpDoc/TemplateTagValueNode.php | 32 +++++++ src/Parser/PhpDocParser.php | 21 +++++ tests/PHPStan/Parser/PhpDocParserTest.php | 104 ++++++++++++++++++++++ 4 files changed, 171 insertions(+) create mode 100644 src/Ast/PhpDoc/TemplateTagValueNode.php diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 3733a50f..a7063119 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -70,6 +70,20 @@ public function getParamTagValues(): array } + /** + * @return TemplateTagValueNode[] + */ + public function getTemplateTagValues(): array + { + return array_column( + array_filter($this->getTagsByName('@template'), static function (PhpDocTagNode $tag): bool { + return $tag->value instanceof TemplateTagValueNode; + }), + 'value' + ); + } + + /** * @return ReturnTagValueNode[] */ diff --git a/src/Ast/PhpDoc/TemplateTagValueNode.php b/src/Ast/PhpDoc/TemplateTagValueNode.php new file mode 100644 index 00000000..4030e884 --- /dev/null +++ b/src/Ast/PhpDoc/TemplateTagValueNode.php @@ -0,0 +1,32 @@ +name = $name; + $this->bound = $bound; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->name} of {$this->bound} {$this->description}"); + } + +} diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 67e4cce3..a0efa34f 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Parser; use PHPStan\PhpDocParser\Ast; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; class PhpDocParser @@ -113,6 +114,10 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseMethodTagValue($tokens); break; + case '@template': + $tagValue = $this->parseTemplateTagValue($tokens); + break; + default: $tagValue = new Ast\PhpDoc\GenericTagValueNode($this->parseOptionalDescription($tokens)); break; @@ -243,6 +248,22 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue); } + private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode + { + $name = $tokens->currentTokenValue(); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + + if ($tokens->tryConsumeTokenValue('of')) { + $bound = $this->typeParser->parse($tokens); + + } else { + $bound = new IdentifierTypeNode('mixed'); + } + + $description = $this->parseOptionalDescription($tokens); + + return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description); + } private function parseOptionalVariableName(TokenIterator $tokens): string { diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 334ed32c..dd9f3aff 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -15,6 +15,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; 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\VarTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -49,6 +50,7 @@ protected function setUp(): void * @dataProvider provideMethodTagsData * @dataProvider provideSingleLinePhpDocData * @dataProvider provideMultiLinePhpDocData + * @dataProvider provideTemplateTagsData * @param string $label * @param string $input * @param PhpDocNode $expectedPhpDocNode @@ -2198,4 +2200,106 @@ public function provideMultiLinePhpDocData(): array ]; } + + public function provideTemplateTagsData(): \Iterator + { + yield [ + 'OK without bound and description', + '/** @template T */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('mixed'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK without bound', + '/** @template T the value type*/', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('mixed'), + 'the value type' + ) + ), + ]), + ]; + + yield [ + 'OK without description', + '/** @template T of DateTime */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('DateTime'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with bound and description', + '/** @template T of DateTime the value type */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('DateTime'), + 'the value type' + ) + ), + ]), + ]; + + yield [ + 'invalid without bound and description', + '/** @template */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new InvalidTagValueNode( + '', + new \PHPStan\PhpDocParser\Parser\ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 14, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + ]; + + yield [ + 'invalid without bound and with description', + '/** @template #desc */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new InvalidTagValueNode( + '#desc', + new \PHPStan\PhpDocParser\Parser\ParserException( + '#desc', + Lexer::TOKEN_OTHER, + 14, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + ]; + } + }