diff --git a/src/Parser/ArrayMapArgVisitor.php b/src/Parser/ArrayMapArgVisitor.php index a495d6e770..c9e5b8a358 100644 --- a/src/Parser/ArrayMapArgVisitor.php +++ b/src/Parser/ArrayMapArgVisitor.php @@ -6,7 +6,6 @@ use PhpParser\Node; use PhpParser\NodeVisitorAbstract; use PHPStan\DependencyInjection\AutowiredService; -use function array_slice; use function count; #[AutowiredService] @@ -18,19 +17,60 @@ final class ArrayMapArgVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { - $functionName = $node->name->toLowerString(); - if ($functionName === 'array_map') { - $args = $node->getArgs(); - if (isset($args[0])) { - $slicedArgs = array_slice($args, 1); - if (count($slicedArgs) > 0) { - $args[0]->value->setAttribute(self::ATTRIBUTE_NAME, $slicedArgs); - } + if (!$this->isArrayMapCall($node)) { + return null; + } + + $args = $node->getArgs(); + if (count($args) < 2) { + return null; + } + + $callbackArg = null; + $arrayArgs = []; + foreach ($args as $i => $arg) { + if ($callbackArg === null) { + if ($arg->name === null && $i === 0) { + $callbackArg = $arg; + continue; + } + if ($arg->name !== null && $arg->name->toString() === 'callback') { + $callbackArg = $arg; + continue; } } + + $arrayArgs[] = $arg; + } + + if ($callbackArg !== null) { + $callbackArg->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs); + return new Node\Expr\FuncCall( + $node->name, + [$callbackArg, ...$arrayArgs], + $node->getAttributes(), + ); } + return null; } + /** + * @phpstan-assert-if-true Node\Expr\FuncCall $node + */ + private function isArrayMapCall(Node $node): bool + { + if (!$node instanceof Node\Expr\FuncCall) { + return false; + } + if (!$node->name instanceof Node\Name) { + return false; + } + if ($node->isFirstClassCallable()) { + return false; + } + + return $node->name->toLowerString() === 'array_map'; + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index aaba0745cc..82522288fd 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2182,4 +2182,21 @@ public function testBug13065(): void $this->analyse([__DIR__ . '/data/bug-13065.php'], $errors); } + #[RequiresPhp('>= 8.0')] + public function testBug12317(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12317.php'], [ + [ + 'Parameter #1 $callback of function array_map expects (callable(Bug12317\Uuid): mixed)|null, Closure(string): string given.', + 28, + ], + [ + 'Parameter $callback of function array_map expects (callable(Bug12317\Uuid): mixed)|null, Closure(string): string given.', + 29, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12317.php b/tests/PHPStan/Rules/Functions/data/bug-12317.php new file mode 100644 index 0000000000..2443fefc09 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12317.php @@ -0,0 +1,31 @@ +uuid; } +} + +class HelloWorld +{ + /** + * @param list $arr + */ + public function sayHello(array $arr): void + { + $callback = static fn(Uuid $uuid): string => (string) $uuid; + + // ok + array_map(array: $arr, callback: $callback); + array_map(callback: $callback, array: $arr); + array_map($callback, $arr); + array_map($callback, array: $arr); + array_map(static fn (Uuid $u1, Uuid $u2): string => (string) $u1, $arr, $arr); + + // should be reported + $invalidCallback = static fn(string $uuid): string => $uuid; + array_map($invalidCallback, $arr); + array_map(array: $arr, callback: $invalidCallback); + } +}