1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Docs for HighOrderFunctionArgHandler::remapLowerBounds

This commit is contained in:
andrew 2023-04-06 18:23:50 +03:00
parent 7fba401fdd
commit 2f7a7178ca
3 changed files with 70 additions and 8 deletions

View File

@ -758,7 +758,7 @@ class ArgumentsAnalyzer
IssueBuffer::maybeAdd(
new InvalidNamedArgument(
'Parameter $' . $key_type->value . ' does not exist on function '
. ($cased_method_id ?: $method_id),
. ($cased_method_id ?: $method_id),
new CodeLocation($statements_analyzer, $arg),
(string)$method_id,
),
@ -778,7 +778,7 @@ class ArgumentsAnalyzer
IssueBuffer::maybeAdd(
new InvalidNamedArgument(
'Parameter $' . $arg->name->name . ' has already been used in '
. ($cased_method_id ?: $method_id),
. ($cased_method_id ?: $method_id),
new CodeLocation($statements_analyzer, $arg->name),
(string) $method_id,
),
@ -990,7 +990,7 @@ class ArgumentsAnalyzer
|| $arg->value instanceof PhpParser\Node\Expr\Ternary
|| (
(
$arg->value instanceof PhpParser\Node\Expr\ConstFetch
$arg->value instanceof PhpParser\Node\Expr\ConstFetch
|| $arg->value instanceof PhpParser\Node\Expr\FuncCall
|| $arg->value instanceof PhpParser\Node\Expr\MethodCall
|| $arg->value instanceof PhpParser\Node\Expr\StaticCall
@ -1188,7 +1188,7 @@ class ArgumentsAnalyzer
IssueBuffer::maybeAdd(
new PossiblyUndefinedVariable(
'Variable ' . $var_id
. ' must be defined prior to use within an unknown function or method',
. ' must be defined prior to use within an unknown function or method',
new CodeLocation($statements_analyzer->getSource(), $arg->value),
),
$statements_analyzer->getSuppressedIssues(),

View File

@ -29,12 +29,39 @@ use function strtolower;
*/
final class HighOrderFunctionArgHandler
{
/**
* Compiles TemplateResult for high-order function
* by previous template args ($inferred_template_result).
*
* It's need for proper template replacement:
*
* ```
* * template T
* * return Closure(T): T
* function id(): Closure { ... }
*
* * template A
* * template B
* *
* * param list<A> $_items
* * param callable(A): B $_ab
* * return list<B>
* function map(array $items, callable $ab): array { ... }
*
* // list<int>
* $numbers = [1, 2, 3];
*
* $result = map($numbers, id());
* // $result is list<int> because template T of id() was inferred by previous arg.
* ```
*/
public static function remapLowerBounds(
StatementsAnalyzer $statements_analyzer,
TemplateResult $inferred_template_result,
HighOrderFunctionArgInfo $input_function,
Union $container_function_type
): TemplateResult {
// Try to infer container callable by $inferred_template_result
$container_type = TemplateInferredTypeReplacer::replace(
$container_function_type,
$inferred_template_result,
@ -44,6 +71,17 @@ final class HighOrderFunctionArgHandler
$input_function_type = $input_function->getFunctionType();
$input_function_template_result = $input_function->getTemplates();
// Traverse side by side 'container' params and 'input' params.
// This maps 'input' templates to 'container' templates.
//
// Example:
// 'input' => Closure(C:Bar, D:Bar): array{C:Bar, D:Bar}
// 'container' => Closure(int, string): array{int, string}
//
// $remapped_lower_bounds will be: [
// 'C' => ['Bar' => [int]],
// 'D' => ['Bar' => [string]]
// ].
foreach ($input_function_type->getAtomicTypes() as $input_atomic) {
if (!$input_atomic instanceof TClosure && !$input_atomic instanceof TCallable) {
continue;
@ -80,19 +118,23 @@ final class HighOrderFunctionArgHandler
HighOrderFunctionArgInfo $high_order_callable_info,
TemplateResult $high_order_template_result
): void {
// Psalm can infer simple callable/closure.
// But can't infer first-class-callable or high-order function.
if ($high_order_callable_info->getType() === HighOrderFunctionArgInfo::TYPE_CALLABLE) {
return;
}
$replaced = TemplateInferredTypeReplacer::replace(
$fully_inferred_callable_type = TemplateInferredTypeReplacer::replace(
$high_order_callable_info->getFunctionType(),
$high_order_template_result,
$statements_analyzer->getCodebase(),
);
$statements_analyzer->node_data->setType($arg_expr, TypeExpander::expandUnion(
// Some templates may not have been replaced.
// They expansion makes error message better.
$expanded = TypeExpander::expandUnion(
$statements_analyzer->getCodebase(),
$replaced,
$fully_inferred_callable_type,
$context->self,
$context->self,
$context->parent,
@ -101,7 +143,9 @@ final class HighOrderFunctionArgHandler
false,
false,
true,
));
);
$statements_analyzer->node_data->setType($arg_expr, $expanded);
}
public static function getCallableArgInfo(
@ -183,6 +227,10 @@ final class HighOrderFunctionArgHandler
);
}
if ($input_arg_expr instanceof PhpParser\Node\Scalar\String_) {
return self::fromLiteralString(Type::getString($input_arg_expr->value), $statements_analyzer);
}
if ($input_arg_expr instanceof PhpParser\Node\Expr\ConstFetch) {
$constant = $context->constants[$input_arg_expr->name->toString()] ?? null;

View File

@ -509,6 +509,14 @@ class CallableTest extends TestCase
$const_id = pipe1([42], id);
$const_composition = pipe1([42], map(id));
$const_sequential = pipe2([42], map(fn($i) => ["num" => $i]), id);
$string_id = pipe1([42], "Functions\id");
$string_composition = pipe1([42], map("Functions\id"));
$string_sequential = pipe2([42], map(fn($i) => ["num" => $i]), "Functions\id");
$class_string_id = pipe1([42], "Functions\Module::id");
$class_string_composition = pipe1([42], map("Functions\Module::id"));
$class_string_sequential = pipe2([42], map(fn($i) => ["num" => $i]), "Functions\Module::id");
}
',
'assertions' => [
@ -521,6 +529,12 @@ class CallableTest extends TestCase
'$const_id===' => 'list{42}',
'$const_composition===' => 'list<42>',
'$const_sequential===' => 'list<array{num: 42}>',
'$string_id===' => 'list{42}',
'$string_composition===' => 'list<42>',
'$string_sequential===' => 'list<array{num: 42}>',
'$class_string_id===' => 'list{42}',
'$class_string_composition===' => 'list<42>',
'$class_string_sequential===' => 'list<array{num: 42}>',
],
'ignored_issues' => [],
'php_version' => '8.0',