From 28043275e6ef6824190f977127260f1f3d91efcf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 20 Dec 2025 10:32:59 +0100 Subject: [PATCH 1/3] Remember function return type extensions --- src/Analyser/MutatingScope.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5f301be474..8c9f02b188 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -98,6 +98,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; use PHPStan\Type\ErrorType; use PHPStan\Type\ExpressionTypeResolverExtensionRegistry; @@ -192,6 +193,9 @@ class MutatingScope implements Scope, NodeCallbackInvoker private static int $resolveClosureTypeDepth = 0; + /** @var array> */ + private static array $functionReturnTypeExtensions = []; + /** * @param int|array{min: int, max: int}|null $configPhpVersion * @param callable(Node $node, Scope $scope): void|null $nodeCallback @@ -2500,7 +2504,15 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu private function getDynamicFunctionReturnType(FuncCall $normalizedNode, FunctionReflection $functionReflection): ?Type { - foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) { + $functionName = $functionReflection->getName(); + if (isset(self::$functionReturnTypeExtensions[$functionName])) { + $extensions = self::$functionReturnTypeExtensions[$functionName]; + } else { + $extensions = $this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions(); + } + + $supportedFunctions = []; + foreach ($extensions as $dynamicFunctionReturnTypeExtension) { if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { continue; } @@ -2510,11 +2522,15 @@ private function getDynamicFunctionReturnType(FuncCall $normalizedNode, Function $normalizedNode, $this, ); + + $supportedFunctions[] = $dynamicFunctionReturnTypeExtension; if ($resolvedType !== null) { + self::$functionReturnTypeExtensions[$functionName] = $supportedFunctions; return $resolvedType; } } + self::$functionReturnTypeExtensions[$functionName] = $supportedFunctions; return null; } From 4d571d6bfb3aaa890d783d88728dbeeabf4327a4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 20 Dec 2025 11:14:48 +0100 Subject: [PATCH 2/3] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8c9f02b188..cb826c1c29 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2525,12 +2525,12 @@ private function getDynamicFunctionReturnType(FuncCall $normalizedNode, Function $supportedFunctions[] = $dynamicFunctionReturnTypeExtension; if ($resolvedType !== null) { - self::$functionReturnTypeExtensions[$functionName] = $supportedFunctions; + self::$functionReturnTypeExtensions[$functionName] ??= $supportedFunctions; return $resolvedType; } } - self::$functionReturnTypeExtensions[$functionName] = $supportedFunctions; + self::$functionReturnTypeExtensions[$functionName] ??= $supportedFunctions; return null; } From 0694bf96eee5205abfeb96bac66bf5c987db52fd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 20 Dec 2025 14:27:39 +0100 Subject: [PATCH 3/3] move cache into DynamicReturnTypeExtensionRegistry --- src/Analyser/MutatingScope.php | 21 +----------------- .../DynamicReturnTypeExtensionRegistry.php | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index cb826c1c29..0b8974823e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -98,7 +98,6 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; -use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; use PHPStan\Type\ErrorType; use PHPStan\Type\ExpressionTypeResolverExtensionRegistry; @@ -193,9 +192,6 @@ class MutatingScope implements Scope, NodeCallbackInvoker private static int $resolveClosureTypeDepth = 0; - /** @var array> */ - private static array $functionReturnTypeExtensions = []; - /** * @param int|array{min: int, max: int}|null $configPhpVersion * @param callable(Node $node, Scope $scope): void|null $nodeCallback @@ -2504,33 +2500,18 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu private function getDynamicFunctionReturnType(FuncCall $normalizedNode, FunctionReflection $functionReflection): ?Type { - $functionName = $functionReflection->getName(); - if (isset(self::$functionReturnTypeExtensions[$functionName])) { - $extensions = self::$functionReturnTypeExtensions[$functionName]; - } else { - $extensions = $this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions(); - } - - $supportedFunctions = []; - foreach ($extensions as $dynamicFunctionReturnTypeExtension) { - if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { - continue; - } - + foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions($functionReflection) as $dynamicFunctionReturnTypeExtension) { $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall( $functionReflection, $normalizedNode, $this, ); - $supportedFunctions[] = $dynamicFunctionReturnTypeExtension; if ($resolvedType !== null) { - self::$functionReturnTypeExtensions[$functionName] ??= $supportedFunctions; return $resolvedType; } } - self::$functionReturnTypeExtensions[$functionName] ??= $supportedFunctions; return null; } diff --git a/src/Type/DynamicReturnTypeExtensionRegistry.php b/src/Type/DynamicReturnTypeExtensionRegistry.php index e2a30437b3..32dc24b6c5 100644 --- a/src/Type/DynamicReturnTypeExtensionRegistry.php +++ b/src/Type/DynamicReturnTypeExtensionRegistry.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; use function array_merge; use function strtolower; @@ -15,6 +16,9 @@ final class DynamicReturnTypeExtensionRegistry /** @var DynamicStaticMethodReturnTypeExtension[][]|null */ private ?array $dynamicStaticMethodReturnTypeExtensionsByClass = null; + /** @var array> */ + private array $dynamicReturnTypeExtensionsByFunction = []; + /** * @param DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions * @param DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions @@ -88,9 +92,23 @@ private function getDynamicExtensionsForType(array $extensions, string $classNam /** * @return DynamicFunctionReturnTypeExtension[] */ - public function getDynamicFunctionReturnTypeExtensions(): array + public function getDynamicFunctionReturnTypeExtensions(FunctionReflection $functionReflection): array { - return $this->dynamicFunctionReturnTypeExtensions; + $functionName = $functionReflection->getName(); + if (isset($this->dynamicReturnTypeExtensionsByFunction[$functionName])) { + return $this->dynamicReturnTypeExtensionsByFunction[$functionName]; + } + + $supportedFunctions = []; + foreach ($this->dynamicFunctionReturnTypeExtensions as $dynamicFunctionReturnTypeExtension) { + if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) { + continue; + } + + $supportedFunctions[] = $dynamicFunctionReturnTypeExtension; + } + + return $this->dynamicReturnTypeExtensionsByFunction[$functionName] ??= $supportedFunctions; } }