diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 580595acd7..1234067633 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -7,8 +7,11 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; +use PHPStan\TrinaryLogic; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; +use PHPStan\Type\NullType; use PHPStan\Type\Type; #[AutowiredService] @@ -27,12 +30,29 @@ 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(); + if (!isset($args[0])) { + return null; + } + + $closureType = $scope->getType($args[0]->value); if (!($closureType instanceof ClosureType)) { return null; } - return $closureType; + if ($closureType->isStaticClosure()->yes()) { + $newThisIsNull = isset($args[1]) ? $scope->getType($args[1]->value)->isNull() : TrinaryLogic::createYes(); + if ($newThisIsNull->yes()) { + return $closureType; + } + if ($newThisIsNull->no()) { + return new NullType(); + } + + return new BenevolentUnionType([$closureType, new NullType()]); + } + + return new BenevolentUnionType([$closureType, new NullType()]); } } diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index 3e275677e2..dc9aaf33a6 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -7,8 +7,11 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; +use PHPStan\TrinaryLogic; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClosureType; use PHPStan\Type\DynamicMethodReturnTypeExtension; +use PHPStan\Type\NullType; use PHPStan\Type\Type; #[AutowiredService] @@ -32,7 +35,20 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - return $closureType; + if ($closureType->isStaticClosure()->yes()) { + $args = $methodCall->getArgs(); + $newThisIsNull = isset($args[0]) ? $scope->getType($args[0]->value)->isNull() : TrinaryLogic::createYes(); + if ($newThisIsNull->yes()) { + return $closureType; + } + if ($newThisIsNull->no()) { + return new NullType(); + } + + return new BenevolentUnionType([$closureType, new NullType()]); + } + + return new BenevolentUnionType([$closureType, new NullType()]); } } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 418b57601d..d6e9e7e38c 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -473,13 +473,14 @@ public function testBug4734(): void { // false positive $errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php'); - $this->assertCount(5, $errors); // could be 3 + $this->assertCount(6, $errors); // could be 4 $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 null but it\'s not a callable.', $errors[3]->getMessage()); // binding a static closure to an object returns null + $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[4]->getMessage()); + $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[5]->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 0000000000..d72aee74c6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5009.php @@ -0,0 +1,46 @@ +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); + +$static = static function (): void {}; +$boundStatic = $static->bindTo($newThis); +assertType('null', $boundStatic); + +$boundStaticNull = $static->bindTo(null); +assertType('static-Closure(): void', $boundStaticNull); + +$staticBound2 = Closure::bind($static, $newThis); +assertType('null', $staticBound2); + +$staticBoundNull = Closure::bind($static, null); +assertType('static-Closure(): void', $staticBoundNull); + +/** @var \stdClass|null $maybeNull */ +$maybeNull = null; +$boundMaybe = $foo->bindTo($maybeNull); +assertType('((Closure(): void)|null)', $boundMaybe); + +$staticBoundMaybe = $static->bindTo($maybeNull); +assertType('((static-Closure(): void)|null)', $staticBoundMaybe); diff --git a/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/closure-return-type-extensions.php index 1445bfad9f..eb21b2fb97 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); diff --git a/tests/PHPStan/Analyser/nsrt/closure-static-type.php b/tests/PHPStan/Analyser/nsrt/closure-static-type.php index 6b84bd967d..2b971fbccc 100644 --- a/tests/PHPStan/Analyser/nsrt/closure-static-type.php +++ b/tests/PHPStan/Analyser/nsrt/closure-static-type.php @@ -26,19 +26,19 @@ public function doFoo(): void public function doBindTo(): void { $static = static function (): void {}; - assertType('static-Closure(): void', $static->bindTo($this)); + assertType('null', $static->bindTo($this)); $nonStatic = function (): void {}; - assertType('Closure(): void', $nonStatic->bindTo($this)); + assertType('((Closure(): void)|null)', $nonStatic->bindTo($this)); } public function doBind(): void { $static = static function (): void {}; - assertType('static-Closure(): void', Closure::bind($static, $this)); + assertType('null', Closure::bind($static, $this)); $nonStatic = function (): void {}; - assertType('Closure(): void', Closure::bind($nonStatic, $this)); + assertType('((Closure(): void)|null)', Closure::bind($nonStatic, $this)); } /** @@ -46,17 +46,17 @@ public function doBind(): void */ public function doUnknown(Closure $unknownClosure): void { - assertType('Closure(): void', $unknownClosure->bindTo($this)); - assertType('Closure(): void', Closure::bind($unknownClosure, $this)); + assertType('((Closure(): void)|null)', $unknownClosure->bindTo($this)); + assertType('((Closure(): void)|null)', Closure::bind($unknownClosure, $this)); } public function doFromCallable(): void { $fn = Closure::fromCallable(static function (): void {}); - assertType('static-Closure(): void', $fn->bindTo($this)); + assertType('null', $fn->bindTo($this)); $fn2 = Closure::fromCallable(function (): void {}); - assertType('Closure(): void', $fn2->bindTo($this)); + assertType('((Closure(): void)|null)', $fn2->bindTo($this)); } } diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index b8ed61a1d9..e22565393f 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -244,6 +244,11 @@ public function testNullCoalesceInGlobalScope(): void ]); } + public function testBug5009(): void + { + $this->analyse([__DIR__ . '/data/bug-5009.php'], []); + } + public function testBug5933(): void { $this->analyse([__DIR__ . '/data/bug-5933.php'], []); diff --git a/tests/PHPStan/Rules/Variables/data/bug-5009.php b/tests/PHPStan/Rules/Variables/data/bug-5009.php new file mode 100644 index 0000000000..a7bf271458 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-5009.php @@ -0,0 +1,18 @@ +callback = $callback->bindTo($this, $this) ?? $callback; + } +}