diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 1f843048b2..b4da779f33 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2020,7 +2020,7 @@ public function restoreThis(self $restoreThisScope): self $expressionTypes, $nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClasses, + $restoreThisScope->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, [], @@ -2059,6 +2059,31 @@ public function isInClosureBind(): bool return $this->inClosureBindScopeClasses !== []; } + /** + * @param list $scopeClasses + */ + public function withClosureBindScopeClasses(array $scopeClasses): self + { + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $scopeClasses, + $this->anonymousFunctionReflection, + $this->isInFirstLevelStatement(), + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + /** * @api * @param ParameterReflection[]|null $callableParameters @@ -2397,6 +2422,23 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type false, )), new AccessoryArrayListType()]); } + if ( + $type instanceof Name + && $this->inClosureBindScopeClasses !== [] + && $this->inClosureBindScopeClasses !== ['static'] + && in_array($type->toLowerString(), ['static', 'self', 'parent'], true) + && $this->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0]) + ) { + return $this->initializerExprTypeResolver->getFunctionType( + $type, + $isNullable, + false, + InitializerExprContext::fromClassReflection( + $this->reflectionProvider->getClass($this->inClosureBindScopeClasses[0]), + ), + ); + } + return $this->initializerExprTypeResolver->getFunctionType($type, $isNullable, false, InitializerExprContext::fromScope($this)); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 05566f1d6f..72f087b400 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3656,7 +3656,8 @@ public function processArgs( $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); if ($closureThisType !== null) { $restoreThisScope = $scopeToPass; - $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()); + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) + ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); } } @@ -3722,7 +3723,8 @@ public function processArgs( ) { $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); if ($closureThisType !== null) { - $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()); + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) + ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); } } diff --git a/tests/PHPStan/Analyser/nsrt/param-closure-this.php b/tests/PHPStan/Analyser/nsrt/param-closure-this.php index 96f9fb8f15..6486cf0952 100644 --- a/tests/PHPStan/Analyser/nsrt/param-closure-this.php +++ b/tests/PHPStan/Analyser/nsrt/param-closure-this.php @@ -316,3 +316,75 @@ public function doFoo(): void } } + +class FooParent +{ + +} + +class FooChild extends FooParent +{ + + private string $secret = 'secret'; + +} + +class StaticReturnClosureThis +{ + + /** + * @param-closure-this FooChild $cb + */ + public function withFooChild(callable $cb): void + { + + } + + /** + * @param-closure-this Foo $cb + */ + public function withFoo(callable $cb): void + { + + } + +} + +class TestStaticResolutionInParamClosureThis +{ + + public function testStaticReturnType(StaticReturnClosureThis $o): void + { + $o->withFoo(function (): static { + assertType('ParamClosureThis\Foo', $this); + return $this; + }); + + $o->withFoo(fn (): static => $this); + } + + public function testSelfReturnType(StaticReturnClosureThis $o): void + { + $o->withFooChild(function (): self { + assertType('ParamClosureThis\FooChild', $this); + return $this; + }); + } + + public function testParentReturnType(StaticReturnClosureThis $o): void + { + $o->withFooChild(function (): parent { + assertType('ParamClosureThis\FooChild', $this); + return $this; + }); + } + + public function testNullableStaticReturnType(StaticReturnClosureThis $o): void + { + $o->withFoo(function (): ?static { + assertType('ParamClosureThis\Foo', $this); + return rand() ? $this : null; + }); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index eda813a24b..4bb5333aef 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3394,6 +3394,15 @@ public function testClosureBindToParamClosureThis(): void ]); } + public function testBug11010(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-11010.php'], []); + } + public function testPureCallable(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/bug-11010.php b/tests/PHPStan/Rules/Methods/data/bug-11010.php new file mode 100644 index 0000000000..07c621cc96 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11010.php @@ -0,0 +1,28 @@ +bindTo(new self, self::class); + if ($cb) { + $cb(); + } + } +} + +function (): void { + HelloWorld::cb(function () { + $this->sayHello(); + }); +};