Skip to content

Commit

Permalink
Fix getFunctionIdsFromCallableArg to return the function id for array…
Browse files Browse the repository at this point in the history
…($this, 'foo') in params providers for example
  • Loading branch information
kkmuffme committed Feb 29, 2024
1 parent 9aa450b commit bf1fc66
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 [];
}

Expand Down
3 changes: 3 additions & 0 deletions tests/Config/Plugin/FunctionPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}
89 changes: 89 additions & 0 deletions tests/Config/Plugin/Hook/CallUserFuncLikeParamsProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Psalm\Test\Config\Plugin\Hook;

use PhpParser;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\FunctionParamsProviderEvent;
use Psalm\Plugin\EventHandler\FunctionParamsProviderInterface;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Type;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Union;

class CallUserFuncLikeParamsProvider implements
FunctionParamsProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['call_user_func_like'];
}

/**
* @return ?array<int, FunctionLikeParameter>
*/
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),
);
}
}
54 changes: 54 additions & 0 deletions tests/Config/PluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
'<?xml version="1.0"?>
<psalm
errorLevel="1"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\\Test\\Config\\Plugin\\FunctionPlugin" />
</plugins>
</psalm>',
),
);

$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);

$file_path = getcwd() . '/src/somefile.php';

$this->addFile(
$file_path,
'<?php
namespace {
/**
* @param callable $callable
* @param array $params
*/
function call_user_func_like($callable, $params): void {}
}
namespace Foo\Acme {
class Bar {
public function run(int $a): int {
return ($a + 2);
}
public function init(): void {
call_user_func_like(array($this, "run"), array("hello"));
}
}
}',
);

$this->analyzeFile($file_path, new Context());
}

public function testAfterAnalysisHooks(): void
{
require_once __DIR__ . '/Plugin/AfterAnalysisPlugin.php';
Expand Down

0 comments on commit bf1fc66

Please sign in to comment.