From 8a6918fc071df815b9ca2d68850f6560cde23a6a Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:51:32 +0000 Subject: [PATCH 1/5] Wip: added resolution of generic @method tags. --- src/PhpDoc/PhpDocNodeResolver.php | 12 ++++++++++ src/PhpDoc/Tag/MethodTag.php | 10 ++++++++ .../AnnotationMethodReflection.php | 3 ++- ...tationsMethodsClassReflectionExtension.php | 14 +++++++++++ .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/generic-method-tags.php | 23 +++++++++++++++++++ 6 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/data/generic-method-tags.php diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 6cbfe6120e..9e7d9ca831 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -185,12 +185,24 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): ); } + $templates = []; + foreach ($tagValue->templateTypes as $templateType) { + $templates[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) + : new MixedType(), + TemplateTypeVariance::createInvariant() + ); + } + $resolved[$tagValue->methodName] = new MethodTag( $tagValue->returnType !== null ? $this->typeNodeResolver->resolve($tagValue->returnType, $nameScope) : new MixedType(), $tagValue->isStatic, $parameters, + $templates ); } } diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index e640418ea8..f86eb0aaa7 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -10,11 +10,13 @@ class MethodTag /** * @param array $parameters + * @param array $templates */ public function __construct( private Type $returnType, private bool $isStatic, private array $parameters, + private array $templates = [], ) { } @@ -37,4 +39,12 @@ public function getParameters(): array return $this->parameters; } + /** + * @return array + */ + public function getTemplates(): array + { + return $this->templates; + } + } diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index d10888abde..cfd5b74e03 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -30,6 +30,7 @@ public function __construct( private bool $isStatic, private bool $isVariadic, private ?Type $throwType, + private TemplateTypeMap $templateTypeMap, ) { } @@ -69,7 +70,7 @@ public function getVariants(): array if ($this->variants === null) { $this->variants = [ new FunctionVariantWithPhpDocs( - TemplateTypeMap::createEmpty(), + $this->templateTypeMap, null, $this->parameters, $this->isVariadic, diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index e1cee2da24..0216b328b0 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -2,12 +2,18 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; +use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Type; + use function count; class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension @@ -57,6 +63,13 @@ private function findClassReflectionWithMethod( ); } + $templateTypeScope = TemplateTypeScope::createWithClass($classReflection->getName()); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $methodTags[$methodName]->getTemplates() + )); + $isStatic = $methodTags[$methodName]->isStatic(); $nativeCallMethodName = $isStatic ? '__callStatic' : '__call'; @@ -75,6 +88,7 @@ private function findClassReflectionWithMethod( $classReflection->hasNativeMethod($nativeCallMethodName) ? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType() : null, + $templateTypeMap, ); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 1006fb0409..73d9e7f373 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -22,6 +22,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type_with_force_array.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/invalid_type.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/json_object_as_array.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-method-tags.php'); require_once __DIR__ . '/data/bug2574.php'; diff --git a/tests/PHPStan/Analyser/data/generic-method-tags.php b/tests/PHPStan/Analyser/data/generic-method-tags.php new file mode 100644 index 0000000000..da7f89bd24 --- /dev/null +++ b/tests/PHPStan/Analyser/data/generic-method-tags.php @@ -0,0 +1,23 @@ +(TVal $param) + */ +class Test +{ + public function __call(): mixed + { + } +} + +function test(int $int, string $string): void +{ + $test = new Test(); + + assertType('int', $test->doThing($int)); + assertType('string', $test->doThing($string)); +} From d52bb84a21ce031d3c45e2c63dce8b423c50a759 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 23 Feb 2024 11:26:10 +0100 Subject: [PATCH 2/5] Use the right scope here --- src/PhpDoc/PhpDocNodeResolver.php | 34 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 9e7d9ca831..9e209f0bdd 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -30,6 +30,9 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\Reflection\PassedByReference; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -160,6 +163,24 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): foreach (['@method', '@psalm-method', '@phpstan-method'] as $tagName) { foreach ($phpDocNode->getMethodTagValues($tagName) as $tagValue) { + $templateTags = []; + + if (count($tagValue->templateTypes) > 0) { + foreach ($tagValue->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) + : new MixedType(), + TemplateTypeVariance::createInvariant() + ); + } + + $templateTypeScope = TemplateTypeScope::createWithMethod($nameScope->getClassName(), $tagValue->methodName); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } + $parameters = []; foreach ($tagValue->parameters as $parameterNode) { $parameterName = substr($parameterNode->parameterName, 1); @@ -185,24 +206,13 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): ); } - $templates = []; - foreach ($tagValue->templateTypes as $templateType) { - $templates[$templateType->name] = new TemplateTag( - $templateType->name, - $templateType->bound !== null - ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) - : new MixedType(), - TemplateTypeVariance::createInvariant() - ); - } - $resolved[$tagValue->methodName] = new MethodTag( $tagValue->returnType !== null ? $this->typeNodeResolver->resolve($tagValue->returnType, $nameScope) : new MixedType(), $tagValue->isStatic, $parameters, - $templates + $templateTags, ); } } From 8e2967e99278bff431798fa42fa285f331aad8ca Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 23 Feb 2024 11:27:02 +0100 Subject: [PATCH 3/5] Rename method --- src/PhpDoc/Tag/MethodTag.php | 8 ++++---- .../AnnotationsMethodsClassReflectionExtension.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index f86eb0aaa7..9f46c124d2 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -10,13 +10,13 @@ class MethodTag /** * @param array $parameters - * @param array $templates + * @param array $templateTags */ public function __construct( private Type $returnType, private bool $isStatic, private array $parameters, - private array $templates = [], + private array $templateTags = [], ) { } @@ -42,9 +42,9 @@ public function getParameters(): array /** * @return array */ - public function getTemplates(): array + public function getTemplateTags(): array { - return $this->templates; + return $this->templateTags; } } diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index 0216b328b0..56a6e96d33 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -67,7 +67,7 @@ private function findClassReflectionWithMethod( $templateTypeMap = new TemplateTypeMap(array_map( static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), - $methodTags[$methodName]->getTemplates() + $methodTags[$methodName]->getTemplateTags() )); $isStatic = $methodTags[$methodName]->isStatic(); From 29810a9be85b5fc153093b40b9f972121a98d8ef Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 23 Feb 2024 11:32:25 +0100 Subject: [PATCH 4/5] Fix CS --- src/PhpDoc/PhpDocNodeResolver.php | 2 +- .../AnnotationsMethodsClassReflectionExtension.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 9e209f0bdd..2f625fde77 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -172,7 +172,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): $templateType->bound !== null ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) : new MixedType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); } diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index 56a6e96d33..2860988c91 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -13,7 +13,7 @@ use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; - +use function array_map; use function count; class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension @@ -67,7 +67,7 @@ private function findClassReflectionWithMethod( $templateTypeMap = new TemplateTypeMap(array_map( static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), - $methodTags[$methodName]->getTemplateTags() + $methodTags[$methodName]->getTemplateTags(), )); $isStatic = $methodTags[$methodName]->isStatic(); From 39cebde6d1671a4dd8ea6ecfd86ecadc989c40ac Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 23 Feb 2024 11:32:43 +0100 Subject: [PATCH 5/5] Fix PHPStan --- src/PhpDoc/PhpDocNodeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 2f625fde77..aa88501817 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -165,7 +165,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): foreach ($phpDocNode->getMethodTagValues($tagName) as $tagValue) { $templateTags = []; - if (count($tagValue->templateTypes) > 0) { + if (count($tagValue->templateTypes) > 0 && $nameScope->getClassName() !== null) { foreach ($tagValue->templateTypes as $templateType) { $templateTags[$templateType->name] = new TemplateTag( $templateType->name,