mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 13:51:54 +01:00
Test for FunctionDynamicStorageProvider
This commit is contained in:
parent
438be03414
commit
3210aab278
190
tests/Config/Plugin/Hook/ArrayMapStorageProvider.php
Normal file
190
tests/Config/Plugin/Hook/ArrayMapStorageProvider.php
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Psalm\Tests\Config\Plugin\Hook;
|
||||||
|
|
||||||
|
use Psalm\Codebase;
|
||||||
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||||
|
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
||||||
|
use Psalm\Internal\Type\TemplateResult;
|
||||||
|
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||||
|
use Psalm\Plugin\EventHandler\Event\FunctionDynamicStorageProviderEvent;
|
||||||
|
use Psalm\Plugin\EventHandler\FunctionDynamicStorageProviderInterface;
|
||||||
|
use Psalm\Storage\FunctionLikeParameter;
|
||||||
|
use Psalm\Storage\FunctionStorage;
|
||||||
|
use Psalm\Type;
|
||||||
|
use Psalm\Type\Atomic\TCallable;
|
||||||
|
use Psalm\Type\Atomic\TTemplateParam;
|
||||||
|
use Psalm\Type\Union;
|
||||||
|
|
||||||
|
use function array_keys;
|
||||||
|
use function array_map;
|
||||||
|
use function count;
|
||||||
|
|
||||||
|
class ArrayMapStorageProvider implements FunctionDynamicStorageProviderInterface
|
||||||
|
{
|
||||||
|
public static function getFunctionIds(): array
|
||||||
|
{
|
||||||
|
return ['custom_array_map'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getFunctionStorage(FunctionDynamicStorageProviderEvent $event): ?FunctionStorage
|
||||||
|
{
|
||||||
|
$context = $event->getContext();
|
||||||
|
$statements_analyzer = $event->getStatementsAnalyzer();
|
||||||
|
$call_args = $event->getCallArgs();
|
||||||
|
$args_count = count($call_args);
|
||||||
|
$expected_callable_args_count = $args_count - 1;
|
||||||
|
|
||||||
|
if ($expected_callable_args_count < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$last_array_arg = $call_args[$args_count - 1];
|
||||||
|
|
||||||
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $last_array_arg->value, $context) === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$input_array_type = $statements_analyzer->node_data->getType($last_array_arg->value);
|
||||||
|
|
||||||
|
if (!$input_array_type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$input_value_type = self::getInputValueType($statements_analyzer->getCodebase(), $input_array_type);
|
||||||
|
|
||||||
|
$all_expected_callables = [
|
||||||
|
self::createExpectedCallable($input_value_type),
|
||||||
|
...self::createRestCallables($expected_callable_args_count),
|
||||||
|
];
|
||||||
|
|
||||||
|
$custom_array_map_storage = new FunctionStorage();
|
||||||
|
$custom_array_map_storage->cased_name = 'custom_array_map';
|
||||||
|
$custom_array_map_storage->template_types = self::createTemplates($expected_callable_args_count);
|
||||||
|
$custom_array_map_storage->return_type = self::getReturnType($all_expected_callables);
|
||||||
|
|
||||||
|
$input_array_param = new FunctionLikeParameter('input', false, $input_array_type);
|
||||||
|
$input_array_param->is_optional = false;
|
||||||
|
|
||||||
|
$custom_array_map_storage->setParams(
|
||||||
|
[
|
||||||
|
...array_map(
|
||||||
|
function (TCallable $expected, int $offset) {
|
||||||
|
$param = new FunctionLikeParameter('fn' . $offset, false, new Union([$expected]));
|
||||||
|
$param->is_optional = false;
|
||||||
|
|
||||||
|
return $param;
|
||||||
|
},
|
||||||
|
$all_expected_callables,
|
||||||
|
array_keys($all_expected_callables)
|
||||||
|
),
|
||||||
|
$input_array_param
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $custom_array_map_storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve value type from array-like type:
|
||||||
|
* list<int> -> int
|
||||||
|
* list<int|string> -> int|string
|
||||||
|
*/
|
||||||
|
private static function getInputValueType(Codebase $codebase, Union $array_like_type): Union
|
||||||
|
{
|
||||||
|
$input_template = self::createTemplate('TIn');
|
||||||
|
|
||||||
|
// Template type that will be inferred via TemplateInferredTypeReplacer
|
||||||
|
$value_type = new Union([$input_template]);
|
||||||
|
|
||||||
|
$templated_array = new Union([
|
||||||
|
new Type\Atomic\TArray([Type::getArrayKey(), $value_type])
|
||||||
|
]);
|
||||||
|
|
||||||
|
$template_result = new TemplateResult(
|
||||||
|
[
|
||||||
|
$input_template->param_name => [
|
||||||
|
$input_template->defining_class => new Union([$input_template])
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
TemplateStandinTypeReplacer::replace(
|
||||||
|
$templated_array,
|
||||||
|
$template_result,
|
||||||
|
$codebase,
|
||||||
|
null,
|
||||||
|
$array_like_type
|
||||||
|
);
|
||||||
|
|
||||||
|
TemplateInferredTypeReplacer::replace($templated_array, $template_result, $codebase);
|
||||||
|
|
||||||
|
return $value_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function createExpectedCallable(Union $input_type, int $return_template_offset = 0): TCallable
|
||||||
|
{
|
||||||
|
$first_expected_callable = new TCallable('callable');
|
||||||
|
$first_expected_callable->params = [new FunctionLikeParameter('a', false, $input_type)];
|
||||||
|
$first_expected_callable->return_type = self::createTemplateType($return_template_offset);
|
||||||
|
|
||||||
|
return $first_expected_callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<TCallable>
|
||||||
|
*/
|
||||||
|
private static function createRestCallables(int $expected_callable_args_count): array
|
||||||
|
{
|
||||||
|
$rest_callable_params = [];
|
||||||
|
|
||||||
|
for ($template_offset = 0; $template_offset < $expected_callable_args_count - 1; $template_offset++) {
|
||||||
|
$next_template_type = self::createTemplateType($template_offset);
|
||||||
|
$rest_callable_params[] = self::createExpectedCallable($next_template_type, $template_offset + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rest_callable_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<TCallable> $all_expected_callables
|
||||||
|
*/
|
||||||
|
private static function getReturnType(array $all_expected_callables): Union
|
||||||
|
{
|
||||||
|
$last_callable_arg = $all_expected_callables[count($all_expected_callables) - 1];
|
||||||
|
|
||||||
|
return new Union([
|
||||||
|
new Type\Atomic\TList($last_callable_arg->return_type ?? Type::getMixed())
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param positive-int $expected_callable_count
|
||||||
|
* @return array<string, non-empty-array<string, Union>>
|
||||||
|
*/
|
||||||
|
private static function createTemplates(int $expected_callable_count): array
|
||||||
|
{
|
||||||
|
$template_params = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $expected_callable_count; $i++) {
|
||||||
|
$template = self::createTemplate('T', $i);
|
||||||
|
|
||||||
|
$template_params[$template->param_name] = [
|
||||||
|
$template->defining_class => new Union([$template])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $template_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function createTemplateType(int $offset = 0): Union
|
||||||
|
{
|
||||||
|
return new Union([self::createTemplate('T', $offset)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function createTemplate(string $prefix, int $offset = 0): TTemplateParam
|
||||||
|
{
|
||||||
|
return new TTemplateParam($prefix . $offset, Type::getMixed(), 'custom_array_map');
|
||||||
|
}
|
||||||
|
}
|
19
tests/Config/Plugin/StoragePlugin.php
Normal file
19
tests/Config/Plugin/StoragePlugin.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Psalm\Tests\Config\Plugin;
|
||||||
|
|
||||||
|
use Psalm\Plugin\PluginEntryPointInterface;
|
||||||
|
use Psalm\Plugin\RegistrationInterface;
|
||||||
|
use Psalm\Tests\Config\Plugin\Hook\ArrayMapStorageProvider;
|
||||||
|
use SimpleXMLElement;
|
||||||
|
|
||||||
|
/** @psalm-suppress UnusedClass */
|
||||||
|
class StoragePlugin implements PluginEntryPointInterface
|
||||||
|
{
|
||||||
|
public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
|
||||||
|
{
|
||||||
|
require_once __DIR__ . '/Hook/ArrayMapStorageProvider.php';
|
||||||
|
|
||||||
|
$registration->registerHooksFromClass(ArrayMapStorageProvider::class);
|
||||||
|
}
|
||||||
|
}
|
@ -1022,4 +1022,58 @@ class PluginTest extends TestCase
|
|||||||
|
|
||||||
$this->analyzeFile($file_path, new Context());
|
$this->analyzeFile($file_path, new Context());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFunctionDynamicStorageProviderHook(): void
|
||||||
|
{
|
||||||
|
require_once __DIR__ . '/Plugin/StoragePlugin.php';
|
||||||
|
|
||||||
|
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
|
||||||
|
TestConfig::loadFromXML(
|
||||||
|
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR,
|
||||||
|
'<?xml version="1.0"?>
|
||||||
|
<psalm
|
||||||
|
errorLevel="1"
|
||||||
|
>
|
||||||
|
<projectFiles>
|
||||||
|
<directory name="src" />
|
||||||
|
</projectFiles>
|
||||||
|
<plugins>
|
||||||
|
<pluginClass class="Psalm\\Tests\\Config\\Plugin\\StoragePlugin" />
|
||||||
|
</plugins>
|
||||||
|
</psalm>'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
|
||||||
|
|
||||||
|
$file_path = getcwd() . '/src/somefile.php';
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
$file_path,
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @param mixed ...$_args
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
function custom_array_map(...$_args) { throw new RuntimeException("???"); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{num: int}> $_list
|
||||||
|
*/
|
||||||
|
function acceptsList(array $_list): void { }
|
||||||
|
|
||||||
|
/** @var list<int> $list */
|
||||||
|
$list = [1, 2, 3];
|
||||||
|
|
||||||
|
$tuples = custom_array_map(
|
||||||
|
fn($a) => $a + 1,
|
||||||
|
fn($a) => ["num" => $a],
|
||||||
|
$list
|
||||||
|
);
|
||||||
|
|
||||||
|
acceptsList($tuples);'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->analyzeFile($file_path, new Context());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user