change: resolve abstracts from container. This will allow us to resolve named aliases

This commit is contained in:
Feek 2020-07-25 14:47:41 -07:00
parent 6eb856073b
commit a227c1b87a
4 changed files with 128 additions and 30 deletions

73
src/ContainerResolver.php Normal file
View File

@ -0,0 +1,73 @@
<?php declare(strict_types=1);
namespace Psalm\LaravelPlugin;
use Illuminate\Contracts\Container\BindingResolutionException;
use Psalm\NodeTypeProvider;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
use function array_key_exists;
use function get_class;
use function count;
final class ContainerResolver
{
/**
* map of abstract to concrete class fqn
* @var array
* @psalm-var array<string, class-string>
*/
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;
}
}

View File

@ -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);
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace Illuminate\Foundation;
use Illuminate\Contracts\Foundation\Application as ApplicationContract;
use Illuminate\Container\Container;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
/**
* Resolve the given type from the container.
*
* @template T
* @param class-string<T> $abstract
* @param array $parameters
* @return T
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = []) {}
}

View File

@ -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
"""
<?php
function testMake(): \Illuminate\Log\LogManager {
return app()->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
"""
<?php
function testMakeWith(): \Illuminate\Log\LogManager {
return app()->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 |