1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Infer closure param type for array_filter/array_map

Fixes #664
This commit is contained in:
Brown 2019-05-07 16:27:25 -04:00
parent 44f0c64877
commit d7ee952084
2 changed files with 84 additions and 3 deletions

View File

@ -349,6 +349,10 @@ class CallAnalyzer
return;
}
if ($method_id === 'array_map' && count($args) === 2) {
$args = array_reverse($args, true);
}
foreach ($args as $argument_offset => $arg) {
if ($function_params !== null) {
$param = $argument_offset < count($function_params)
@ -397,6 +401,8 @@ class CallAnalyzer
$toggled_class_exists = true;
}
$codebase = $statements_analyzer->getCodebase();
if ($arg->value instanceof PhpParser\Node\Expr\Closure
&& $generic_params
&& $param
@ -409,12 +415,33 @@ class CallAnalyzer
}
)
) {
if (count($args) === 2
&& (($argument_offset === 1 && $method_id === 'array_filter')
|| ($argument_offset === 0 && $method_id === 'array_map'))
) {
$replaced_type = new Type\Union([
new Type\Atomic\TCallable(
'callable',
[
new \Psalm\Storage\FunctionLikeParameter(
'function',
false,
new Type\Union([
new Type\Atomic\TTemplateParam(
'ArrayValue',
Type::getMixed()
)
])
)
]
)
]);
} else {
$replaced_type = clone $param->type;
}
$empty_generic_params = [];
$codebase = $statements_analyzer->getCodebase();
$replaced_type->replaceTemplateTypesWithStandins(
$generic_params,
$empty_generic_params,
@ -460,6 +487,38 @@ class CallAnalyzer
$context->inside_call = false;
}
if (count($args) === 2
&& (($argument_offset === 0 && $method_id === 'array_filter')
|| ($argument_offset === 1 || $method_id === 'array_map'))
) {
$generic_param_type = new Type\Union([
new Type\Atomic\TArray([
Type::getArrayKey(),
new Type\Union([
new Type\Atomic\TTemplateParam(
'ArrayValue',
Type::getMixed()
)
])
])
]);
$template_types = ['ArrayValue' => ['' => [Type::getMixed()]]];
if ($generic_params === null) {
$generic_params = [];
}
$generic_param_type->replaceTemplateTypesWithStandins(
$template_types,
$generic_params,
$codebase,
isset($arg->value->inferredType)
? $arg->value->inferredType
: null
);
}
if ($context->collect_references
&& ($arg->value instanceof PhpParser\Node\Expr\AssignOp
|| $arg->value instanceof PhpParser\Node\Expr\PreInc

View File

@ -1627,6 +1627,18 @@ class FunctionCallTest extends TestCase
'$seconds' => 'string|int|float',
],
],
'inferArrayMapReturnType' => [
'<?php
/** @return array<string> */
function Foo(DateTime ...$dateTimes) : array {
return array_map(
function ($dateTime) {
return (string) ($dateTime->format("c"));
},
$dateTimes
);
}',
],
];
}
@ -1647,6 +1659,16 @@ class FunctionCallTest extends TestCase
'error_message' => 'MixedArgumentTypeCoercion',
'error_levels' => ['MissingClosureParamType', 'MissingClosureReturnType'],
],
'arrayFilterUseMethodOnInferrableInt' => [
'<?php
$a = array_filter([1, 2, 3, 4], function ($i) { return $i->foo(); });',
'error_message' => 'InvalidMethodCall',
],
'arrayMapUseMethodOnInferrableInt' => [
'<?php
$a = array_map(function ($i) { return $i->foo(); }, [1, 2, 3, 4]);',
'error_message' => 'InvalidMethodCall',
],
'invalidScalarArgument' => [
'<?php
function fooFoo(int $a): void {}