1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 13:51:54 +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( IssueBuffer::maybeAdd(
new InvalidNamedArgument( new InvalidNamedArgument(
'Parameter $' . $key_type->value . ' does not exist on function ' 'Parameter $' . $key_type->value . ' does not exist on function '
. ($cased_method_id ?: $method_id), . ($cased_method_id ?: $method_id),
new CodeLocation($statements_analyzer, $arg), new CodeLocation($statements_analyzer, $arg),
(string)$method_id, (string)$method_id,
), ),
@ -778,7 +778,7 @@ class ArgumentsAnalyzer
IssueBuffer::maybeAdd( IssueBuffer::maybeAdd(
new InvalidNamedArgument( new InvalidNamedArgument(
'Parameter $' . $arg->name->name . ' has already been used in ' '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), new CodeLocation($statements_analyzer, $arg->name),
(string) $method_id, (string) $method_id,
), ),
@ -990,7 +990,7 @@ class ArgumentsAnalyzer
|| $arg->value instanceof PhpParser\Node\Expr\Ternary || $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\FuncCall
|| $arg->value instanceof PhpParser\Node\Expr\MethodCall || $arg->value instanceof PhpParser\Node\Expr\MethodCall
|| $arg->value instanceof PhpParser\Node\Expr\StaticCall || $arg->value instanceof PhpParser\Node\Expr\StaticCall
@ -1188,7 +1188,7 @@ class ArgumentsAnalyzer
IssueBuffer::maybeAdd( IssueBuffer::maybeAdd(
new PossiblyUndefinedVariable( new PossiblyUndefinedVariable(
'Variable ' . $var_id '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), new CodeLocation($statements_analyzer->getSource(), $arg->value),
), ),
$statements_analyzer->getSuppressedIssues(), $statements_analyzer->getSuppressedIssues(),

View File

@ -29,12 +29,39 @@ use function strtolower;
*/ */
final class HighOrderFunctionArgHandler 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( public static function remapLowerBounds(
StatementsAnalyzer $statements_analyzer, StatementsAnalyzer $statements_analyzer,
TemplateResult $inferred_template_result, TemplateResult $inferred_template_result,
HighOrderFunctionArgInfo $input_function, HighOrderFunctionArgInfo $input_function,
Union $container_function_type Union $container_function_type
): TemplateResult { ): TemplateResult {
// Try to infer container callable by $inferred_template_result
$container_type = TemplateInferredTypeReplacer::replace( $container_type = TemplateInferredTypeReplacer::replace(
$container_function_type, $container_function_type,
$inferred_template_result, $inferred_template_result,
@ -44,6 +71,17 @@ final class HighOrderFunctionArgHandler
$input_function_type = $input_function->getFunctionType(); $input_function_type = $input_function->getFunctionType();
$input_function_template_result = $input_function->getTemplates(); $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) { foreach ($input_function_type->getAtomicTypes() as $input_atomic) {
if (!$input_atomic instanceof TClosure && !$input_atomic instanceof TCallable) { if (!$input_atomic instanceof TClosure && !$input_atomic instanceof TCallable) {
continue; continue;
@ -80,19 +118,23 @@ final class HighOrderFunctionArgHandler
HighOrderFunctionArgInfo $high_order_callable_info, HighOrderFunctionArgInfo $high_order_callable_info,
TemplateResult $high_order_template_result TemplateResult $high_order_template_result
): void { ): 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) { if ($high_order_callable_info->getType() === HighOrderFunctionArgInfo::TYPE_CALLABLE) {
return; return;
} }
$replaced = TemplateInferredTypeReplacer::replace( $fully_inferred_callable_type = TemplateInferredTypeReplacer::replace(
$high_order_callable_info->getFunctionType(), $high_order_callable_info->getFunctionType(),
$high_order_template_result, $high_order_template_result,
$statements_analyzer->getCodebase(), $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(), $statements_analyzer->getCodebase(),
$replaced, $fully_inferred_callable_type,
$context->self, $context->self,
$context->self, $context->self,
$context->parent, $context->parent,
@ -101,7 +143,9 @@ final class HighOrderFunctionArgHandler
false, false,
false, false,
true, true,
)); );
$statements_analyzer->node_data->setType($arg_expr, $expanded);
} }
public static function getCallableArgInfo( 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) { if ($input_arg_expr instanceof PhpParser\Node\Expr\ConstFetch) {
$constant = $context->constants[$input_arg_expr->name->toString()] ?? null; $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_id = pipe1([42], id);
$const_composition = pipe1([42], map(id)); $const_composition = pipe1([42], map(id));
$const_sequential = pipe2([42], map(fn($i) => ["num" => $i]), 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' => [ 'assertions' => [
@ -521,6 +529,12 @@ class CallableTest extends TestCase
'$const_id===' => 'list{42}', '$const_id===' => 'list{42}',
'$const_composition===' => 'list<42>', '$const_composition===' => 'list<42>',
'$const_sequential===' => 'list<array{num: 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' => [], 'ignored_issues' => [],
'php_version' => '8.0', 'php_version' => '8.0',