Skip to content

Commit 9edcffb

Browse files
authored
Fix analysis on array_map with named arguments
1 parent d1ea968 commit 9edcffb

File tree

3 files changed

+98
-10
lines changed

3 files changed

+98
-10
lines changed

src/Parser/ArrayMapArgVisitor.php

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use PhpParser\Node;
77
use PhpParser\NodeVisitorAbstract;
88
use PHPStan\DependencyInjection\AutowiredService;
9-
use function array_slice;
109
use function count;
1110

1211
#[AutowiredService]
@@ -18,19 +17,60 @@ final class ArrayMapArgVisitor extends NodeVisitorAbstract
1817
#[Override]
1918
public function enterNode(Node $node): ?Node
2019
{
21-
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) {
22-
$functionName = $node->name->toLowerString();
23-
if ($functionName === 'array_map') {
24-
$args = $node->getArgs();
25-
if (isset($args[0])) {
26-
$slicedArgs = array_slice($args, 1);
27-
if (count($slicedArgs) > 0) {
28-
$args[0]->value->setAttribute(self::ATTRIBUTE_NAME, $slicedArgs);
29-
}
20+
if (!$this->isArrayMapCall($node)) {
21+
return null;
22+
}
23+
24+
$args = $node->getArgs();
25+
if (count($args) < 2) {
26+
return null;
27+
}
28+
29+
$callbackArg = null;
30+
$arrayArgs = [];
31+
foreach ($args as $i => $arg) {
32+
if ($callbackArg === null) {
33+
if ($arg->name === null && $i === 0) {
34+
$callbackArg = $arg;
35+
continue;
36+
}
37+
if ($arg->name !== null && $arg->name->toString() === 'callback') {
38+
$callbackArg = $arg;
39+
continue;
3040
}
3141
}
42+
43+
$arrayArgs[] = $arg;
44+
}
45+
46+
if ($callbackArg !== null) {
47+
$callbackArg->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs);
48+
return new Node\Expr\FuncCall(
49+
$node->name,
50+
[$callbackArg, ...$arrayArgs],
51+
$node->getAttributes(),
52+
);
3253
}
54+
3355
return null;
3456
}
3557

58+
/**
59+
* @phpstan-assert-if-true Node\Expr\FuncCall $node
60+
*/
61+
private function isArrayMapCall(Node $node): bool
62+
{
63+
if (!$node instanceof Node\Expr\FuncCall) {
64+
return false;
65+
}
66+
if (!$node->name instanceof Node\Name) {
67+
return false;
68+
}
69+
if ($node->isFirstClassCallable()) {
70+
return false;
71+
}
72+
73+
return $node->name->toLowerString() === 'array_map';
74+
}
75+
3676
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,4 +2182,21 @@ public function testBug13065(): void
21822182
$this->analyse([__DIR__ . '/data/bug-13065.php'], $errors);
21832183
}
21842184

2185+
#[RequiresPhp('>= 8.0')]
2186+
public function testBug12317(): void
2187+
{
2188+
$this->checkExplicitMixed = true;
2189+
$this->checkImplicitMixed = true;
2190+
$this->analyse([__DIR__ . '/data/bug-12317.php'], [
2191+
[
2192+
'Parameter #1 $callback of function array_map expects (callable(Bug12317\Uuid): mixed)|null, Closure(string): string given.',
2193+
28,
2194+
],
2195+
[
2196+
'Parameter $callback of function array_map expects (callable(Bug12317\Uuid): mixed)|null, Closure(string): string given.',
2197+
29,
2198+
],
2199+
]);
2200+
}
2201+
21852202
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12317;
4+
5+
class Uuid {
6+
public function __construct(public string $uuid) {}
7+
public function __toString() { return $this->uuid; }
8+
}
9+
10+
class HelloWorld
11+
{
12+
/**
13+
* @param list<Uuid> $arr
14+
*/
15+
public function sayHello(array $arr): void
16+
{
17+
$callback = static fn(Uuid $uuid): string => (string) $uuid;
18+
19+
// ok
20+
array_map(array: $arr, callback: $callback);
21+
array_map(callback: $callback, array: $arr);
22+
array_map($callback, $arr);
23+
array_map($callback, array: $arr);
24+
array_map(static fn (Uuid $u1, Uuid $u2): string => (string) $u1, $arr, $arr);
25+
26+
// should be reported
27+
$invalidCallback = static fn(string $uuid): string => $uuid;
28+
array_map($invalidCallback, $arr);
29+
array_map(array: $arr, callback: $invalidCallback);
30+
}
31+
}

0 commit comments

Comments
 (0)