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 580595acd74..8b0908dadfb 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -7,8 +7,10 @@ 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; #[AutowiredService] @@ -27,12 +29,21 @@ 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; } - return $closureType; + if ($closureType->isStaticClosure()->no()) { + return $closureType; + } + + 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 3e275677e27..7423c73198d 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -7,8 +7,10 @@ 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; #[AutowiredService] @@ -32,7 +34,16 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - return $closureType; + if ($closureType->isStaticClosure()->no()) { + return $closureType; + } + + $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 new file mode 100644 index 00000000000..691af4c4646 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5009.php @@ -0,0 +1,46 @@ +bindTo(null); +assertType('Closure(): void', $bar); + +$baz = Closure::bind($foo, null); +assertType('Closure(): void', $baz); + +$newThis = new \stdClass(); +$bound = $foo->bindTo($newThis); +assertType('Closure(): void', $bound); + +$staticBound = Closure::bind($foo, $newThis); +assertType('Closure(): void', $staticBound); + +$bound2 = $foo->bindTo($newThis, 'stdClass'); +assertType('Closure(): void', $bound2); + +$static = static function (): void {}; +$boundStatic = $static->bindTo($newThis); +assertType('((Closure(): void)|null)', $boundStatic); + +$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', $boundMaybe); + +$staticBoundMaybe = $static->bindTo($maybeNull); +assertType('((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 1445bfad9f7..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', $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);