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