diff --git a/extension.neon b/extension.neon index fdfbfdcf..af69d8bf 100644 --- a/extension.neon +++ b/extension.neon @@ -336,3 +336,8 @@ services: tags: - phpstan.properties.readWriteExtension - phpstan.additionalConstructorsExtension + + # CacheInterface::get() return type + - + factory: PHPStan\Type\Symfony\CacheInterfaceGetDynamicReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] diff --git a/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php b/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php new file mode 100644 index 00000000..ce736bfe --- /dev/null +++ b/src/Type/Symfony/CacheInterfaceGetDynamicReturnTypeExtension.php @@ -0,0 +1,48 @@ +getName() === 'get'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (!isset($methodCall->getArgs()[1])) { + return null; + } + + $callbackReturnType = $scope->getType($methodCall->getArgs()[1]->value); + if ($callbackReturnType->isCallable()->yes()) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $callbackReturnType->getCallableParametersAcceptors($scope) + ); + $returnType = $parametersAcceptor->getReturnType(); + + // generalize template parameters + return $returnType->generalize(GeneralizePrecision::templateArgument()); + } + + return null; + } + +} diff --git a/tests/Type/Symfony/data/cache.php b/tests/Type/Symfony/data/cache.php index 6b0728d4..a8862177 100644 --- a/tests/Type/Symfony/data/cache.php +++ b/tests/Type/Symfony/data/cache.php @@ -12,6 +12,26 @@ function testCacheCallable(\Symfony\Contracts\Cache\CacheInterface $cache): voi assertType('string', $result); }; +/** + * @param callable():string $fn + */ +function testNonScalarCacheCallable(\Symfony\Contracts\Cache\CacheInterface $cache, callable $fn): void { + $result = $cache->get('foo', $fn); + + assertType('string', $result); +}; + + +/** + * @param callable():non-empty-string $fn + */ +function testCacheCallableReturnTypeGeneralization(\Symfony\Contracts\Cache\CacheInterface $cache, callable $fn): void { + $result = $cache->get('foo', $fn); + + assertType('string', $result); +}; + + /** * @param \Symfony\Contracts\Cache\CallbackInterface<\stdClass> $cb */