From d210c6afbf52e1e041874109dd27e5e65f565bad Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Sun, 25 Jan 2026 12:11:50 +0100 Subject: [PATCH] Support StaticMethodParameterClosureTypeExtension for New_ expressions --- src/Analyser/NodeScopeResolver.php | 11 ++++ ...-closure-type-extension-arrow-function.php | 54 ++++++++++++++++- .../data/parameter-closure-type-extension.php | 58 ++++++++++++++++++- 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 64d0dd9ec1..a9ce34be41 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4816,6 +4816,17 @@ private function getParameterTypeFromParameterClosureTypeExtension(CallLike $cal return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); } } + } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) { + $staticCall = new StaticCall( + $callLike->class, + new Identifier('__construct'), + $callLike->getArgs(), + ); + foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) { + if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { + return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope); + } + } } elseif ($callLike instanceof MethodCall) { foreach ($this->parameterClosureTypeExtensionProvider->getMethodParameterClosureTypeExtensions() as $methodParameterClosureTypeExtension) { if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) { diff --git a/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php b/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php index b3876d7408..c52cb4a5a5 100644 --- a/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php +++ b/tests/PHPStan/Analyser/data/parameter-closure-type-extension-arrow-function.php @@ -107,7 +107,15 @@ class StaticMethodParameterClosureTypeExtension implements \PHPStan\Type\StaticM public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool { - return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithCallable'; + if ($methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithCallable') { + return true; + } + + if ($methodReflection->getDeclaringClass()->getName() === Bar::class && $methodReflection->getName() === '__construct') { + return true; + } + + return false; } public function getTypeFromStaticMethodCall( @@ -116,6 +124,32 @@ public function getTypeFromStaticMethodCall( ParameterReflection $parameter, Scope $scope ): ?Type { + if ($methodReflection->getDeclaringClass()->getName() === Bar::class && $methodReflection->getName() === '__construct') { + $args = $methodCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new CallableType( + [ + new NativeParameterReflection('test', false, new IntegerType(), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + return new CallableType( + [ + new NativeParameterReflection('test', false, new StringType(), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + return new CallableType( [ new NativeParameterReflection('test', false, new FloatType(), PassedByReference::createNo(), false, null), @@ -173,6 +207,20 @@ public function getValue() } } +class Bar +{ + + /** + * @param int $foo + * @param callable(mixed) $callback + */ + public function __construct(int $foo, callable $callback) + { + + } + +} + /** * @param int $foo * @param callable(Generic) $callback @@ -192,6 +240,10 @@ function test(Foo $foo): void (new Foo)->methodWithCallable(2, fn (Generic $i) => assertType('string', $i->getValue())); Foo::staticMethodWithCallable(fn ($i) => assertType('float', $i)); + + new Bar(1, fn ($i) => assertType('int', $i)); + + new Bar(2, fn ($i) => assertType('string', $i)); } functionWithCallable(1, fn ($i) => assertType('int', $i->getValue())); diff --git a/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php b/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php index e081621ff1..5f1d595b7b 100644 --- a/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php +++ b/tests/PHPStan/Analyser/data/parameter-closure-type-extension.php @@ -117,7 +117,15 @@ class StaticMethodParameterClosureTypeExtension implements \PHPStan\Type\StaticM public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool { - return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithClosure'; + if ($methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithClosure') { + return true; + } + + if ($methodReflection->getDeclaringClass()->getName() === Bar::class && $methodReflection->getName() === '__construct') { + return true; + } + + return false; } public function getTypeFromStaticMethodCall( @@ -126,6 +134,32 @@ public function getTypeFromStaticMethodCall( ParameterReflection $parameter, Scope $scope ): ?Type { + if ($methodReflection->getDeclaringClass()->getName() === Bar::class && $methodReflection->getName() === '__construct') { + $args = $methodCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new ClosureType( + [ + new NativeParameterReflection('test', false, new IntegerType(), PassedByReference::createNo(), false, null), + ], + new VoidType() + ); + } + + return new ClosureType( + [ + new NativeParameterReflection('test', false, new StringType(), PassedByReference::createNo(), false, null), + ], + new VoidType() + ); + } + return new ClosureType( [ new NativeParameterReflection('test', false, new FloatType(), PassedByReference::createNo(), false, null), @@ -185,6 +219,20 @@ public function getValue() } } +class Bar +{ + + /** + * @param int $foo + * @param Closure(mixed): void $callback + */ + public function __construct(int $foo, Closure $callback) + { + + } + +} + /** * @param int $foo * @param Closure(Generic): void $callback @@ -209,6 +257,14 @@ function test(Foo $foo): void Foo::staticMethodWithClosure(function ($i) { assertType('float', $i); }); + + new Bar(1, function ($i) { + assertType('int', $i); + }); + + new Bar(2, function ($i) { + assertType('string', $i); + }); } functionWithClosure(1, function ($i) {