diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 63741b2c04..e6f316d752 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -67,6 +67,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; @@ -272,6 +273,7 @@ public function __construct( private readonly TypeSpecifier $typeSpecifier, private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider, + private readonly ParameterClosureThisExtensionProvider $parameterClosureThisExtensionProvider, private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider, private readonly ScopeFactory $scopeFactory, #[AutowiredParameter] @@ -5060,6 +5062,55 @@ private function processPropertyHooks( } } + /** + * @param FunctionReflection|MethodReflection|null $calleeReflection + */ + public function resolveClosureThisType( + ?CallLike $call, + $calleeReflection, + ParameterReflection $parameter, + MutatingScope $scope, + ): ?Type + { + if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->parameterClosureThisExtensionProvider->getFunctionParameterClosureThisExtensions() as $extension) { + if (! $extension->isFunctionSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->parameterClosureThisExtensionProvider->getStaticMethodParameterClosureThisExtensions() as $extension) { + if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->parameterClosureThisExtensionProvider->getMethodParameterClosureThisExtensions() as $extension) { + if (! $extension->isMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } + + if ($parameter instanceof ExtendedParameterReflection) { + return $parameter->getClosureThisType(); + } + + return null; + } + /** * @param MethodReflection|FunctionReflection|null $calleeReflection * @param callable(Node $node, Scope $scope): void $nodeCallback @@ -5163,11 +5214,13 @@ private function processArgs( if ( $closureBindScope === null && $parameter instanceof ExtendedParameterReflection - && $parameter->getClosureThisType() !== null && !$arg->value->static ) { - $restoreThisScope = $scopeToPass; - $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); + $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($closureThisType !== null) { + $restoreThisScope = $scopeToPass; + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()); + } } if ($parameter !== null) { @@ -5217,10 +5270,12 @@ private function processArgs( if ( $closureBindScope === null && $parameter instanceof ExtendedParameterReflection - && $parameter->getClosureThisType() !== null && !$arg->value->static ) { - $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); + $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($closureThisType !== null) { + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()); + } } if ($parameter !== null) { diff --git a/src/DependencyInjection/Type/LazyParameterClosureThisExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterClosureThisExtensionProvider.php new file mode 100644 index 0000000000..915fd3d621 --- /dev/null +++ b/src/DependencyInjection/Type/LazyParameterClosureThisExtensionProvider.php @@ -0,0 +1,47 @@ +functionExtensions ??= $this->container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getMethodParameterClosureThisExtensions(): array + { + return $this->methodExtensions ??= $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getStaticMethodParameterClosureThisExtensions(): array + { + return $this->staticMethodExtensions ??= $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} diff --git a/src/DependencyInjection/Type/ParameterClosureThisExtensionProvider.php b/src/DependencyInjection/Type/ParameterClosureThisExtensionProvider.php new file mode 100644 index 0000000000..9528a0a764 --- /dev/null +++ b/src/DependencyInjection/Type/ParameterClosureThisExtensionProvider.php @@ -0,0 +1,27 @@ + LazyDynamicThrowTypeExtensionProvider::FUNCTION_TAG, DynamicMethodThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::METHOD_TAG, DynamicStaticMethodThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::STATIC_METHOD_TAG, + FunctionParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::FUNCTION_TAG, + MethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::METHOD_TAG, + StaticMethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::STATIC_METHOD_TAG, FunctionParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::FUNCTION_TAG, MethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::METHOD_TAG, StaticMethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::STATIC_METHOD_TAG, diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 36c383bc56..47c8a997e9 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -17,6 +17,7 @@ use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; @@ -105,6 +106,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), $readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), $this->shouldPolluteScopeWithLoopInitialAssignments(), diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 7c50ebaf5c..be8065f52a 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Analyser\ScopeContext; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; +use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; use PHPStan\File\FileHelper; @@ -83,6 +84,7 @@ public static function processFile( $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'), diff --git a/src/Type/FunctionParameterClosureThisExtension.php b/src/Type/FunctionParameterClosureThisExtension.php new file mode 100644 index 0000000000..92d64d3a96 --- /dev/null +++ b/src/Type/FunctionParameterClosureThisExtension.php @@ -0,0 +1,33 @@ +getByType(DynamicThrowTypeExtensionProvider::class), self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), false, diff --git a/tests/PHPStan/Analyser/ParameterClosureThisExtensionTest.php b/tests/PHPStan/Analyser/ParameterClosureThisExtensionTest.php new file mode 100644 index 0000000000..fed715244f --- /dev/null +++ b/tests/PHPStan/Analyser/ParameterClosureThisExtensionTest.php @@ -0,0 +1,32 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/parameter-closure-this-extension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/parameter-closure-this-extension.php b/tests/PHPStan/Analyser/data/parameter-closure-this-extension.php new file mode 100644 index 0000000000..31660060ef --- /dev/null +++ b/tests/PHPStan/Analyser/data/parameter-closure-this-extension.php @@ -0,0 +1,108 @@ +getName() === 'ParameterClosureThisExtension\testFunction' + && $parameter->getName() === 'closure'; + } + + public function getClosureThisTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new ObjectType(TestContext::class); + } +} + +class TestMethodParameterClosureThisExtension implements \PHPStan\Type\MethodParameterClosureThisExtension +{ + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === TestClass::class + && $methodReflection->getName() === 'methodWithClosure' + && $parameter->getName() === 'closure'; + } + + public function getClosureThisTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new ObjectType(TestContext::class); + } +} + +class TestStaticMethodParameterClosureThisExtension implements \PHPStan\Type\StaticMethodParameterClosureThisExtension +{ + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === TestClass::class + && $methodReflection->getName() === 'staticMethodWithClosure' + && $parameter->getName() === 'closure'; + } + + public function getClosureThisTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new ObjectType(TestContext::class); + } +} + +class TestContext +{ + public function contextMethod(): string + { + return 'context'; + } +} + +class TestClass +{ + public function methodWithClosure(callable $closure): void + { + } + + public static function staticMethodWithClosure(callable $closure): void + { + } +} + +/** + * @param callable $closure + */ +function testFunction(callable $closure): void +{ +} + +testFunction(function () { + assertType('ParameterClosureThisExtension\TestContext', $this); + assertType('string', $this->contextMethod()); +}); + +$test = new TestClass(); +$test->methodWithClosure(function () { + assertType('ParameterClosureThisExtension\TestContext', $this); + assertType('string', $this->contextMethod()); +}); + +TestClass::staticMethodWithClosure(function () { + assertType('ParameterClosureThisExtension\TestContext', $this); + assertType('string', $this->contextMethod()); +}); + +testFunction(fn () => assertType('ParameterClosureThisExtension\TestContext', $this)); + +testFunction(static function () { + assertType('*ERROR*', $this); +}); + +testFunction(static fn () => assertType('*ERROR*', $this)); diff --git a/tests/PHPStan/Analyser/parameter-closure-this-extension.neon b/tests/PHPStan/Analyser/parameter-closure-this-extension.neon new file mode 100644 index 0000000000..2ee57c9f19 --- /dev/null +++ b/tests/PHPStan/Analyser/parameter-closure-this-extension.neon @@ -0,0 +1,13 @@ +services: + - + class: ParameterClosureThisExtension\TestFunctionParameterClosureThisExtension + tags: + - phpstan.functionParameterClosureThisExtension + - + class: ParameterClosureThisExtension\TestMethodParameterClosureThisExtension + tags: + - phpstan.methodParameterClosureThisExtension + - + class: ParameterClosureThisExtension\TestStaticMethodParameterClosureThisExtension + tags: + - phpstan.staticMethodParameterClosureThisExtension