mirror of
https://github.com/danog/psalm-plugin-laravel.git
synced 2024-11-26 20:34:48 +01:00
change: resolve abstracts from container. This will allow us to resolve named aliases
This commit is contained in:
parent
6eb856073b
commit
a227c1b87a
73
src/ContainerResolver.php
Normal file
73
src/ContainerResolver.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 = []) {}
|
||||
}
|
@ -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 |
|
||||
|
Loading…
Reference in New Issue
Block a user