Skip to content

Commit 2a4686e

Browse files
authored
Add generics support to @method definitions
1 parent c7c2609 commit 2a4686e

File tree

3 files changed

+157
-7
lines changed

3 files changed

+157
-7
lines changed

src/Ast/PhpDoc/MethodTagValueNode.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\PhpDocParser\Ast\NodeAttributes;
66
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
7+
use function count;
78
use function implode;
89

910
class MethodTagValueNode implements PhpDocTagValueNode
@@ -20,19 +21,23 @@ class MethodTagValueNode implements PhpDocTagValueNode
2021
/** @var string */
2122
public $methodName;
2223

24+
/** @var TemplateTagValueNode[] */
25+
public $templateTypes;
26+
2327
/** @var MethodTagValueParameterNode[] */
2428
public $parameters;
2529

2630
/** @var string (may be empty) */
2731
public $description;
2832

29-
public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description)
33+
public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description, array $templateTypes = [])
3034
{
3135
$this->isStatic = $isStatic;
3236
$this->returnType = $returnType;
3337
$this->methodName = $methodName;
3438
$this->parameters = $parameters;
3539
$this->description = $description;
40+
$this->templateTypes = $templateTypes;
3641
}
3742

3843

@@ -42,7 +47,8 @@ public function __toString(): string
4247
$returnType = $this->returnType !== null ? "{$this->returnType} " : '';
4348
$parameters = implode(', ', $this->parameters);
4449
$description = $this->description !== '' ? " {$this->description}" : '';
45-
return "{$static}{$returnType}{$this->methodName}({$parameters}){$description}";
50+
$templateTypes = count($this->templateTypes) > 0 ? '<' . implode(', ', $this->templateTypes) . '>' : '';
51+
return "{$static}{$returnType}{$this->methodName}{$templateTypes}({$parameters}){$description}";
4652
}
4753

4854
}

src/Parser/PhpDocParser.php

+16-5
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph
182182
case '@template-contravariant':
183183
case '@phpstan-template-contravariant':
184184
case '@psalm-template-contravariant':
185-
$tagValue = $this->parseTemplateTagValue($tokens);
185+
$tagValue = $this->parseTemplateTagValue($tokens, true);
186186
break;
187187

188188
case '@extends':
@@ -346,6 +346,14 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa
346346
exit;
347347
}
348348

349+
$templateTypes = [];
350+
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
351+
do {
352+
$templateTypes[] = $this->parseTemplateTagValue($tokens, false);
353+
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
354+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
355+
}
356+
349357
$parameters = [];
350358
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
351359
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
@@ -357,10 +365,9 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa
357365
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
358366

359367
$description = $this->parseOptionalDescription($tokens);
360-
return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description);
368+
return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes);
361369
}
362370

363-
364371
private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
365372
{
366373
switch ($tokens->currentTokenType()) {
@@ -390,7 +397,7 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc
390397
return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue);
391398
}
392399

393-
private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode
400+
private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode
394401
{
395402
$name = $tokens->currentTokenValue();
396403
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
@@ -408,7 +415,11 @@ private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\Templa
408415
$default = null;
409416
}
410417

411-
$description = $this->parseOptionalDescription($tokens);
418+
if ($parseDescription) {
419+
$description = $this->parseOptionalDescription($tokens);
420+
} else {
421+
$description = '';
422+
}
412423

413424
return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
414425
}

tests/PHPStan/Parser/PhpDocParserTest.php

+133
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\PhpDocParser\Parser;
44

