From 2a4686e97458586321f8ffa133aaa967552b7200 Mon Sep 17 00:00:00 2001 From: Tomohito YABU Date: Thu, 8 Dec 2022 01:09:23 +0900 Subject: [PATCH] Add generics support to `@method` definitions --- src/Ast/PhpDoc/MethodTagValueNode.php | 10 +- src/Parser/PhpDocParser.php | 21 +++- tests/PHPStan/Parser/PhpDocParserTest.php | 133 ++++++++++++++++++++++ 3 files changed, 157 insertions(+), 7 deletions(-) diff --git a/src/Ast/PhpDoc/MethodTagValueNode.php b/src/Ast/PhpDoc/MethodTagValueNode.php index 155897bb..075cec04 100644 --- a/src/Ast/PhpDoc/MethodTagValueNode.php +++ b/src/Ast/PhpDoc/MethodTagValueNode.php @@ -4,6 +4,7 @@ use PHPStan\PhpDocParser\Ast\NodeAttributes; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use function count; use function implode; class MethodTagValueNode implements PhpDocTagValueNode @@ -20,19 +21,23 @@ class MethodTagValueNode implements PhpDocTagValueNode /** @var string */ public $methodName; + /** @var TemplateTagValueNode[] */ + public $templateTypes; + /** @var MethodTagValueParameterNode[] */ public $parameters; /** @var string (may be empty) */ public $description; - public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description) + public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description, array $templateTypes = []) { $this->isStatic = $isStatic; $this->returnType = $returnType; $this->methodName = $methodName; $this->parameters = $parameters; $this->description = $description; + $this->templateTypes = $templateTypes; } @@ -42,7 +47,8 @@ public function __toString(): string $returnType = $this->returnType !== null ? "{$this->returnType} " : ''; $parameters = implode(', ', $this->parameters); $description = $this->description !== '' ? " {$this->description}" : ''; - return "{$static}{$returnType}{$this->methodName}({$parameters}){$description}"; + $templateTypes = count($this->templateTypes) > 0 ? '<' . implode(', ', $this->templateTypes) . '>' : ''; + return "{$static}{$returnType}{$this->methodName}{$templateTypes}({$parameters}){$description}"; } } diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 9badbe61..d9942b3d 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -182,7 +182,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph case '@template-contravariant': case '@phpstan-template-contravariant': case '@psalm-template-contravariant': - $tagValue = $this->parseTemplateTagValue($tokens); + $tagValue = $this->parseTemplateTagValue($tokens, true); break; case '@extends': @@ -346,6 +346,14 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa exit; } + $templateTypes = []; + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { + do { + $templateTypes[] = $this->parseTemplateTagValue($tokens, false); + } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + } + $parameters = []; $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { @@ -357,10 +365,9 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); $description = $this->parseOptionalDescription($tokens); - return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description); + return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes); } - private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode { switch ($tokens->currentTokenType()) { @@ -390,7 +397,7 @@ 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 + private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode { $name = $tokens->currentTokenValue(); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); @@ -408,7 +415,11 @@ private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\Templa $default = null; } - $description = $this->parseOptionalDescription($tokens); + if ($parseDescription) { + $description = $this->parseOptionalDescription($tokens); + } else { + $description = ''; + } return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index d7a45c4a..0672e3b7 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDocParser\Parser; use Iterator; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; @@ -44,6 +45,7 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -2201,6 +2203,98 @@ public function provideMethodTagsData(): Iterator ), ]), ]; + + yield [ + 'OK non-static, with return type and parameter with generic type', + '/** @method ?T randomElement(array $array = [\'a\', \'b\']) */', + new PhpDocNode([ + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + false, + new NullableTypeNode(new IdentifierTypeNode('T')), + 'randomElement', + [ + new MethodTagValueParameterNode( + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('array-key'), + new IdentifierTypeNode('T'), + ] + ), + false, + false, + '$array', + new ConstExprArrayNode([ + new ConstExprArrayItemNode( + null, + new ConstExprStringNode('\'a\'') + ), + new ConstExprArrayItemNode( + null, + new ConstExprStringNode('\'b\'') + ), + ]) + ), + ], + '', + [ + new TemplateTagValueNode( + 'T', + null, + '', + new IdentifierTypeNode('string') + ), + ] + ) + ), + ]), + ]; + + yield [ + 'OK static, with return type and multiple parameters with generic type', + '/** @method static bool compare(T1 $t1, T2 $t2, T3 $t3) */', + new PhpDocNode([ + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + true, + new IdentifierTypeNode('bool'), + 'compare', + [ + new MethodTagValueParameterNode( + new IdentifierTypeNode('T1'), + false, + false, + '$t1', + null + ), + new MethodTagValueParameterNode( + new IdentifierTypeNode('T2'), + false, + false, + '$t2', + null + ), + new MethodTagValueParameterNode( + new IdentifierTypeNode('T3'), + false, + false, + '$t3', + null + ), + ], + '', + [ + new TemplateTagValueNode('T1', null, ''), + new TemplateTagValueNode('T2', new IdentifierTypeNode('Bar'), ''), + new TemplateTagValueNode('T3', new IdentifierTypeNode('Baz'), ''), + ] + ) + ), + ]), + ]; } @@ -3078,6 +3172,45 @@ public function provideMultiLinePhpDocData(): array ), ]), ], + [ + 'OK with template method', + '/** + * @template TKey as array-key + * @template TValue + * @method TKey|null find(TValue $v) find index of $v + */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode('TKey', new IdentifierTypeNode('array-key'), '') + ), + new PhpDocTagNode( + '@template', + new TemplateTagValueNode('TValue', null, '') + ), + new PhpDocTagNode( + '@method', + new MethodTagValueNode( + false, + new UnionTypeNode([ + new IdentifierTypeNode('TKey'), + new IdentifierTypeNode('null'), + ]), + 'find', + [ + new MethodTagValueParameterNode( + new IdentifierTypeNode('TValue'), + false, + false, + '$v', + null + ), + ], + 'find index of $v' + ) + ), + ]), + ], [ 'OK with multiline conditional return type', '/**