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