From abaea939624a64afa721d1f4a8416081c2e97e6f Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Fri, 23 Feb 2024 17:55:02 +0000 Subject: [PATCH] Add support for generic CallableType. --- src/PhpDoc/TypeNodeResolver.php | 51 +++++++++++++++++-- src/Type/CallableType.php | 12 ++++- src/Type/Generic/TemplateTypeScope.php | 9 ++++ .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/generic-callables.php | 14 +++++ 5 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/generic-callables.php diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index c43095a82fb..8f09f6a8180 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -10,6 +10,7 @@ use PhpParser\Node\Name; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -66,7 +67,12 @@ use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; +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\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Helper\GetTemplateTypeType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; @@ -874,18 +880,49 @@ static function (string $variance): TemplateTypeVariance { private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $nameScope): Type { $mainType = $this->resolve($typeNode->identifier, $nameScope); + + $templateTags = []; + + if (count($typeNode->templateTypes) > 0) { + foreach ($typeNode->templateTypes as $templateType) { + $templateTags[$templateType->name] = new TemplateTag( + $templateType->name, + $templateType->bound !== null + ? $this->resolve($templateType->bound, $nameScope) + : new MixedType(), + TemplateTypeVariance::createInvariant(), + ); + } + $templateTypeScope = TemplateTypeScope::createWithAnonymousFunction(); + + $templateTypeMap = new TemplateTypeMap(array_map( + static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), + $templateTags + )); + + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + } else { + $templateTypeMap = TemplateTypeMap::createEmpty(); + } + $isVariadic = false; $parameters = array_map( - function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic): NativeParameterReflection { + function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic, $templateTypeMap): NativeParameterReflection { $isVariadic = $isVariadic || $parameterNode->isVariadic; $parameterName = $parameterNode->parameterName; if (str_starts_with($parameterName, '$')) { $parameterName = substr($parameterName, 1); } + return new NativeParameterReflection( $parameterName, $parameterNode->isOptional || $parameterNode->isVariadic, - $this->resolve($parameterNode->type, $nameScope), + TemplateTypeHelper::resolveTemplateTypes( + $this->resolve($parameterNode->type, $nameScope), + $templateTypeMap, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant() + ), $parameterNode->isReference ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $parameterNode->isVariadic, null, @@ -893,10 +930,16 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi }, $typeNode->parameters, ); - $returnType = $this->resolve($typeNode->returnType, $nameScope); + + $returnType = TemplateTypeHelper::resolveTemplateTypes( + $this->resolve($typeNode->returnType, $nameScope), + $templateTypeMap, + TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant() + ); if ($mainType instanceof CallableType) { - return new CallableType($parameters, $returnType, $isVariadic); + return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap); } elseif ( $mainType instanceof ObjectType diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 698e5b72507..1c0500deb0a 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -54,6 +54,10 @@ class CallableType implements CompoundType, ParametersAcceptor private bool $isCommonCallable; + private TemplateTypeMap $templateTypeMap; + + private TemplateTypeMap $resolvedTemplateTypeMap; + /** * @api * @param array|null $parameters @@ -62,11 +66,15 @@ public function __construct( ?array $parameters = null, ?Type $returnType = null, private bool $variadic = true, + ?TemplateTypeMap $templateTypeMap = null, + ?TemplateTypeMap $resolvedTemplateTypeMap = null, ) { $this->parameters = $parameters ?? []; $this->returnType = $returnType ?? new MixedType(); $this->isCommonCallable = $parameters === null && $returnType === null; + $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); } /** @@ -243,12 +251,12 @@ public function toArrayKey(): Type public function getTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->templateTypeMap; } public function getResolvedTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->resolvedTemplateTypeMap; } public function getCallSiteVarianceMap(): TemplateTypeVarianceMap diff --git a/src/Type/Generic/TemplateTypeScope.php b/src/Type/Generic/TemplateTypeScope.php index f9a76257202..e40eab90d31 100644 --- a/src/Type/Generic/TemplateTypeScope.php +++ b/src/Type/Generic/TemplateTypeScope.php @@ -7,6 +7,11 @@ class TemplateTypeScope { + public static function createWithAnonymousFunction(): self + { + return new self(null, null); + } + public static function createWithFunction(string $functionName): self { return new self(null, $functionName); @@ -48,6 +53,10 @@ public function equals(self $other): bool /** @api */ public function describe(): string { + if ($this->className === null && $this->functionName === null) { + return 'anonymous function'; + } + if ($this->className === null) { return sprintf('function %s()', $this->functionName); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 3df5fed94c8..73fa915d020 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -27,6 +27,7 @@ public function dataFileAsserts(): iterable require_once __DIR__ . '/data/bug2574.php'; yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-callables.php'); require_once __DIR__ . '/data/bug2577.php'; diff --git a/tests/PHPStan/Analyser/data/generic-callables.php b/tests/PHPStan/Analyser/data/generic-callables.php new file mode 100644 index 00000000000..ce299ebdfde --- /dev/null +++ b/tests/PHPStan/Analyser/data/generic-callables.php @@ -0,0 +1,14 @@ +(TRet $val): TRet $callable + */ +function test(callable $callable, int $int, string $str): void +{ + assertType('int', $callable($int)); + //assertType('string', $callable($str)); +}