From 438be03414887899f5fa7c00783030881de6e1bf Mon Sep 17 00:00:00 2001 From: adrew Date: Sun, 23 Jan 2022 21:25:36 +0300 Subject: [PATCH 01/18] Ability to provide dynamically created function storage via plugin hook --- .../Expression/Call/ArgumentsAnalyzer.php | 15 ++- .../Expression/Call/FunctionCallAnalyzer.php | 76 ++++++++++----- src/Psalm/Internal/Codebase/Functions.php | 5 + .../FunctionDynamicStorageProvider.php | 94 +++++++++++++++++++ .../FunctionDynamicStorageProviderEvent.php | 65 +++++++++++++ ...unctionDynamicStorageProviderInterface.php | 18 ++++ src/Psalm/PluginRegistrationSocket.php | 5 + 7 files changed, 251 insertions(+), 27 deletions(-) create mode 100644 src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php create mode 100644 src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php create mode 100644 src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 0b8e84cdd09..6f2b1082274 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -346,13 +346,26 @@ private static function getHighOrderFuncStorage( $codebase = $statements_analyzer->getCodebase(); try { - if ($function_like_call instanceof PhpParser\Node\Expr\FuncCall) { + if ($function_like_call instanceof PhpParser\Node\Expr\FuncCall && + !$function_like_call->isFirstClassCallable() + ) { $function_id = strtolower((string) $function_like_call->name->getAttribute('resolvedName')); if (empty($function_id)) { return null; } + if ($codebase->functions->dynamic_storage_provider->has($function_id)) { + return $codebase->functions->dynamic_storage_provider->getFunctionSignature( + $function_like_call, + $statements_analyzer, + $function_id, + $function_like_call->getArgs(), + $context, + new CodeLocation($statements_analyzer, $function_like_call), + ); + } + return $codebase->functions->getStorage($statements_analyzer, $function_id); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index ce2198ac666..204ee372e8e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -526,39 +526,63 @@ private static function handleNamedFunction( $function_call_info->defined_constants = []; $function_call_info->global_variables = []; $args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs(); + $has_dynamic_storage = false; + + if ($codebase->functions->dynamic_storage_provider->has($function_call_info->function_id)) { + $dynamic_function_storage = $codebase->functions->dynamic_storage_provider->getFunctionSignature( + $stmt, + $statements_analyzer, + $function_call_info->function_id, + $args, + $context, + $code_location + ); + + if ($dynamic_function_storage) { + $has_dynamic_storage = true; + + $function_call_info->function_storage = $dynamic_function_storage; + $function_call_info->function_params = $function_call_info->function_storage->params; + $function_call_info->allow_named_args = $dynamic_function_storage->allow_named_arg_calls; + $function_call_info->defined_constants = $dynamic_function_storage->defined_constants; + $function_call_info->global_variables = $dynamic_function_storage->global_variables; + } + } if ($function_call_info->function_exists) { - if (!$function_call_info->in_call_map || $function_call_info->is_stubbed) { - try { - $function_call_info->function_storage = $function_storage = $codebase_functions->getStorage( - $statements_analyzer, - strtolower($function_call_info->function_id) - ); + if (!$has_dynamic_storage) { + if (!$function_call_info->in_call_map || $function_call_info->is_stubbed) { + try { + $function_call_info->function_storage = $function_storage = $codebase_functions->getStorage( + $statements_analyzer, + strtolower($function_call_info->function_id) + ); - $function_call_info->function_params = $function_call_info->function_storage->params; + $function_call_info->function_params = $function_call_info->function_storage->params; - if (!$function_storage->allow_named_arg_calls) { - $function_call_info->allow_named_args = false; - } + if (!$function_storage->allow_named_arg_calls) { + $function_call_info->allow_named_args = false; + } - if (!$is_predefined) { - $function_call_info->defined_constants = $function_storage->defined_constants; - $function_call_info->global_variables = $function_storage->global_variables; + if (!$is_predefined) { + $function_call_info->defined_constants = $function_storage->defined_constants; + $function_call_info->global_variables = $function_storage->global_variables; + } + } catch (UnexpectedValueException $e) { + $function_call_info->function_params = [ + new FunctionLikeParameter('args', false, null, null, null, false, false, true) + ]; } - } catch (UnexpectedValueException $e) { - $function_call_info->function_params = [ - new FunctionLikeParameter('args', false, null, null, null, false, false, true) - ]; - } - } else { - $function_callable = InternalCallMapHandler::getCallableFromCallMapById( - $codebase, - $function_call_info->function_id, - $args, - $statements_analyzer->node_data - ); + } else { + $function_callable = InternalCallMapHandler::getCallableFromCallMapById( + $codebase, + $function_call_info->function_id, + $args, + $statements_analyzer->node_data + ); - $function_call_info->function_params = $function_callable->params; + $function_call_info->function_params = $function_callable->params; + } } if ($codebase->functions->params_provider->has($function_call_info->function_id)) { diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index c29ce9815fd..1ec430be897 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -12,6 +12,7 @@ use Psalm\Internal\Provider\FunctionExistenceProvider; use Psalm\Internal\Provider\FunctionParamsProvider; use Psalm\Internal\Provider\FunctionReturnTypeProvider; +use Psalm\Internal\Provider\FunctionDynamicStorageProvider; use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\NodeTypeProvider; use Psalm\StatementsSource; @@ -55,6 +56,9 @@ class Functions /** @var FunctionParamsProvider */ public $params_provider; + /** @var FunctionDynamicStorageProvider */ + public $dynamic_storage_provider; + /** * @var Reflection */ @@ -67,6 +71,7 @@ public function __construct(FileStorageProvider $storage_provider, Reflection $r $this->return_type_provider = new FunctionReturnTypeProvider(); $this->existence_provider = new FunctionExistenceProvider(); $this->params_provider = new FunctionParamsProvider(); + $this->dynamic_storage_provider = new FunctionDynamicStorageProvider(); self::$stubbed_functions = []; } diff --git a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php new file mode 100644 index 00000000000..556ca7d5db0 --- /dev/null +++ b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php @@ -0,0 +1,94 @@ +> */ + private static $handlers = []; + + /** @var array */ + private static $dynamic_storages = []; + + /** + * @param class-string $class + */ + public function registerClass(string $class): void + { + $callable = Closure::fromCallable([$class, 'getFunctionStorage']); + + foreach ($class::getFunctionIds() as $function_id) { + $this->registerClosure($function_id, $callable); + } + } + + /** + * @param Closure(FunctionDynamicStorageProviderEvent): ?FunctionStorage $c + */ + public function registerClosure(string $fq_function_name, Closure $c): void + { + self::$handlers[strtolower($fq_function_name)][] = $c; + } + + public function has(string $fq_function_name): bool + { + return isset(self::$handlers[strtolower($fq_function_name)]); + } + + /** + * @param list $call_args + */ + public function getFunctionSignature( + PhpParser\Node\Expr\FuncCall $stmt, + StatementsAnalyzer $statements_analyzer, + string $function_id, + array $call_args, + Context $context, + CodeLocation $code_location + ): ?FunctionStorage { + $dynamic_storage_id = strtolower($statements_analyzer->getFilePath()) + . ':' . $stmt->getLine() + . ':' . (int)$stmt->getAttribute('startFilePos') + . ':dynamic-storage' + . ':-:' . strtolower($function_id); + + if (isset(self::$dynamic_storages[$dynamic_storage_id])) { + return self::$dynamic_storages[$dynamic_storage_id]; + } + + foreach (self::$handlers[strtolower($function_id)] ?? [] as $class_handler) { + $event = new FunctionDynamicStorageProviderEvent( + $statements_analyzer, + $function_id, + $call_args, + $context, + $code_location + ); + + $result = $class_handler($event); + + if ($result) { + self::$dynamic_storages[$dynamic_storage_id] = $result; + return $result; + } + } + + return null; + } +} diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php new file mode 100644 index 00000000000..dad62a0968f --- /dev/null +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php @@ -0,0 +1,65 @@ + */ + private array $call_args; + private Context $context; + private CodeLocation $code_location; + + /** + * @param list $call_args + */ + public function __construct( + StatementsAnalyzer $statements_analyzer, + string $function_id, + array $call_args, + Context $context, + CodeLocation $code_location + ) { + $this->statements_analyzer = $statements_analyzer; + $this->function_id = $function_id; + $this->call_args = $call_args; + $this->context = $context; + $this->code_location = $code_location; + } + + public function getStatementsAnalyzer(): StatementsAnalyzer + { + return $this->statements_analyzer; + } + + public function getFunctionId(): string + { + return $this->function_id; + } + + /** + * @return list + */ + public function getCallArgs(): array + { + return $this->call_args; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getCodeLocation(): CodeLocation + { + return $this->code_location; + } +} diff --git a/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php b/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php new file mode 100644 index 00000000000..89efb006a53 --- /dev/null +++ b/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php @@ -0,0 +1,18 @@ + + */ + public static function getFunctionIds(): array; + + public static function getFunctionStorage(FunctionDynamicStorageProviderEvent $event): ?FunctionStorage; +} diff --git a/src/Psalm/PluginRegistrationSocket.php b/src/Psalm/PluginRegistrationSocket.php index 0b0b4cbcc7f..7f319a9170c 100644 --- a/src/Psalm/PluginRegistrationSocket.php +++ b/src/Psalm/PluginRegistrationSocket.php @@ -9,6 +9,7 @@ use Psalm\Plugin\EventHandler\FunctionExistenceProviderInterface; use Psalm\Plugin\EventHandler\FunctionParamsProviderInterface; use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; +use Psalm\Plugin\EventHandler\FunctionDynamicStorageProviderInterface; use Psalm\Plugin\EventHandler\MethodExistenceProviderInterface; use Psalm\Plugin\EventHandler\MethodParamsProviderInterface; use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; @@ -108,6 +109,10 @@ public function registerHooksFromClass(string $handler): void if (is_subclass_of($handler, FunctionReturnTypeProviderInterface::class)) { $this->codebase->functions->return_type_provider->registerClass($handler); } + + if (is_subclass_of($handler, FunctionDynamicStorageProviderInterface::class)) { + $this->codebase->functions->dynamic_storage_provider->registerClass($handler); + } } /** From 3210aab278561de33a671997f8bc6866f5faf608 Mon Sep 17 00:00:00 2001 From: adrew Date: Sun, 23 Jan 2022 23:09:46 +0300 Subject: [PATCH 02/18] Test for FunctionDynamicStorageProvider --- .../Plugin/Hook/ArrayMapStorageProvider.php | 190 ++++++++++++++++++ tests/Config/Plugin/StoragePlugin.php | 19 ++ tests/Config/PluginTest.php | 54 +++++ 3 files changed, 263 insertions(+) create mode 100644 tests/Config/Plugin/Hook/ArrayMapStorageProvider.php create mode 100644 tests/Config/Plugin/StoragePlugin.php diff --git a/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php b/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php new file mode 100644 index 00000000000..38effea091a --- /dev/null +++ b/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php @@ -0,0 +1,190 @@ +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 + * list -> 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 + */ + 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 $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> + */ + 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'); + } +} diff --git a/tests/Config/Plugin/StoragePlugin.php b/tests/Config/Plugin/StoragePlugin.php new file mode 100644 index 00000000000..3a8b380f563 --- /dev/null +++ b/tests/Config/Plugin/StoragePlugin.php @@ -0,0 +1,19 @@ +registerHooksFromClass(ArrayMapStorageProvider::class); + } +} diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index 0de42408ea9..8968f00f948 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -1022,4 +1022,58 @@ function output(array $build) {} $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, + ' + + + + + + + + ' + ) + ); + + $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); + + $file_path = getcwd() . '/src/somefile.php'; + + $this->addFile( + $file_path, + ' $_list + */ + function acceptsList(array $_list): void { } + + /** @var list $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()); + } } From ec9b2288cc6e52e4367995a49baff474f9ce1161 Mon Sep 17 00:00:00 2001 From: adrew Date: Sun, 23 Jan 2022 23:28:49 +0300 Subject: [PATCH 03/18] Fix CS --- src/Psalm/Internal/Codebase/Functions.php | 2 +- src/Psalm/PluginRegistrationSocket.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index 1ec430be897..9b5c01d257c 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -9,10 +9,10 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Provider\FileStorageProvider; +use Psalm\Internal\Provider\FunctionDynamicStorageProvider; use Psalm\Internal\Provider\FunctionExistenceProvider; use Psalm\Internal\Provider\FunctionParamsProvider; use Psalm\Internal\Provider\FunctionReturnTypeProvider; -use Psalm\Internal\Provider\FunctionDynamicStorageProvider; use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\NodeTypeProvider; use Psalm\StatementsSource; diff --git a/src/Psalm/PluginRegistrationSocket.php b/src/Psalm/PluginRegistrationSocket.php index 7f319a9170c..0c8d2db915a 100644 --- a/src/Psalm/PluginRegistrationSocket.php +++ b/src/Psalm/PluginRegistrationSocket.php @@ -6,10 +6,10 @@ use LogicException; use Psalm\Internal\Analyzer\FileAnalyzer; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Plugin\EventHandler\FunctionDynamicStorageProviderInterface; use Psalm\Plugin\EventHandler\FunctionExistenceProviderInterface; use Psalm\Plugin\EventHandler\FunctionParamsProviderInterface; use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; -use Psalm\Plugin\EventHandler\FunctionDynamicStorageProviderInterface; use Psalm\Plugin\EventHandler\MethodExistenceProviderInterface; use Psalm\Plugin\EventHandler\MethodParamsProviderInterface; use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; From e93c90bc893a1d994a2829ecafe5b2162b0bfbc5 Mon Sep 17 00:00:00 2001 From: adrew Date: Sun, 23 Jan 2022 23:33:14 +0300 Subject: [PATCH 04/18] Fix double run FunctionDynamicStorageProvider for already analysed function --- .../Internal/Provider/FunctionDynamicStorageProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php index 556ca7d5db0..8021c656092 100644 --- a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php +++ b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php @@ -23,7 +23,7 @@ final class FunctionDynamicStorageProvider /** @var array> */ private static $handlers = []; - /** @var array */ + /** @var array */ private static $dynamic_storages = []; /** @@ -82,9 +82,9 @@ public function getFunctionSignature( ); $result = $class_handler($event); + self::$dynamic_storages[$dynamic_storage_id] = $result; if ($result) { - self::$dynamic_storages[$dynamic_storage_id] = $result; return $result; } } From ce1cec5d3afc36b12aa709e47acc9c03cae0d719 Mon Sep 17 00:00:00 2001 From: adrew Date: Mon, 24 Jan 2022 00:28:25 +0300 Subject: [PATCH 05/18] Using FuncCall instead of list in FunctionDynamicStorageProvider for more flexibility --- .../Expression/Call/ArgumentsAnalyzer.php | 1 - .../Expression/Call/FunctionCallAnalyzer.php | 1 - .../Provider/FunctionDynamicStorageProvider.php | 6 +----- .../FunctionDynamicStorageProviderEvent.php | 17 +++++------------ .../Plugin/Hook/ArrayMapStorageProvider.php | 8 +++++++- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 6f2b1082274..7d217d1f97c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -360,7 +360,6 @@ private static function getHighOrderFuncStorage( $function_like_call, $statements_analyzer, $function_id, - $function_like_call->getArgs(), $context, new CodeLocation($statements_analyzer, $function_like_call), ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 204ee372e8e..94a59b0dff0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -533,7 +533,6 @@ private static function handleNamedFunction( $stmt, $statements_analyzer, $function_call_info->function_id, - $args, $context, $code_location ); diff --git a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php index 8021c656092..71db7ecada8 100644 --- a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php +++ b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php @@ -51,14 +51,10 @@ public function has(string $fq_function_name): bool return isset(self::$handlers[strtolower($fq_function_name)]); } - /** - * @param list $call_args - */ public function getFunctionSignature( PhpParser\Node\Expr\FuncCall $stmt, StatementsAnalyzer $statements_analyzer, string $function_id, - array $call_args, Context $context, CodeLocation $code_location ): ?FunctionStorage { @@ -76,7 +72,7 @@ public function getFunctionSignature( $event = new FunctionDynamicStorageProviderEvent( $statements_analyzer, $function_id, - $call_args, + $stmt, $context, $code_location ); diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php index dad62a0968f..9d22afa3623 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php @@ -13,24 +13,20 @@ class FunctionDynamicStorageProviderEvent { private StatementsAnalyzer $statements_analyzer; private string $function_id; - /** @var list */ - private array $call_args; + private PhpParser\Node\Expr\FuncCall $func_call; private Context $context; private CodeLocation $code_location; - /** - * @param list $call_args - */ public function __construct( StatementsAnalyzer $statements_analyzer, string $function_id, - array $call_args, + PhpParser\Node\Expr\FuncCall $func_call, Context $context, CodeLocation $code_location ) { $this->statements_analyzer = $statements_analyzer; $this->function_id = $function_id; - $this->call_args = $call_args; + $this->func_call = $func_call; $this->context = $context; $this->code_location = $code_location; } @@ -45,12 +41,9 @@ public function getFunctionId(): string return $this->function_id; } - /** - * @return list - */ - public function getCallArgs(): array + public function getExpr(): PhpParser\Node\Expr\FuncCall { - return $this->call_args; + return $this->func_call; } public function getContext(): Context diff --git a/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php b/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php index 38effea091a..e9a1fff902a 100644 --- a/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php +++ b/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php @@ -29,9 +29,15 @@ public static function getFunctionIds(): array public static function getFunctionStorage(FunctionDynamicStorageProviderEvent $event): ?FunctionStorage { + $func_call = $event->getExpr(); + + if ($func_call->isFirstClassCallable()) { + return null; + } + $context = $event->getContext(); $statements_analyzer = $event->getStatementsAnalyzer(); - $call_args = $event->getCallArgs(); + $call_args = $func_call->getArgs(); $args_count = count($call_args); $expected_callable_args_count = $args_count - 1; From a4e56ae28873dd8cc8c3eefe0849701832880699 Mon Sep 17 00:00:00 2001 From: adrew Date: Mon, 24 Jan 2022 00:36:07 +0300 Subject: [PATCH 06/18] Fix method name --- .../Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php | 2 +- .../Statements/Expression/Call/FunctionCallAnalyzer.php | 2 +- src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 7d217d1f97c..ed02fa5205f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -356,7 +356,7 @@ private static function getHighOrderFuncStorage( } if ($codebase->functions->dynamic_storage_provider->has($function_id)) { - return $codebase->functions->dynamic_storage_provider->getFunctionSignature( + return $codebase->functions->dynamic_storage_provider->getFunctionStorage( $function_like_call, $statements_analyzer, $function_id, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 94a59b0dff0..47558f9bc11 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -529,7 +529,7 @@ private static function handleNamedFunction( $has_dynamic_storage = false; if ($codebase->functions->dynamic_storage_provider->has($function_call_info->function_id)) { - $dynamic_function_storage = $codebase->functions->dynamic_storage_provider->getFunctionSignature( + $dynamic_function_storage = $codebase->functions->dynamic_storage_provider->getFunctionStorage( $stmt, $statements_analyzer, $function_call_info->function_id, diff --git a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php index 71db7ecada8..b7c033a99a3 100644 --- a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php +++ b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php @@ -51,7 +51,7 @@ public function has(string $fq_function_name): bool return isset(self::$handlers[strtolower($fq_function_name)]); } - public function getFunctionSignature( + public function getFunctionStorage( PhpParser\Node\Expr\FuncCall $stmt, StatementsAnalyzer $statements_analyzer, string $function_id, From 89c6a70dda79098932057cf5151d46d28a2b504a Mon Sep 17 00:00:00 2001 From: adrew Date: Mon, 24 Jan 2022 12:10:07 +0300 Subject: [PATCH 07/18] Simplify dynamic storage handling in FunctionCallAnalyzer --- .../Expression/Call/FunctionCallAnalyzer.php | 66 +++++++++---------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 47558f9bc11..13e3c50afdf 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -526,7 +526,7 @@ private static function handleNamedFunction( $function_call_info->defined_constants = []; $function_call_info->global_variables = []; $args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs(); - $has_dynamic_storage = false; + $dynamic_function_storage = null; if ($codebase->functions->dynamic_storage_provider->has($function_call_info->function_id)) { $dynamic_function_storage = $codebase->functions->dynamic_storage_provider->getFunctionStorage( @@ -536,52 +536,46 @@ private static function handleNamedFunction( $context, $code_location ); + } + if ($function_call_info->function_exists) { if ($dynamic_function_storage) { - $has_dynamic_storage = true; - $function_call_info->function_storage = $dynamic_function_storage; - $function_call_info->function_params = $function_call_info->function_storage->params; + $function_call_info->function_params = $dynamic_function_storage->params; $function_call_info->allow_named_args = $dynamic_function_storage->allow_named_arg_calls; $function_call_info->defined_constants = $dynamic_function_storage->defined_constants; $function_call_info->global_variables = $dynamic_function_storage->global_variables; - } - } - - if ($function_call_info->function_exists) { - if (!$has_dynamic_storage) { - if (!$function_call_info->in_call_map || $function_call_info->is_stubbed) { - try { - $function_call_info->function_storage = $function_storage = $codebase_functions->getStorage( - $statements_analyzer, - strtolower($function_call_info->function_id) - ); - - $function_call_info->function_params = $function_call_info->function_storage->params; + } elseif (!$function_call_info->in_call_map || $function_call_info->is_stubbed) { + try { + $function_call_info->function_storage = $function_storage = $codebase_functions->getStorage( + $statements_analyzer, + strtolower($function_call_info->function_id) + ); - if (!$function_storage->allow_named_arg_calls) { - $function_call_info->allow_named_args = false; - } + $function_call_info->function_params = $function_call_info->function_storage->params; - if (!$is_predefined) { - $function_call_info->defined_constants = $function_storage->defined_constants; - $function_call_info->global_variables = $function_storage->global_variables; - } - } catch (UnexpectedValueException $e) { - $function_call_info->function_params = [ - new FunctionLikeParameter('args', false, null, null, null, false, false, true) - ]; + if (!$function_storage->allow_named_arg_calls) { + $function_call_info->allow_named_args = false; } - } else { - $function_callable = InternalCallMapHandler::getCallableFromCallMapById( - $codebase, - $function_call_info->function_id, - $args, - $statements_analyzer->node_data - ); - $function_call_info->function_params = $function_callable->params; + if (!$is_predefined) { + $function_call_info->defined_constants = $function_storage->defined_constants; + $function_call_info->global_variables = $function_storage->global_variables; + } + } catch (UnexpectedValueException $e) { + $function_call_info->function_params = [ + new FunctionLikeParameter('args', false, null, null, null, false, false, true) + ]; } + } else { + $function_callable = InternalCallMapHandler::getCallableFromCallMapById( + $codebase, + $function_call_info->function_id, + $args, + $statements_analyzer->node_data + ); + + $function_call_info->function_params = $function_callable->params; } if ($codebase->functions->params_provider->has($function_call_info->function_id)) { From aefe971b90059a73b050d071256aab7f63f4c3e9 Mon Sep 17 00:00:00 2001 From: adrew Date: Mon, 24 Jan 2022 22:45:26 +0300 Subject: [PATCH 08/18] Fix template creation in ArrayMapStorageProvider --- tests/Config/Plugin/Hook/ArrayMapStorageProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php b/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php index e9a1fff902a..0717e029547 100644 --- a/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php +++ b/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php @@ -177,7 +177,7 @@ private static function createTemplates(int $expected_callable_count): array $template = self::createTemplate('T', $i); $template_params[$template->param_name] = [ - $template->defining_class => new Union([$template]) + $template->defining_class => $template->as ]; } From 3c22ecfa98fe4f466b5c36abb0cfe4aabb07f2e8 Mon Sep 17 00:00:00 2001 From: adrew Date: Tue, 25 Jan 2022 23:32:48 +0300 Subject: [PATCH 09/18] Remove args pre-analysis before run params provider hook --- .../Statements/Expression/Call/FunctionCallAnalyzer.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 13e3c50afdf..c30646bd8b4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -579,15 +579,6 @@ private static function handleNamedFunction( } if ($codebase->functions->params_provider->has($function_call_info->function_id)) { - ArgumentsAnalyzer::analyze( - $statements_analyzer, - $stmt->getArgs(), - $function_call_info->function_params, - $function_call_info->function_id, - $function_call_info->allow_named_args, - $context - ); - $function_call_info->function_params = $codebase->functions->params_provider->getFunctionParams( $statements_analyzer, $function_call_info->function_id, From 9b383a5a19dd32cdd20e62fcd798bd6be50cd118 Mon Sep 17 00:00:00 2001 From: adrew Date: Wed, 26 Jan 2022 01:16:34 +0300 Subject: [PATCH 10/18] Try to create public api for new hook --- .../FunctionDynamicStorageProvider.php | 18 +- src/Psalm/Plugin/ArgTypeInferer.php | 36 ++++ src/Psalm/Plugin/DynamicFunctionStorage.php | 76 +++++++ src/Psalm/Plugin/DynamicTemplateProvider.php | 30 +++ .../FunctionDynamicStorageProviderEvent.php | 36 +++- ...unctionDynamicStorageProviderInterface.php | 4 +- .../Plugin/Hook/ArrayMapStorageProvider.php | 196 ------------------ .../Hook/CustomArrayMapStorageProvider.php | 177 ++++++++++++++++ tests/Config/Plugin/StoragePlugin.php | 6 +- 9 files changed, 365 insertions(+), 214 deletions(-) create mode 100644 src/Psalm/Plugin/ArgTypeInferer.php create mode 100644 src/Psalm/Plugin/DynamicFunctionStorage.php create mode 100644 src/Psalm/Plugin/DynamicTemplateProvider.php delete mode 100644 tests/Config/Plugin/Hook/ArrayMapStorageProvider.php create mode 100644 tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php diff --git a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php index b7c033a99a3..1ae35cf86cd 100644 --- a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php +++ b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php @@ -9,6 +9,9 @@ use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Plugin\ArgTypeInferer; +use Psalm\Plugin\DynamicFunctionStorage; +use Psalm\Plugin\DynamicTemplateProvider; use Psalm\Plugin\EventHandler\Event\FunctionDynamicStorageProviderEvent; use Psalm\Plugin\EventHandler\FunctionDynamicStorageProviderInterface; use Psalm\Storage\FunctionStorage; @@ -20,7 +23,7 @@ */ final class FunctionDynamicStorageProvider { - /** @var array> */ + /** @var array> */ private static $handlers = []; /** @var array */ @@ -39,7 +42,7 @@ public function registerClass(string $class): void } /** - * @param Closure(FunctionDynamicStorageProviderEvent): ?FunctionStorage $c + * @param Closure(FunctionDynamicStorageProviderEvent): ?DynamicFunctionStorage $c */ public function registerClosure(string $fq_function_name, Closure $c): void { @@ -70,19 +73,20 @@ public function getFunctionStorage( foreach (self::$handlers[strtolower($function_id)] ?? [] as $class_handler) { $event = new FunctionDynamicStorageProviderEvent( + new ArgTypeInferer($context, $statements_analyzer), + new DynamicTemplateProvider('fn-' . strtolower($function_id)), $statements_analyzer, $function_id, $stmt, $context, - $code_location + $code_location, ); $result = $class_handler($event); - self::$dynamic_storages[$dynamic_storage_id] = $result; - if ($result) { - return $result; - } + return self::$dynamic_storages[$dynamic_storage_id] = $result + ? $result->toFunctionStorage($function_id) + : null; } return null; diff --git a/src/Psalm/Plugin/ArgTypeInferer.php b/src/Psalm/Plugin/ArgTypeInferer.php new file mode 100644 index 00000000000..fe255edd652 --- /dev/null +++ b/src/Psalm/Plugin/ArgTypeInferer.php @@ -0,0 +1,36 @@ +context = $context; + $this->statements_analyzer = $statements_analyzer; + } + + /** + * @return false|Union + */ + public function infer(PhpParser\Node\Arg $arg) + { + if (ExpressionAnalyzer::analyze($this->statements_analyzer, $arg->value, $this->context) === false) { + return false; + } + + return $this->statements_analyzer->node_data->getType($arg->value) ?? Type::getMixed(); + } +} diff --git a/src/Psalm/Plugin/DynamicFunctionStorage.php b/src/Psalm/Plugin/DynamicFunctionStorage.php new file mode 100644 index 00000000000..5d5256c7eef --- /dev/null +++ b/src/Psalm/Plugin/DynamicFunctionStorage.php @@ -0,0 +1,76 @@ + + */ + public array $params = []; + + /** + * A function return type. Maybe null. + * That means we can infer it in {@see FunctionReturnTypeProviderInterface} hook. + */ + public ?Union $return_type = null; + + /** + * A function can have template args or return type. + * Plugin hook must fill all used templates here. + * + * @var list + */ + public array $templates = []; + + /** + * Determines if a function can be called with named arguments. + */ + public bool $allow_named_arg_calls = true; + + /** + * Function purity. + * If function is pure then plugin hook should set it to true. + */ + public bool $pure = false; + + /** + * Determines if a function can be called with a various number of arguments. + */ + public bool $variadic = false; + + /** + * @internal + */ + public function toFunctionStorage(string $function_cased_name): FunctionStorage + { + $storage = new FunctionStorage(); + $storage->cased_name = $function_cased_name; + $storage->setParams($this->params); + $storage->return_type = $this->return_type; + $storage->allow_named_arg_calls = $this->allow_named_arg_calls; + $storage->pure = $this->pure; + $storage->variadic = $this->variadic; + + if (!empty($this->templates)) { + $storage->template_types = []; + + foreach ($this->templates as $template) { + $storage->template_types[$template->param_name] = [ + $template->defining_class => $template->as + ]; + } + } + + return $storage; + } +} diff --git a/src/Psalm/Plugin/DynamicTemplateProvider.php b/src/Psalm/Plugin/DynamicTemplateProvider.php new file mode 100644 index 00000000000..fbbcb3a36fd --- /dev/null +++ b/src/Psalm/Plugin/DynamicTemplateProvider.php @@ -0,0 +1,30 @@ +defining_class = $defining_class; + } + + /** + * If {@see DynamicFunctionStorage} requires template params this method can create it. + */ + public function createTemplate(string $param_name, Union $as = null): TTemplateParam + { + return new TTemplateParam($param_name, $as ?? Type::getMixed(), $this->defining_class); + } +} diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php index 9d22afa3623..8498a6b474b 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php @@ -6,34 +6,58 @@ use PhpParser; use Psalm\CodeLocation; +use Psalm\Codebase; use Psalm\Context; -use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Plugin\ArgTypeInferer; +use Psalm\Plugin\DynamicTemplateProvider; +use Psalm\StatementsSource; class FunctionDynamicStorageProviderEvent { - private StatementsAnalyzer $statements_analyzer; + private ArgTypeInferer $arg_type_inferer; + private DynamicTemplateProvider $template_provider; + private StatementsSource $statement_source; private string $function_id; private PhpParser\Node\Expr\FuncCall $func_call; private Context $context; private CodeLocation $code_location; public function __construct( - StatementsAnalyzer $statements_analyzer, + ArgTypeInferer $arg_type_inferer, + DynamicTemplateProvider $template_provider, + StatementsSource $statements_source, string $function_id, PhpParser\Node\Expr\FuncCall $func_call, Context $context, CodeLocation $code_location ) { - $this->statements_analyzer = $statements_analyzer; + $this->statement_source = $statements_source; $this->function_id = $function_id; $this->func_call = $func_call; $this->context = $context; $this->code_location = $code_location; + $this->arg_type_inferer = $arg_type_inferer; + $this->template_provider = $template_provider; } - public function getStatementsAnalyzer(): StatementsAnalyzer + public function getArgTypeInferer(): ArgTypeInferer { - return $this->statements_analyzer; + return $this->arg_type_inferer; + } + + public function getTemplateProvider(): DynamicTemplateProvider + { + return $this->template_provider; + } + + public function getCodebase(): Codebase + { + return $this->statement_source->getCodebase(); + } + + public function getStatementSource(): StatementsSource + { + return $this->statement_source; } public function getFunctionId(): string diff --git a/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php b/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php index 89efb006a53..7eed98d174c 100644 --- a/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php +++ b/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php @@ -4,8 +4,8 @@ namespace Psalm\Plugin\EventHandler; +use Psalm\Plugin\DynamicFunctionStorage; use Psalm\Plugin\EventHandler\Event\FunctionDynamicStorageProviderEvent; -use Psalm\Storage\FunctionStorage; interface FunctionDynamicStorageProviderInterface { @@ -14,5 +14,5 @@ interface FunctionDynamicStorageProviderInterface */ public static function getFunctionIds(): array; - public static function getFunctionStorage(FunctionDynamicStorageProviderEvent $event): ?FunctionStorage; + public static function getFunctionStorage(FunctionDynamicStorageProviderEvent $event): ?DynamicFunctionStorage; } diff --git a/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php b/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php deleted file mode 100644 index 0717e029547..00000000000 --- a/tests/Config/Plugin/Hook/ArrayMapStorageProvider.php +++ /dev/null @@ -1,196 +0,0 @@ -getExpr(); - - if ($func_call->isFirstClassCallable()) { - return null; - } - - $context = $event->getContext(); - $statements_analyzer = $event->getStatementsAnalyzer(); - $call_args = $func_call->getArgs(); - $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 - * list -> 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 - */ - 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 $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> - */ - 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 => $template->as - ]; - } - - 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'); - } -} diff --git a/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php new file mode 100644 index 00000000000..a7cda33a91d --- /dev/null +++ b/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php @@ -0,0 +1,177 @@ +getExpr(); + + if ($func_call->isFirstClassCallable()) { + return null; + } + + $template_provider = $event->getTemplateProvider(); + $arg_type_inferer = $event->getArgTypeInferer(); + $call_args = $func_call->getArgs(); + $args_count = count($call_args); + $expected_callable_args_count = $args_count - 1; + + if ($expected_callable_args_count < 1) { + return null; + } + + $last_arg = $call_args[$args_count - 1]; + + if (!($input_array_type = $arg_type_inferer->infer($last_arg)) || + !($input_value_type = self::toValueType($event->getCodebase(), $input_array_type)) + ) { + return null; + } + + $all_expected_callables = [ + self::createExpectedCallable($input_value_type, $template_provider), + ...self::createRestCallables($template_provider, $expected_callable_args_count), + ]; + + $custom_array_map_storage = new DynamicFunctionStorage(); + $custom_array_map_storage->templates = self::createTemplates($template_provider, $expected_callable_args_count); + $custom_array_map_storage->return_type = self::createReturnType($all_expected_callables); + $custom_array_map_storage->params = [ + ...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) + ), + self::createLastArrayMapParam($input_array_type) + ]; + + return $custom_array_map_storage; + } + + private static function createLastArrayMapParam(Union $input_array_type): FunctionLikeParameter + { + $last_array_map_param = new FunctionLikeParameter('input', false, $input_array_type); + $last_array_map_param->is_optional = false; + + return $last_array_map_param; + } + + /** + * Resolves value type from array-like type: + * list -> int + * list -> int|string + */ + private static function toValueType(Codebase $codebase, Union $array_like_type): ?Union + { + $value_types = []; + + foreach ($array_like_type->getAtomicTypes() as $atomic) { + if ($atomic instanceof Type\Atomic\TList) { + $value_types[] = $atomic->type_param; + } elseif ($atomic instanceof Type\Atomic\TArray) { + $value_types[] = $atomic->type_params[1]; + } elseif ($atomic instanceof Type\Atomic\TKeyedArray) { + $value_types[] = $atomic->getGenericValueType(); + } else { + return null; + } + } + + return Type::combineUnionTypeArray($value_types, $codebase); + } + + private static function createExpectedCallable( + Union $input_type, + DynamicTemplateProvider $template_provider, + int $return_template_offset = 0 + ): TCallable { + $expected_callable = new TCallable('callable'); + $expected_callable->params = [new FunctionLikeParameter('a', false, $input_type)]; + $expected_callable->return_type = new Union([ + $template_provider->createTemplate('T' . $return_template_offset) + ]); + + return $expected_callable; + } + + /** + * @return list + */ + private static function createRestCallables( + DynamicTemplateProvider $template_provider, + int $expected_callable_args_count + ): array { + $rest_callable_params = []; + + for ($template_offset = 0; $template_offset < $expected_callable_args_count - 1; $template_offset++) { + $rest_callable_params[] = self::createExpectedCallable( + new Union([ + $template_provider->createTemplate('T' . $template_offset) + ]), + $template_provider, + $template_offset + 1 + ); + } + + return $rest_callable_params; + } + + /** + * Extracts return type for custom_array_map from last callable arg. + * + * @param list $all_expected_callables + */ + private static function createReturnType(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()) + ]); + } + + /** + * Creates variadic template list for custom_array_map function. + * + * @return list + */ + private static function createTemplates( + DynamicTemplateProvider $template_provider, + int $expected_callable_count + ): array { + $template_params = []; + + for ($i = 0; $i < $expected_callable_count; $i++) { + $template_params[] = $template_provider->createTemplate('T' . $i); + } + + return $template_params; + } +} diff --git a/tests/Config/Plugin/StoragePlugin.php b/tests/Config/Plugin/StoragePlugin.php index 3a8b380f563..564d912a065 100644 --- a/tests/Config/Plugin/StoragePlugin.php +++ b/tests/Config/Plugin/StoragePlugin.php @@ -4,7 +4,7 @@ use Psalm\Plugin\PluginEntryPointInterface; use Psalm\Plugin\RegistrationInterface; -use Psalm\Tests\Config\Plugin\Hook\ArrayMapStorageProvider; +use Psalm\Tests\Config\Plugin\Hook\CustomArrayMapStorageProvider; use SimpleXMLElement; /** @psalm-suppress UnusedClass */ @@ -12,8 +12,8 @@ class StoragePlugin implements PluginEntryPointInterface { public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void { - require_once __DIR__ . '/Hook/ArrayMapStorageProvider.php'; + require_once __DIR__ . '/Hook/CustomArrayMapStorageProvider.php'; - $registration->registerHooksFromClass(ArrayMapStorageProvider::class); + $registration->registerHooksFromClass(CustomArrayMapStorageProvider::class); } } From 693b295c013ef78f41259e34be048b9f26fbb356 Mon Sep 17 00:00:00 2001 From: adrew Date: Wed, 26 Jan 2022 12:15:19 +0300 Subject: [PATCH 11/18] Improve ArgTypeInferer --- src/Psalm/Plugin/ArgTypeInferer.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Psalm/Plugin/ArgTypeInferer.php b/src/Psalm/Plugin/ArgTypeInferer.php index fe255edd652..f0dbf793568 100644 --- a/src/Psalm/Plugin/ArgTypeInferer.php +++ b/src/Psalm/Plugin/ArgTypeInferer.php @@ -27,6 +27,12 @@ public function __construct(Context $context, StatementsAnalyzer $statements_ana */ public function infer(PhpParser\Node\Arg $arg) { + $already_inferred_type = $this->statements_analyzer->node_data->getType($arg->value); + + if ($already_inferred_type) { + return $already_inferred_type; + } + if (ExpressionAnalyzer::analyze($this->statements_analyzer, $arg->value, $this->context) === false) { return false; } From 46f695993b1852adbaeab01a13cd2a4cf4b6b85e Mon Sep 17 00:00:00 2001 From: adrew Date: Wed, 26 Jan 2022 12:29:09 +0300 Subject: [PATCH 12/18] Make FunctionDynamicStorageProviderEvent final --- .../Event/FunctionDynamicStorageProviderEvent.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php index 8498a6b474b..d1dc5e792bc 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php @@ -12,7 +12,7 @@ use Psalm\Plugin\DynamicTemplateProvider; use Psalm\StatementsSource; -class FunctionDynamicStorageProviderEvent +final class FunctionDynamicStorageProviderEvent { private ArgTypeInferer $arg_type_inferer; private DynamicTemplateProvider $template_provider; @@ -22,6 +22,9 @@ class FunctionDynamicStorageProviderEvent private Context $context; private CodeLocation $code_location; + /** + * @internal + */ public function __construct( ArgTypeInferer $arg_type_inferer, DynamicTemplateProvider $template_provider, From de2257ecd311adb85e0a7bdbecbb5f023d74289f Mon Sep 17 00:00:00 2001 From: adrew Date: Wed, 26 Jan 2022 12:30:49 +0300 Subject: [PATCH 13/18] Make ArgTypeInferer constructor internal --- src/Psalm/Plugin/ArgTypeInferer.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Psalm/Plugin/ArgTypeInferer.php b/src/Psalm/Plugin/ArgTypeInferer.php index f0dbf793568..00bd911dab7 100644 --- a/src/Psalm/Plugin/ArgTypeInferer.php +++ b/src/Psalm/Plugin/ArgTypeInferer.php @@ -16,6 +16,9 @@ final class ArgTypeInferer private Context $context; private StatementsAnalyzer $statements_analyzer; + /** + * @internal + */ public function __construct(Context $context, StatementsAnalyzer $statements_analyzer) { $this->context = $context; From e5dae6a90140b2cdad932ffb6a14737da96719f0 Mon Sep 17 00:00:00 2001 From: adrew Date: Wed, 26 Jan 2022 14:14:24 +0300 Subject: [PATCH 14/18] Prevent first-class-callable handling with FunctionDynamicStorageProvider --- .../Internal/Provider/FunctionDynamicStorageProvider.php | 4 ++++ .../Event/FunctionDynamicStorageProviderEvent.php | 7 +++++-- .../Config/Plugin/Hook/CustomArrayMapStorageProvider.php | 8 +------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php index 1ae35cf86cd..452db991eea 100644 --- a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php +++ b/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php @@ -61,6 +61,10 @@ public function getFunctionStorage( Context $context, CodeLocation $code_location ): ?FunctionStorage { + if ($stmt->isFirstClassCallable()) { + return null; + } + $dynamic_storage_id = strtolower($statements_analyzer->getFilePath()) . ':' . $stmt->getLine() . ':' . (int)$stmt->getAttribute('startFilePos') diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php index d1dc5e792bc..8754ad34d16 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php @@ -68,9 +68,12 @@ public function getFunctionId(): string return $this->function_id; } - public function getExpr(): PhpParser\Node\Expr\FuncCall + /** + * @return list + */ + public function getArgs(): array { - return $this->func_call; + return $this->func_call->getArgs(); } public function getContext(): Context diff --git a/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php index a7cda33a91d..b0fcdde11b7 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php @@ -26,15 +26,9 @@ public static function getFunctionIds(): array public static function getFunctionStorage(FunctionDynamicStorageProviderEvent $event): ?DynamicFunctionStorage { - $func_call = $event->getExpr(); - - if ($func_call->isFirstClassCallable()) { - return null; - } - $template_provider = $event->getTemplateProvider(); $arg_type_inferer = $event->getArgTypeInferer(); - $call_args = $func_call->getArgs(); + $call_args = $event->getArgs(); $args_count = count($call_args); $expected_callable_args_count = $args_count - 1; From dd0e00e153302e5c9dfd6a71f45c420cdb8cedd7 Mon Sep 17 00:00:00 2001 From: adrew Date: Fri, 28 Jan 2022 12:51:01 +0300 Subject: [PATCH 15/18] Renaming all occurrences of "function dynamic storage" to "dynamic function storage" --- src/Psalm/Internal/Codebase/Functions.php | 6 +++--- ...ider.php => DynamicFunctionStorageProvider.php} | 14 +++++++------- ...=> DynamicFunctionStorageProviderInterface.php} | 6 +++--- ...php => DynamicFunctionStorageProviderEvent.php} | 2 +- src/Psalm/PluginRegistrationSocket.php | 4 ++-- ...p => CustomArrayMapFunctionStorageProvider.php} | 8 ++++---- tests/Config/Plugin/StoragePlugin.php | 6 +++--- 7 files changed, 23 insertions(+), 23 deletions(-) rename src/Psalm/Internal/Provider/{FunctionDynamicStorageProvider.php => DynamicFunctionStorageProvider.php} (84%) rename src/Psalm/Plugin/EventHandler/{FunctionDynamicStorageProviderInterface.php => DynamicFunctionStorageProviderInterface.php} (61%) rename src/Psalm/Plugin/EventHandler/Event/{FunctionDynamicStorageProviderEvent.php => DynamicFunctionStorageProviderEvent.php} (97%) rename tests/Config/Plugin/Hook/{CustomArrayMapStorageProvider.php => CustomArrayMapFunctionStorageProvider.php} (95%) diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index 9b5c01d257c..cf8c36d1d6b 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -9,7 +9,7 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Provider\FileStorageProvider; -use Psalm\Internal\Provider\FunctionDynamicStorageProvider; +use Psalm\Internal\Provider\DynamicFunctionStorageProvider; use Psalm\Internal\Provider\FunctionExistenceProvider; use Psalm\Internal\Provider\FunctionParamsProvider; use Psalm\Internal\Provider\FunctionReturnTypeProvider; @@ -56,7 +56,7 @@ class Functions /** @var FunctionParamsProvider */ public $params_provider; - /** @var FunctionDynamicStorageProvider */ + /** @var DynamicFunctionStorageProvider */ public $dynamic_storage_provider; /** @@ -71,7 +71,7 @@ public function __construct(FileStorageProvider $storage_provider, Reflection $r $this->return_type_provider = new FunctionReturnTypeProvider(); $this->existence_provider = new FunctionExistenceProvider(); $this->params_provider = new FunctionParamsProvider(); - $this->dynamic_storage_provider = new FunctionDynamicStorageProvider(); + $this->dynamic_storage_provider = new DynamicFunctionStorageProvider(); self::$stubbed_functions = []; } diff --git a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php b/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php similarity index 84% rename from src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php rename to src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php index 452db991eea..985b3c4f1fe 100644 --- a/src/Psalm/Internal/Provider/FunctionDynamicStorageProvider.php +++ b/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php @@ -12,8 +12,8 @@ use Psalm\Plugin\ArgTypeInferer; use Psalm\Plugin\DynamicFunctionStorage; use Psalm\Plugin\DynamicTemplateProvider; -use Psalm\Plugin\EventHandler\Event\FunctionDynamicStorageProviderEvent; -use Psalm\Plugin\EventHandler\FunctionDynamicStorageProviderInterface; +use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent; +use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface; use Psalm\Storage\FunctionStorage; use function strtolower; @@ -21,16 +21,16 @@ /** * @internal */ -final class FunctionDynamicStorageProvider +final class DynamicFunctionStorageProvider { - /** @var array> */ + /** @var array> */ private static $handlers = []; /** @var array */ private static $dynamic_storages = []; /** - * @param class-string $class + * @param class-string $class */ public function registerClass(string $class): void { @@ -42,7 +42,7 @@ public function registerClass(string $class): void } /** - * @param Closure(FunctionDynamicStorageProviderEvent): ?DynamicFunctionStorage $c + * @param Closure(DynamicFunctionStorageProviderEvent): ?DynamicFunctionStorage $c */ public function registerClosure(string $fq_function_name, Closure $c): void { @@ -76,7 +76,7 @@ public function getFunctionStorage( } foreach (self::$handlers[strtolower($function_id)] ?? [] as $class_handler) { - $event = new FunctionDynamicStorageProviderEvent( + $event = new DynamicFunctionStorageProviderEvent( new ArgTypeInferer($context, $statements_analyzer), new DynamicTemplateProvider('fn-' . strtolower($function_id)), $statements_analyzer, diff --git a/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php b/src/Psalm/Plugin/EventHandler/DynamicFunctionStorageProviderInterface.php similarity index 61% rename from src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php rename to src/Psalm/Plugin/EventHandler/DynamicFunctionStorageProviderInterface.php index 7eed98d174c..ee57cb5e13b 100644 --- a/src/Psalm/Plugin/EventHandler/FunctionDynamicStorageProviderInterface.php +++ b/src/Psalm/Plugin/EventHandler/DynamicFunctionStorageProviderInterface.php @@ -5,14 +5,14 @@ namespace Psalm\Plugin\EventHandler; use Psalm\Plugin\DynamicFunctionStorage; -use Psalm\Plugin\EventHandler\Event\FunctionDynamicStorageProviderEvent; +use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent; -interface FunctionDynamicStorageProviderInterface +interface DynamicFunctionStorageProviderInterface { /** * @return array */ public static function getFunctionIds(): array; - public static function getFunctionStorage(FunctionDynamicStorageProviderEvent $event): ?DynamicFunctionStorage; + public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage; } diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/DynamicFunctionStorageProviderEvent.php similarity index 97% rename from src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php rename to src/Psalm/Plugin/EventHandler/Event/DynamicFunctionStorageProviderEvent.php index 8754ad34d16..639d2746e32 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionDynamicStorageProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/DynamicFunctionStorageProviderEvent.php @@ -12,7 +12,7 @@ use Psalm\Plugin\DynamicTemplateProvider; use Psalm\StatementsSource; -final class FunctionDynamicStorageProviderEvent +final class DynamicFunctionStorageProviderEvent { private ArgTypeInferer $arg_type_inferer; private DynamicTemplateProvider $template_provider; diff --git a/src/Psalm/PluginRegistrationSocket.php b/src/Psalm/PluginRegistrationSocket.php index 0c8d2db915a..7f2927adda5 100644 --- a/src/Psalm/PluginRegistrationSocket.php +++ b/src/Psalm/PluginRegistrationSocket.php @@ -6,7 +6,7 @@ use LogicException; use Psalm\Internal\Analyzer\FileAnalyzer; use Psalm\Internal\Scanner\FileScanner; -use Psalm\Plugin\EventHandler\FunctionDynamicStorageProviderInterface; +use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface; use Psalm\Plugin\EventHandler\FunctionExistenceProviderInterface; use Psalm\Plugin\EventHandler\FunctionParamsProviderInterface; use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; @@ -110,7 +110,7 @@ public function registerHooksFromClass(string $handler): void $this->codebase->functions->return_type_provider->registerClass($handler); } - if (is_subclass_of($handler, FunctionDynamicStorageProviderInterface::class)) { + if (is_subclass_of($handler, DynamicFunctionStorageProviderInterface::class)) { $this->codebase->functions->dynamic_storage_provider->registerClass($handler); } } diff --git a/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php similarity index 95% rename from tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php rename to tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php index b0fcdde11b7..0c5046bbf1e 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php @@ -5,8 +5,8 @@ use Psalm\Codebase; use Psalm\Plugin\DynamicFunctionStorage; use Psalm\Plugin\DynamicTemplateProvider; -use Psalm\Plugin\EventHandler\Event\FunctionDynamicStorageProviderEvent; -use Psalm\Plugin\EventHandler\FunctionDynamicStorageProviderInterface; +use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent; +use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type; use Psalm\Type\Atomic\TCallable; @@ -17,14 +17,14 @@ use function array_map; use function count; -class CustomArrayMapStorageProvider implements FunctionDynamicStorageProviderInterface +class CustomArrayMapFunctionStorageProvider implements DynamicFunctionStorageProviderInterface { public static function getFunctionIds(): array { return ['custom_array_map']; } - public static function getFunctionStorage(FunctionDynamicStorageProviderEvent $event): ?DynamicFunctionStorage + public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage { $template_provider = $event->getTemplateProvider(); $arg_type_inferer = $event->getArgTypeInferer(); diff --git a/tests/Config/Plugin/StoragePlugin.php b/tests/Config/Plugin/StoragePlugin.php index 564d912a065..d5967486737 100644 --- a/tests/Config/Plugin/StoragePlugin.php +++ b/tests/Config/Plugin/StoragePlugin.php @@ -4,7 +4,7 @@ use Psalm\Plugin\PluginEntryPointInterface; use Psalm\Plugin\RegistrationInterface; -use Psalm\Tests\Config\Plugin\Hook\CustomArrayMapStorageProvider; +use Psalm\Tests\Config\Plugin\Hook\CustomArrayMapFunctionStorageProvider; use SimpleXMLElement; /** @psalm-suppress UnusedClass */ @@ -12,8 +12,8 @@ class StoragePlugin implements PluginEntryPointInterface { public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void { - require_once __DIR__ . '/Hook/CustomArrayMapStorageProvider.php'; + require_once __DIR__ . '/Hook/CustomArrayMapFunctionStorageProvider.php'; - $registration->registerHooksFromClass(CustomArrayMapStorageProvider::class); + $registration->registerHooksFromClass(CustomArrayMapFunctionStorageProvider::class); } } From 1a8367a2739a82eda8d2bdf55c85ee3bd86f373b Mon Sep 17 00:00:00 2001 From: adrew Date: Fri, 28 Jan 2022 14:27:40 +0300 Subject: [PATCH 16/18] Add some doc about DynamicFunctionStorageProvider --- .../Internal/Provider/DynamicFunctionStorageProvider.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php b/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php index 985b3c4f1fe..d3f37a4f469 100644 --- a/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php +++ b/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php @@ -12,13 +12,16 @@ use Psalm\Plugin\ArgTypeInferer; use Psalm\Plugin\DynamicFunctionStorage; use Psalm\Plugin\DynamicTemplateProvider; -use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent; use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface; +use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent; use Psalm\Storage\FunctionStorage; use function strtolower; /** + * For each function call analysis will be created individual FunctionStorage in plugin hook. + * If it is created be aware, it shadows the FunctionStorage Psalm may generate during the scanning phase. + * * @internal */ final class DynamicFunctionStorageProvider From 70cea447c819672b9178f03515af2f0f4803c5bd Mon Sep 17 00:00:00 2001 From: adrew Date: Fri, 28 Jan 2022 14:39:01 +0300 Subject: [PATCH 17/18] Fix CS --- src/Psalm/Internal/Codebase/Functions.php | 2 +- .../Plugin/Hook/CustomArrayMapFunctionStorageProvider.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index cf8c36d1d6b..323e5fa5580 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -8,8 +8,8 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\MethodIdentifier; -use Psalm\Internal\Provider\FileStorageProvider; use Psalm\Internal\Provider\DynamicFunctionStorageProvider; +use Psalm\Internal\Provider\FileStorageProvider; use Psalm\Internal\Provider\FunctionExistenceProvider; use Psalm\Internal\Provider\FunctionParamsProvider; use Psalm\Internal\Provider\FunctionReturnTypeProvider; diff --git a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php index 0c5046bbf1e..d3b938f8833 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php @@ -5,8 +5,8 @@ use Psalm\Codebase; use Psalm\Plugin\DynamicFunctionStorage; use Psalm\Plugin\DynamicTemplateProvider; -use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent; use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface; +use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type; use Psalm\Type\Atomic\TCallable; From 5b78125d1156164979b5741e2542298afa133fe1 Mon Sep 17 00:00:00 2001 From: adrew Date: Fri, 28 Jan 2022 15:16:40 +0300 Subject: [PATCH 18/18] Add doc for DynamicFunctionStorageProviderInterface --- docs/running_psalm/plugins/authoring_plugins.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/running_psalm/plugins/authoring_plugins.md b/docs/running_psalm/plugins/authoring_plugins.md index 494313970f1..9690a1b4b5e 100644 --- a/docs/running_psalm/plugins/authoring_plugins.md +++ b/docs/running_psalm/plugins/authoring_plugins.md @@ -92,6 +92,7 @@ class SomePlugin implements \Psalm\Plugin\EventHandler\AfterStatementAnalysisInt - `PropertyExistenceProviderInterface` - can be used to override Psalm's builtin property existence checks for one or more classes. - `PropertyTypeProviderInterface` - can be used to override Psalm's builtin property type lookup for one or more classes. - `PropertyVisibilityProviderInterface` - can be used to override Psalm's builtin property visibility checks for one or more classes. +- `DynamicFunctionStorageProviderInterface` - can be used to override signature for one or more functions. It means you can define variadic param list. Infer return type by input args. Define function templates. Also check out `Psalm\Plugin\DynamicFunctionStorage` to find out what api it brings for you to change function's definition. Here are a couple of example plugins: - [StringChecker](https://github.com/vimeo/psalm/blob/master/examples/plugins/StringChecker.php) - checks class references in strings