From bf1fc6686fcd8e22f38c3465f3f34b7fcd642a38 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:27:50 +0100 Subject: [PATCH] Fix getFunctionIdsFromCallableArg to return the function id for array($this, 'foo') in params providers for example --- .../Statements/Expression/CallAnalyzer.php | 9 ++ tests/Config/Plugin/FunctionPlugin.php | 3 + .../Hook/CallUserFuncLikeParamsProvider.php | 89 +++++++++++++++++++ tests/Config/PluginTest.php | 54 +++++++++++ 4 files changed, 155 insertions(+) create mode 100644 tests/Config/Plugin/Hook/CallUserFuncLikeParamsProvider.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index a547b291cb5..4ea737202ee 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -35,6 +35,7 @@ use Psalm\Node\Expr\BinaryOp\VirtualIdentical; use Psalm\Node\Expr\VirtualConstFetch; use Psalm\Node\VirtualName; +use Psalm\StatementsSource; use Psalm\Storage\Assertion\Falsy; use Psalm\Storage\Assertion\IsIdentical; use Psalm\Storage\Assertion\IsType; @@ -568,6 +569,14 @@ public static function getFunctionIdsFromCallableArg( if (!$file_source instanceof StatementsAnalyzer || !($class_arg_type = $file_source->node_data->getType($class_arg)) ) { + if ($file_source instanceof StatementsSource + && ($fqcln = $file_source->getFQCLN()) !== null + && $class_arg instanceof PhpParser\Node\Expr\Variable + && $class_arg->name === 'this' + ) { + return [$fqcln . '::' . $method_name_arg->value]; + } + return []; } diff --git a/tests/Config/Plugin/FunctionPlugin.php b/tests/Config/Plugin/FunctionPlugin.php index b17575bda08..fb27dc6910a 100644 --- a/tests/Config/Plugin/FunctionPlugin.php +++ b/tests/Config/Plugin/FunctionPlugin.php @@ -4,6 +4,7 @@ use Psalm\Plugin\PluginEntryPointInterface; use Psalm\Plugin\RegistrationInterface; +use Psalm\Test\Config\Plugin\Hook\CallUserFuncLikeParamsProvider; use Psalm\Test\Config\Plugin\Hook\MagicFunctionProvider; use SimpleXMLElement; @@ -12,8 +13,10 @@ class FunctionPlugin implements PluginEntryPointInterface { public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void { + require_once __DIR__ . '/Hook/CallUserFuncLikeParamsProvider.php'; require_once __DIR__ . '/Hook/MagicFunctionProvider.php'; + $registration->registerHooksFromClass(CallUserFuncLikeParamsProvider::class); $registration->registerHooksFromClass(MagicFunctionProvider::class); } } diff --git a/tests/Config/Plugin/Hook/CallUserFuncLikeParamsProvider.php b/tests/Config/Plugin/Hook/CallUserFuncLikeParamsProvider.php new file mode 100644 index 00000000000..456fcde3411 --- /dev/null +++ b/tests/Config/Plugin/Hook/CallUserFuncLikeParamsProvider.php @@ -0,0 +1,89 @@ + + */ + public static function getFunctionIds(): array + { + return ['call_user_func_like']; + } + + /** + * @return ?array + */ + public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array + { + $statements_source = $event->getStatementsSource(); + if (!$statements_source instanceof StatementsAnalyzer) { + return null; + } + + $call_args = $event->getCallArgs(); + if (!isset($call_args[0])) { + return null; + } + + $function_call_arg = $call_args[0]; + + $mapping_function_ids = array(); + if ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_ + || $function_call_arg->value instanceof PhpParser\Node\Expr\Array_ + || $function_call_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat + ) { + $mapping_function_ids = CallAnalyzer::getFunctionIdsFromCallableArg( + $statements_source, + $function_call_arg->value, + ); + } + + if (!isset($mapping_function_ids[0])) { + return null; + } + + $codebase = $event->getStatementsSource()->getCodebase(); + $function_like_storage = $codebase->getFunctionLikeStorage($statements_source, $mapping_function_ids[0]); + + $callback_param_types = []; + foreach ($function_like_storage->params as $function_like_parameter) { + $param_type_union = $function_like_parameter->type; + if (!$param_type_union) { + $param_type_union = Type::getMixed(); + } + + if ($function_like_parameter->is_nullable || $function_like_parameter->is_optional) { + $param_type_union = $param_type_union->setPossiblyUndefined(true); + } + + $callback_param_types[] = $param_type_union; + } + + if ($callback_param_types === []) { + $callback_params = Type::getEmptyArrayAtomic(); + } else { + $callback_params = new TKeyedArray( + $callback_param_types, + ); + } + + return array( + new FunctionLikeParameter('callable', false, new Union([new TCallable()]), null, null, null, false), + new FunctionLikeParameter('params', false, new Union([$callback_params]), null, null, null, false), + ); + } +} diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index bfd98cffc0b..a52428e8098 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -767,6 +767,60 @@ public function testFunctionProviderHooksInvalidArg(): void $this->analyzeFile($file_path, new Context()); } + public function testFunctionProviderHooksThisClassInvalidArg(): void + { + $this->expectExceptionMessage('InvalidScalarArgument'); + $this->expectException(CodeException::class); + require_once __DIR__ . '/Plugin/FunctionPlugin.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, + 'analyzeFile($file_path, new Context()); + } + public function testAfterAnalysisHooks(): void { require_once __DIR__ . '/Plugin/AfterAnalysisPlugin.php';