From 4444cef535cdbb274fa28b2a5a91c5cce2731d21 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sat, 16 May 2026 12:17:20 +0000 Subject: [PATCH 1/4] Include `null` in return type of `Closure::bindTo()` and `Closure::bind()` dynamic return type extensions - ClosureBindToDynamicReturnTypeExtension now returns TypeCombinator::union($closureType, new NullType()) instead of just $closureType - ClosureBindDynamicReturnTypeExtension (for static Closure::bind()) gets the same fix - Updated existing tests that asserted the buggy non-nullable return type - Updated AnalyserIntegrationTest::testBug4734 to expect the 4 new "Trying to invoke ... null" errors Closes https://github.com/phpstan/phpstan/issues/5009 --- .../ClosureBindDynamicReturnTypeExtension.php | 4 ++- ...losureBindToDynamicReturnTypeExtension.php | 4 ++- .../Analyser/AnalyserIntegrationTest.php | 10 +++++--- tests/PHPStan/Analyser/nsrt/bug-5009.php | 25 +++++++++++++++++++ .../nsrt/closure-return-type-extensions.php | 6 ++--- 5 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-5009.php diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 580595acd74..60b7332be49 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -9,7 +9,9 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; #[AutowiredService] final class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension @@ -32,7 +34,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, return null; } - return $closureType; + return TypeCombinator::union($closureType, new NullType()); } } diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index 3e275677e27..f343e097dba 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -9,7 +9,9 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicMethodReturnTypeExtension; +use PHPStan\Type\NullType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; #[AutowiredService] final class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension @@ -32,7 +34,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - return $closureType; + return TypeCombinator::union($closureType, new NullType()); } } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 27b9f44c3de..0dd1e00e662 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -473,13 +473,17 @@ public function testBug4734(): void { // false positive $errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php'); - $this->assertCount(5, $errors); // could be 3 + $this->assertCount(9, $errors); // could be 3 $this->assertSame('Static property Bug4734\Foo::$httpMethodParameterOverride (bool) is never assigned false so the property type can be changed to true.', $errors[0]->getMessage()); // should not error $this->assertSame('Property Bug4734\Foo::$httpMethodParameterOverride2 (bool) is never assigned false so the property type can be changed to true.', $errors[1]->getMessage()); // should not error $this->assertSame('Unsafe access to private property Bug4734\Foo::$httpMethodParameterOverride through static::.', $errors[2]->getMessage()); - $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[3]->getMessage()); - $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[4]->getMessage()); + $this->assertSame('Trying to invoke (Closure(): void)|null but it might not be a callable.', $errors[3]->getMessage()); + $this->assertSame('Trying to invoke (Closure(): void)|null but it might not be a callable.', $errors[4]->getMessage()); + $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[5]->getMessage()); + $this->assertSame('Trying to invoke (Closure(): void)|null but it might not be a callable.', $errors[6]->getMessage()); + $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[7]->getMessage()); + $this->assertSame('Trying to invoke (Closure(): void)|null but it might not be a callable.', $errors[8]->getMessage()); } #[RequiresPhp('>= 8.1.0')] diff --git a/tests/PHPStan/Analyser/nsrt/bug-5009.php b/tests/PHPStan/Analyser/nsrt/bug-5009.php new file mode 100644 index 00000000000..85c07b1719d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5009.php @@ -0,0 +1,25 @@ +bindTo(null); +assertType('(Closure(): void)|null', $bar); + +$baz = Closure::bind($foo, null); +assertType('(Closure(): void)|null', $baz); + +$newThis = new \stdClass(); +$bound = $foo->bindTo($newThis); +assertType('(Closure(): void)|null', $bound); + +$staticBound = Closure::bind($foo, $newThis); +assertType('(Closure(): void)|null', $staticBound); + +$bound2 = $foo->bindTo($newThis, 'stdClass'); +assertType('(Closure(): void)|null', $bound2); diff --git a/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php index 1445bfad9f7..df94e0ee17d 100644 --- a/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php +++ b/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php @@ -11,13 +11,13 @@ $newThis = new class {}; $boundClosure = $closure->bindTo($newThis); -assertType('Closure(object): true', $boundClosure); +assertType('(Closure(object): true)|null', $boundClosure); $staticallyBoundClosure = \Closure::bind($closure, $newThis); -assertType('Closure(object): true', $staticallyBoundClosure); +assertType('(Closure(object): true)|null', $staticallyBoundClosure); $returnType = $closure->call($newThis, new class {}); assertType('true', $returnType); $staticallyBoundClosureCaseInsensitive = \closure::bind($closure, $newThis); -assertType('Closure(object): true', $staticallyBoundClosureCaseInsensitive); +assertType('(Closure(object): true)|null', $staticallyBoundClosureCaseInsensitive); From bbbfe98ddcd57734136a93ab3d7c16dcbac9f798 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 12:38:10 +0000 Subject: [PATCH 2/4] Use BenevolentUnionType for Closure::bindTo() and Closure::bind() return types The function signature map declares these as `__benevolent`, so the dynamic return type extensions should preserve the benevolent union semantics instead of creating a strict union. This avoids false positives for code that invokes the result without null-checking, matching the behavior of the default signature map. Co-Authored-By: Claude Opus 4.6 --- src/Type/Php/ClosureBindDynamicReturnTypeExtension.php | 4 ++-- .../Php/ClosureBindToDynamicReturnTypeExtension.php | 4 ++-- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 10 +++------- tests/PHPStan/Analyser/nsrt/bug-5009.php | 10 +++++----- .../Analyser/nsrt/closure-return-type-extensions.php | 6 +++--- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 60b7332be49..1e00c2e08ac 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -7,11 +7,11 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; #[AutowiredService] final class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension @@ -34,7 +34,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, return null; } - return TypeCombinator::union($closureType, new NullType()); + return new BenevolentUnionType([$closureType, new NullType()]); } } diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index f343e097dba..51993559a05 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -7,11 +7,11 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; #[AutowiredService] final class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension @@ -34,7 +34,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - return TypeCombinator::union($closureType, new NullType()); + return new BenevolentUnionType([$closureType, new NullType()]); } } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 0dd1e00e662..27b9f44c3de 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -473,17 +473,13 @@ public function testBug4734(): void { // false positive $errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php'); - $this->assertCount(9, $errors); // could be 3 + $this->assertCount(5, $errors); // could be 3 $this->assertSame('Static property Bug4734\Foo::$httpMethodParameterOverride (bool) is never assigned false so the property type can be changed to true.', $errors[0]->getMessage()); // should not error $this->assertSame('Property Bug4734\Foo::$httpMethodParameterOverride2 (bool) is never assigned false so the property type can be changed to true.', $errors[1]->getMessage()); // should not error $this->assertSame('Unsafe access to private property Bug4734\Foo::$httpMethodParameterOverride through static::.', $errors[2]->getMessage()); - $this->assertSame('Trying to invoke (Closure(): void)|null but it might not be a callable.', $errors[3]->getMessage()); - $this->assertSame('Trying to invoke (Closure(): void)|null but it might not be a callable.', $errors[4]->getMessage()); - $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[5]->getMessage()); - $this->assertSame('Trying to invoke (Closure(): void)|null but it might not be a callable.', $errors[6]->getMessage()); - $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[7]->getMessage()); - $this->assertSame('Trying to invoke (Closure(): void)|null but it might not be a callable.', $errors[8]->getMessage()); + $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[3]->getMessage()); + $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[4]->getMessage()); } #[RequiresPhp('>= 8.1.0')] diff --git a/tests/PHPStan/Analyser/nsrt/bug-5009.php b/tests/PHPStan/Analyser/nsrt/bug-5009.php index 85c07b1719d..a9160d4f6f5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5009.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5009.php @@ -9,17 +9,17 @@ $foo = function (): void {}; $bar = $foo->bindTo(null); -assertType('(Closure(): void)|null', $bar); +assertType('((Closure(): void)|null)', $bar); $baz = Closure::bind($foo, null); -assertType('(Closure(): void)|null', $baz); +assertType('((Closure(): void)|null)', $baz); $newThis = new \stdClass(); $bound = $foo->bindTo($newThis); -assertType('(Closure(): void)|null', $bound); +assertType('((Closure(): void)|null)', $bound); $staticBound = Closure::bind($foo, $newThis); -assertType('(Closure(): void)|null', $staticBound); +assertType('((Closure(): void)|null)', $staticBound); $bound2 = $foo->bindTo($newThis, 'stdClass'); -assertType('(Closure(): void)|null', $bound2); +assertType('((Closure(): void)|null)', $bound2); diff --git a/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php index df94e0ee17d..eb21b2fb97d 100644 --- a/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php +++ b/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php @@ -11,13 +11,13 @@ $newThis = new class {}; $boundClosure = $closure->bindTo($newThis); -assertType('(Closure(object): true)|null', $boundClosure); +assertType('((Closure(object): true)|null)', $boundClosure); $staticallyBoundClosure = \Closure::bind($closure, $newThis); -assertType('(Closure(object): true)|null', $staticallyBoundClosure); +assertType('((Closure(object): true)|null)', $staticallyBoundClosure); $returnType = $closure->call($newThis, new class {}); assertType('true', $returnType); $staticallyBoundClosureCaseInsensitive = \closure::bind($closure, $newThis); -assertType('(Closure(object): true)|null', $staticallyBoundClosureCaseInsensitive); +assertType('((Closure(object): true)|null)', $staticallyBoundClosureCaseInsensitive); From 9a6bdf3fcc4aedd119addd65e11677778603483a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 13:03:48 +0000 Subject: [PATCH 3/4] Exclude null from Closure::bindTo()/bind() return type when $newThis is null When $newThis is null, bindTo()/bind() only changes the closure's scope without binding $this, which always succeeds. Null is only returned when binding a static closure with a non-null $newThis. Co-Authored-By: Claude Opus 4.6 --- .../ClosureBindDynamicReturnTypeExtension.php | 7 ++++++- .../ClosureBindToDynamicReturnTypeExtension.php | 5 +++++ tests/PHPStan/Analyser/nsrt/bug-5009.php | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 1e00c2e08ac..7a83c983935 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -29,11 +29,16 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - $closureType = $scope->getType($methodCall->getArgs()[0]->value); + $args = $methodCall->getArgs(); + $closureType = $scope->getType($args[0]->value); if (!($closureType instanceof ClosureType)) { return null; } + if (isset($args[1]) && $scope->getType($args[1]->value)->isNull()->yes()) { + return $closureType; + } + return new BenevolentUnionType([$closureType, new NullType()]); } diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index 51993559a05..089745099cf 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -34,6 +34,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } + $args = $methodCall->getArgs(); + if (isset($args[0]) && $scope->getType($args[0]->value)->isNull()->yes()) { + return $closureType; + } + return new BenevolentUnionType([$closureType, new NullType()]); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-5009.php b/tests/PHPStan/Analyser/nsrt/bug-5009.php index a9160d4f6f5..7e64dcc0632 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5009.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5009.php @@ -9,10 +9,10 @@ $foo = function (): void {}; $bar = $foo->bindTo(null); -assertType('((Closure(): void)|null)', $bar); +assertType('Closure(): void', $bar); $baz = Closure::bind($foo, null); -assertType('((Closure(): void)|null)', $baz); +assertType('Closure(): void', $baz); $newThis = new \stdClass(); $bound = $foo->bindTo($newThis); @@ -23,3 +23,15 @@ $bound2 = $foo->bindTo($newThis, 'stdClass'); assertType('((Closure(): void)|null)', $bound2); + +$static = static function (): void {}; +$boundStatic = $static->bindTo($newThis); +assertType('((Closure(): void)|null)', $boundStatic); + +$boundStaticNull = $static->bindTo(null); +assertType('Closure(): void', $boundStaticNull); + +/** @var \stdClass|null $maybeNull */ +$maybeNull = null; +$boundMaybe = $foo->bindTo($maybeNull); +assertType('((Closure(): void)|null)', $boundMaybe); From 2a8f6c8d58be34333e0d176b1de941f016290985 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 13:19:28 +0000 Subject: [PATCH 4/4] Track static-ness of closures in ClosureType to exclude null from bindTo()/bind() return type Add an optional `?TrinaryLogic $isStatic` parameter to ClosureType's constructor to track whether a closure was declared with the `static` keyword. ClosureTypeResolver now passes this from the AST node. The bind extensions use this to exclude null from the return type when the closure is known to be non-static (binding always succeeds for non-static closures regardless of $newThis). For static or possibly-static closures with non-null $newThis, the return type remains BenevolentUnionType(Closure|null) since PHP returns null when binding an object to a static closure. Co-Authored-By: Claude Opus 4.6 --- .../ExprHandler/Helper/ClosureTypeResolver.php | 3 +++ src/Reflection/InitializerExprTypeResolver.php | 1 + src/Type/ClosureType.php | 12 ++++++++++++ .../ClosureBindDynamicReturnTypeExtension.php | 4 ++++ .../ClosureBindToDynamicReturnTypeExtension.php | 4 ++++ tests/PHPStan/Analyser/nsrt/bug-5009.php | 17 +++++++++++++---- 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php index e996bafbdda..09a24a1249b 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php @@ -220,6 +220,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu usedVariables: $cachedClosureData['usedVariables'], acceptsNamedArguments: TrinaryLogic::createYes(), mustUseReturnValue: $mustUseReturnValue, + isStatic: TrinaryLogic::createFromBoolean($expr->static), ); } if (self::$resolveClosureTypeDepth >= 2) { @@ -227,6 +228,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $parameters, $scope->getFunctionType($expr->returnType, false, false), $isVariadic, + isStatic: TrinaryLogic::createFromBoolean($expr->static), ); } @@ -446,6 +448,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu usedVariables: $usedVariables, acceptsNamedArguments: TrinaryLogic::createYes(), mustUseReturnValue: $mustUseReturnValue, + isStatic: TrinaryLogic::createFromBoolean($expr->static), ); } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 495b8266664..08d8964fd8c 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -263,6 +263,7 @@ public function getType(Expr $expr, InitializerExprContext $context): Type TemplateTypeMap::createEmpty(), TemplateTypeVarianceMap::createEmpty(), acceptsNamedArguments: TrinaryLogic::createYes(), + isStatic: TrinaryLogic::createYes(), ); } if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index a819526021f..2c4f5a7f9b3 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -88,6 +88,8 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor private Assertions $assertions; + private TrinaryLogic $isStatic; + /** * @api * @param list|null $parameters @@ -112,6 +114,7 @@ public function __construct( ?TrinaryLogic $acceptsNamedArguments = null, ?TrinaryLogic $mustUseReturnValue = null, ?Assertions $assertions = null, + ?TrinaryLogic $isStatic = null, ) { if ($acceptsNamedArguments === null) { @@ -132,6 +135,7 @@ public function __construct( $this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty(); $this->impurePoints = $impurePoints ?? [new SimpleImpurePoint('functionCall', 'call to an unknown Closure', false)]; $this->assertions = $assertions ?? Assertions::createEmpty(); + $this->isStatic = $isStatic ?? TrinaryLogic::createMaybe(); } public function getAsserts(): Assertions @@ -302,6 +306,7 @@ function (): string { $this->usedVariables, $this->acceptsNamedArguments, $this->mustUseReturnValue, + isStatic: $this->isStatic, ); return $printer->print($selfWithoutParameterNames->toPhpDocNode()); @@ -461,6 +466,11 @@ public function isCommonCallable(): bool return $this->isCommonCallable; } + public function isStaticClosure(): TrinaryLogic + { + return $this->isStatic; + } + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { return [$this]; @@ -709,6 +719,7 @@ public function traverse(callable $cb): Type $this->acceptsNamedArguments, $this->mustUseReturnValue, $this->assertions, + $this->isStatic, ); } @@ -761,6 +772,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $this->acceptsNamedArguments, $this->mustUseReturnValue, $this->assertions, + $this->isStatic, ); } diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 7a83c983935..8b0908dadfb 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -35,6 +35,10 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, return null; } + if ($closureType->isStaticClosure()->no()) { + return $closureType; + } + if (isset($args[1]) && $scope->getType($args[1]->value)->isNull()->yes()) { return $closureType; } diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index 089745099cf..7423c73198d 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -34,6 +34,10 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } + if ($closureType->isStaticClosure()->no()) { + return $closureType; + } + $args = $methodCall->getArgs(); if (isset($args[0]) && $scope->getType($args[0]->value)->isNull()->yes()) { return $closureType; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5009.php b/tests/PHPStan/Analyser/nsrt/bug-5009.php index 7e64dcc0632..691af4c4646 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5009.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5009.php @@ -16,13 +16,13 @@ $newThis = new \stdClass(); $bound = $foo->bindTo($newThis); -assertType('((Closure(): void)|null)', $bound); +assertType('Closure(): void', $bound); $staticBound = Closure::bind($foo, $newThis); -assertType('((Closure(): void)|null)', $staticBound); +assertType('Closure(): void', $staticBound); $bound2 = $foo->bindTo($newThis, 'stdClass'); -assertType('((Closure(): void)|null)', $bound2); +assertType('Closure(): void', $bound2); $static = static function (): void {}; $boundStatic = $static->bindTo($newThis); @@ -31,7 +31,16 @@ $boundStaticNull = $static->bindTo(null); assertType('Closure(): void', $boundStaticNull); +$staticBound2 = Closure::bind($static, $newThis); +assertType('((Closure(): void)|null)', $staticBound2); + +$staticBoundNull = Closure::bind($static, null); +assertType('Closure(): void', $staticBoundNull); + /** @var \stdClass|null $maybeNull */ $maybeNull = null; $boundMaybe = $foo->bindTo($maybeNull); -assertType('((Closure(): void)|null)', $boundMaybe); +assertType('Closure(): void', $boundMaybe); + +$staticBoundMaybe = $static->bindTo($maybeNull); +assertType('((Closure(): void)|null)', $staticBoundMaybe);