diff --git a/src/Parser/ArrayMapArgVisitor.php b/src/Parser/ArrayMapArgVisitor.php index 0c62d0c7c4..4564bdcb30 100644 --- a/src/Parser/ArrayMapArgVisitor.php +++ b/src/Parser/ArrayMapArgVisitor.php @@ -4,7 +4,7 @@ use PhpParser\Node; use PhpParser\NodeVisitorAbstract; -use function array_slice; +use function array_splice; use function count; final class ArrayMapArgVisitor extends NodeVisitorAbstract @@ -14,19 +14,43 @@ final class ArrayMapArgVisitor extends NodeVisitorAbstract 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; + } + + $callbackPos = 0; + if ($args[1]->name !== null && $args[1]->name->name === 'callback') { + $callbackPos = 1; + } + $callbackArg = $args[$callbackPos]; + $arrayArgs = $args; + array_splice($arrayArgs, $callbackPos, 1); + $callbackArg->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs); + 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/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 703d345806..13dcc95ba5 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -80,6 +80,10 @@ public static function selectFromArgs( && count($parametersAcceptors) > 0 ) { $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + if ($arrayMapArgs === null && isset($args[1])) { + // callback argument of array_map() may be the second one (named argument) + $arrayMapArgs = $args[1]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + } if ($arrayMapArgs !== null) { $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a6513f03cb..56a7efc7ea 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1936,4 +1936,24 @@ public function testBug12051(): void $this->analyse([__DIR__ . '/data/bug-12051.php'], []); } + public function testBug12317(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $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); + } +}