diff --git a/src/ContainerResolver.php b/src/ContainerResolver.php new file mode 100644 index 0000000..71ac89a --- /dev/null +++ b/src/ContainerResolver.php @@ -0,0 +1,73 @@ + + */ + private static $cache = []; + + /** + * @psalm-return class-string|null + */ + public static function resolveFromApplicationContainer(string $abstract): ?string + { + if (array_key_exists($abstract, static::$cache)) { + return static::$cache[$abstract]; + } + + // dynamic analysis to resolve the actual type from the container + try { + $concrete = ApplicationHelper::getApp()->make($abstract); + } catch (BindingResolutionException $e) { + return null; + } + + $concreteClass = get_class($concrete); + + if (!$concreteClass) { + return null; + } + + static::$cache[$abstract] = $concreteClass; + + return $concreteClass; + } + + /** + * @param array<\PhpParser\Node\Arg> $call_args + */ + public static function resolvePsalmTypeFromApplicationContainerViaArgs(NodeTypeProvider $nodeTypeProvider, array $call_args): ?Union + { + if (! count($call_args)) { + return null; + } + + $firstArgType = $nodeTypeProvider->getType($call_args[0]->value); + + if ($firstArgType && $firstArgType->isSingleStringLiteral()) { + $abstract = $firstArgType->getSingleStringLiteral()->value; + $concreteClass = static::resolveFromApplicationContainer($abstract); + if ($concreteClass) { + return new Union([ + new TNamedObject($concreteClass), + ]); + } + } + + return null; + } +} diff --git a/src/ReturnTypeProvider/AppReturnTypeProvider.php b/src/ReturnTypeProvider/AppReturnTypeProvider.php index ed373ea..b52a9aa 100644 --- a/src/ReturnTypeProvider/AppReturnTypeProvider.php +++ b/src/ReturnTypeProvider/AppReturnTypeProvider.php @@ -4,15 +4,21 @@ namespace Psalm\LaravelPlugin\ReturnTypeProvider; use Psalm\CodeLocation; use Psalm\Context; +use Psalm\Internal\MethodIdentifier; use Psalm\LaravelPlugin\ApplicationHelper; +use Psalm\LaravelPlugin\ContainerResolver; use Psalm\Plugin\Hook\FunctionReturnTypeProviderInterface; +use Psalm\Plugin\Hook\MethodReturnTypeProviderInterface; use Psalm\StatementsSource; use Psalm\Type; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; use function get_class; +use function array_filter; +use function array_map; +use function in_array; -final class AppReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class AppReturnTypeProvider implements FunctionReturnTypeProviderInterface, MethodReturnTypeProviderInterface { /** @@ -34,16 +40,27 @@ final class AppReturnTypeProvider implements FunctionReturnTypeProviderInterface ]); } - // @todo: this should really proxy to \Illuminate\Foundation\Application::make, but i was struggling with that + return ContainerResolver::resolvePsalmTypeFromApplicationContainerViaArgs($statements_source->getNodeTypeProvider(), $call_args) ?? Type::getMixed(); + } - $firstArgType = $statements_source->getNodeTypeProvider()->getType($call_args[0]->value); + public static function getClassLikeNames(): array + { + return [get_class(ApplicationHelper::getApp())]; + } - if ($firstArgType && $firstArgType->isSingleStringLiteral()) { - return new Union([ - new TNamedObject($firstArgType->getSingleStringLiteral()->value), - ]); + public static function getMethodReturnType(StatementsSource $source, string $fq_classlike_name, string $method_name_lowercase, array $call_args, Context $context, CodeLocation $code_location, array $template_type_parameters = null, string $called_fq_classlike_name = null, string $called_method_name_lowercase = null) + { + // lumen doesn't have the likes of makeWith, so we will ensure these methods actually exist on the underlying + // app contract + $methods = array_filter(['make', 'makewith'], function (string $methodName) use ($source, $fq_classlike_name) { + $methodId = new MethodIdentifier($fq_classlike_name, $methodName); + return $source->getCodebase()->methodExists($methodId); + }); + + if (!in_array($method_name_lowercase, $methods)) { + return null; } - return Type::getMixed(); + return ContainerResolver::resolvePsalmTypeFromApplicationContainerViaArgs($source->getNodeTypeProvider(), $call_args); } } diff --git a/src/Stubs/Application.stubphp b/src/Stubs/Application.stubphp deleted file mode 100644 index 62138d0..0000000 --- a/src/Stubs/Application.stubphp +++ /dev/null @@ -1,22 +0,0 @@ - $abstract - * @param array $parameters - * @return T - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ - public function make($abstract, array $parameters = []) {} -} diff --git a/tests/acceptance/Container.feature b/tests/acceptance/Container.feature index e77e619..6b04c54 100644 --- a/tests/acceptance/Container.feature +++ b/tests/acceptance/Container.feature @@ -78,3 +78,33 @@ Feature: Container """ When I run Psalm Then I see no errors + + Scenario: container can resolve aliases + Given I have the following code + """ + make('log'); + } + + function testMakeWith(): \Illuminate\Log\LogManager { + return app()->makeWith('log'); + } + """ + When I run Psalm + Then I see no errors + + Scenario: container cannot resolve unknown aliases + Given I have the following code + """ + makeWith('logg'); + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | InvalidReturnType | The declared return type 'Illuminate\Log\LogManager' for testMakeWith is incorrect, got 'logg' | + | InvalidReturnStatement | The inferred type 'logg' does not match the declared return type 'Illuminate\Log\LogManager' for testMakeWith |