diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 8ca071cbb..b3e3774e1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -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 } ) ) { - $replaced_type = clone $param->type; + 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 diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 2577e0536..dc5d9348f 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1627,6 +1627,18 @@ class FunctionCallTest extends TestCase '$seconds' => 'string|int|float', ], ], + 'inferArrayMapReturnType' => [ + ' */ + 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' => [ + 'foo(); });', + 'error_message' => 'InvalidMethodCall', + ], + 'arrayMapUseMethodOnInferrableInt' => [ + 'foo(); }, [1, 2, 3, 4]);', + 'error_message' => 'InvalidMethodCall', + ], 'invalidScalarArgument' => [ '