55
use Iterator;
6+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode;
67
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
78
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
89
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
@@ -44,6 +45,7 @@
4445
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
4546
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
4647
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
48+
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
4749
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
4850
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
4951
use PHPStan\PhpDocParser\Lexer\Lexer;
@@ -2201,6 +2203,98 @@ public function provideMethodTagsData(): Iterator
22012203
),
22022204
]),
22032205
];
2206+
2207+
yield [
2208+
'OK non-static, with return type and parameter with generic type',
2209+
'/** @method ?T randomElement<T = string>(array<array-key, T> $array = [\'a\', \'b\']) */',
2210+
new PhpDocNode([
2211+
new PhpDocTagNode(
2212+
'@method',
2213+
new MethodTagValueNode(
2214+
false,
2215+
new NullableTypeNode(new IdentifierTypeNode('T')),
2216+
'randomElement',
2217+
[
2218+
new MethodTagValueParameterNode(
2219+
new GenericTypeNode(
2220+
new IdentifierTypeNode('array'),
2221+
[
2222+
new IdentifierTypeNode('array-key'),
2223+
new IdentifierTypeNode('T'),
2224+
]
2225+
),
2226+
false,
2227+
false,
2228+
'$array',
2229+
new ConstExprArrayNode([
2230+
new ConstExprArrayItemNode(
2231+
null,
2232+
new ConstExprStringNode('\'a\'')
2233+
),
2234+
new ConstExprArrayItemNode(
2235+
null,
2236+
new ConstExprStringNode('\'b\'')
2237+
),
2238+
])
2239+
),
2240+
],
2241+
'',
2242+
[
2243+
new TemplateTagValueNode(
2244+
'T',
2245+
null,
2246+
'',
2247+
new IdentifierTypeNode('string')
2248+
),
2249+
]
2250+
)
2251+
),
2252+
]),
2253+
];
2254+
2255+
yield [
2256+
'OK static, with return type and multiple parameters with generic type',
2257+
'/** @method static bool compare<T1, T2 of Bar, T3 as Baz>(T1 $t1, T2 $t2, T3 $t3) */',
2258+
new PhpDocNode([
2259+
new PhpDocTagNode(
2260+
'@method',
2261+
new MethodTagValueNode(
2262+
true,
2263+
new IdentifierTypeNode('bool'),
2264+
'compare',
2265+
[
2266+
new MethodTagValueParameterNode(
2267+
new IdentifierTypeNode('T1'),
2268+
false,
2269+
false,
2270+
'$t1',
2271+
null
2272+
),
2273+
new MethodTagValueParameterNode(
2274+
new IdentifierTypeNode('T2'),
2275+
false,
2276+
false,
2277+
'$t2',
2278+
null
2279+
),
2280+
new MethodTagValueParameterNode(
2281+
new IdentifierTypeNode('T3'),
2282+
false,
2283+
false,
2284+
'$t3',
2285+
null
2286+
),
2287+
],
2288+
'',
2289+
[
2290+
new TemplateTagValueNode('T1', null, ''),
2291+
new TemplateTagValueNode('T2', new IdentifierTypeNode('Bar'), ''),
2292+
new TemplateTagValueNode('T3', new IdentifierTypeNode('Baz'), ''),
2293+
]
2294+
)
2295+
),
2296+
]),
2297+
];
22042298
}
22052299

22062300

@@ -3078,6 +3172,45 @@ public function provideMultiLinePhpDocData(): array
30783172
),
30793173
]),
30803174
],
3175+
[
3176+
'OK with template method',
3177+
'/**
3178+
* @template TKey as array-key
3179+
* @template TValue
3180+
* @method TKey|null find(TValue $v) find index of $v
3181+
*/',
3182+
new PhpDocNode([
3183+
new PhpDocTagNode(
3184+
'@template',
3185+
new TemplateTagValueNode('TKey', new IdentifierTypeNode('array-key'), '')
3186+
),
3187+
new PhpDocTagNode(
3188+
'@template',
3189+
new TemplateTagValueNode('TValue', null, '')
3190+
),
3191+
new PhpDocTagNode(
3192+
'@method',
3193+
new MethodTagValueNode(
3194+
false,
3195+
new UnionTypeNode([
3196+
new IdentifierTypeNode('TKey'),
3197+
new IdentifierTypeNode('null'),
3198+
]),
3199+
'find',
3200+
[
3201+
new MethodTagValueParameterNode(
3202+
new IdentifierTypeNode('TValue'),
3203+
false,
3204+
false,
3205+
'$v',
3206+
null
3207+
),
3208+
],
3209+
'find index of $v'
3210+
)
3211+
),
3212+
]),
3213+
],
30813214
[
30823215
'OK with multiline conditional return type',
30833216
'/**

0 commit comments

Comments
 (0)