Skip to content

Commit e9b40b7

Browse files
authored
Add resolution of generic @method tags
1 parent 97408e6 commit e9b40b7

File tree

6 files changed

+72
-1
lines changed

6 files changed

+72
-1
lines changed

src/PhpDoc/PhpDocNodeResolver.php

+22
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
3131
use PHPStan\Reflection\PassedByReference;
3232
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
33+
use PHPStan\Type\Generic\TemplateTypeFactory;
34+
use PHPStan\Type\Generic\TemplateTypeMap;
35+
use PHPStan\Type\Generic\TemplateTypeScope;
3336
use PHPStan\Type\Generic\TemplateTypeVariance;
3437
use PHPStan\Type\MixedType;
3538
use PHPStan\Type\Type;
@@ -160,6 +163,24 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope):
160163

161164
foreach (['@method', '@psalm-method', '@phpstan-method'] as $tagName) {
162165
foreach ($phpDocNode->getMethodTagValues($tagName) as $tagValue) {
166+
$templateTags = [];
167+
168+
if (count($tagValue->templateTypes) > 0 && $nameScope->getClassName() !== null) {
169+
foreach ($tagValue->templateTypes as $templateType) {
170+
$templateTags[$templateType->name] = new TemplateTag(
171+
$templateType->name,
172+
$templateType->bound !== null
173+
? $this->typeNodeResolver->resolve($templateType->bound, $nameScope)
174+
: new MixedType(),
175+
TemplateTypeVariance::createInvariant(),
176+
);
177+
}
178+
179+
$templateTypeScope = TemplateTypeScope::createWithMethod($nameScope->getClassName(), $tagValue->methodName);
180+
$templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags));
181+
$nameScope = $nameScope->withTemplateTypeMap($templateTypeMap);
182+
}
183+
163184
$parameters = [];
164185
foreach ($tagValue->parameters as $parameterNode) {
165186
$parameterName = substr($parameterNode->parameterName, 1);
@@ -191,6 +212,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope):
191212
: new MixedType(),
192213
$tagValue->isStatic,
193214
$parameters,
215+
$templateTags,
194216
);
195217
}
196218
}

src/PhpDoc/Tag/MethodTag.php

+10
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ class MethodTag
1010

1111
/**
1212
* @param array<string, MethodTagParameter> $parameters
13+
* @param array<string, TemplateTag> $templateTags
1314
*/
1415
public function __construct(
1516
private Type $returnType,
1617
private bool $isStatic,
1718
private array $parameters,
19+
private array $templateTags = [],
1820
)
1921
{
2022
}
@@ -37,4 +39,12 @@ public function getParameters(): array
3739
return $this->parameters;
3840
}
3941

42+
/**
43+
* @return array<string, TemplateTag>
44+
*/
45+
public function getTemplateTags(): array
46+
{
47+
return $this->templateTags;
48+
}
49+
4050
}

src/Reflection/Annotations/AnnotationMethodReflection.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function __construct(
3030
private bool $isStatic,
3131
private bool $isVariadic,
3232
private ?Type $throwType,
33+
private TemplateTypeMap $templateTypeMap,
3334
)
3435
{
3536
}
@@ -69,7 +70,7 @@ public function getVariants(): array
6970
if ($this->variants === null) {
7071
$this->variants = [
7172
new FunctionVariantWithPhpDocs(
72-
TemplateTypeMap::createEmpty(),
73+
$this->templateTypeMap,
7374
null,
7475
$this->parameters,
7576
$this->isVariadic,

src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php

+14
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
namespace PHPStan\Reflection\Annotations;
44

5+
use PHPStan\PhpDoc\Tag\TemplateTag;
56
use PHPStan\Reflection\ClassReflection;
67
use PHPStan\Reflection\ExtendedMethodReflection;
78
use PHPStan\Reflection\MethodReflection;
89
use PHPStan\Reflection\MethodsClassReflectionExtension;
10+
use PHPStan\Type\Generic\TemplateTypeFactory;
911
use PHPStan\Type\Generic\TemplateTypeHelper;
12+
use PHPStan\Type\Generic\TemplateTypeMap;
13+
use PHPStan\Type\Generic\TemplateTypeScope;
1014
use PHPStan\Type\Generic\TemplateTypeVariance;
15+
use PHPStan\Type\Type;
16+
use function array_map;
1117
use function count;
1218

1319
class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension
@@ -57,6 +63,13 @@ private function findClassReflectionWithMethod(
5763
);
5864
}
5965

66+
$templateTypeScope = TemplateTypeScope::createWithClass($classReflection->getName());
67+
68+
$templateTypeMap = new TemplateTypeMap(array_map(
69+
static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag),
70+
$methodTags[$methodName]->getTemplateTags(),
71+
));
72+
6073
$isStatic = $methodTags[$methodName]->isStatic();
6174
$nativeCallMethodName = $isStatic ? '__callStatic' : '__call';
6275

@@ -75,6 +88,7 @@ private function findClassReflectionWithMethod(
7588
$classReflection->hasNativeMethod($nativeCallMethodName)
7689
? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType()
7790
: null,
91+
$templateTypeMap,
7892
);
7993
}
8094

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public function dataFileAsserts(): iterable
2222
yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type_with_force_array.php');
2323
yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/invalid_type.php');
2424
yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/json_object_as_array.php');
25+
yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-method-tags.php');
2526

2627
require_once __DIR__ . '/data/bug2574.php';
2728

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace GenericMethodTags;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @method TVal doThing<TVal of mixed>(TVal $param)
9+
*/
10+
class Test
11+
{
12+
public function __call(): mixed
13+
{
14+
}
15+
}
16+
17+
function test(int $int, string $string): void
18+
{
19+
$test = new Test();
20+
21+
assertType('int', $test->doThing($int));
22+
assertType('string', $test->doThing($string));
23+
}

0 commit comments

Comments
 (0)