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:
parent
7fba401fdd
commit
2f7a7178ca
@ -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(),
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user