Skip to content

Commit 3e0ecec

Browse files
committed
array_map - understand call with multiple arrays
1 parent 9c3b765 commit 3e0ecec

8 files changed

+162
-21
lines changed

src/Analyser/MutatingScope.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -1517,9 +1517,20 @@ private function resolveType(Expr $node): Type
15171517
&& $argOrder === 0
15181518
&& isset($funcCall->args[1])
15191519
) {
1520-
$callableParameters = [
1521-
new DummyParameter('item', $this->getType($funcCall->args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
1522-
];
1520+
if (!isset($funcCall->args[2])) {
1521+
$callableParameters = [
1522+
new DummyParameter('item', $this->getType($funcCall->args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
1523+
];
1524+
} else {
1525+
$callableParameters = [];
1526+
foreach ($funcCall->args as $i => $funcCallArg) {
1527+
if ($i === 0) {
1528+
continue;
1529+
}
1530+
1531+
$callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
1532+
}
1533+
}
15231534
}
15241535
}
15251536
}

src/Analyser/NodeScopeResolver.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use PHPStan\BetterReflection\Reflector\ClassReflector;
5151
use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
5252
use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
53+
use PHPStan\DependencyInjection\BleedingEdgeToggle;
5354
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
5455
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
5556
use PHPStan\File\FileHelper;
@@ -3157,7 +3158,7 @@ private function processArgs(
31573158
}
31583159
}
31593160

3160-
if ($calleeReflection instanceof FunctionReflection) {
3161+
if (!BleedingEdgeToggle::isBleedingEdge() && $calleeReflection instanceof FunctionReflection) {
31613162
if (
31623163
$i === 0
31633164
&& $calleeReflection->getName() === 'array_map'

src/Reflection/ParametersAcceptorSelector.php

+15-3
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,24 @@ public static function selectFromArgs(
6565
) {
6666
$acceptor = $parametersAcceptors[0];
6767
$parameters = $acceptor->getParameters();
68+
if (!isset($args[2])) {
69+
$callbackParameters = [
70+
new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
71+
];
72+
} else {
73+
$callbackParameters = [];
74+
foreach ($args as $i => $arg) {
75+
if ($i === 0) {
76+
continue;
77+
}
78+
79+
$callbackParameters[] = new DummyParameter('item', $scope->getType($arg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
80+
}
81+
}
6882
$parameters[0] = new NativeParameterReflection(
6983
$parameters[0]->getName(),
7084
$parameters[0]->isOptional(),
71-
new CallableType([
72-
new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
73-
], new MixedType(), false),
85+
new CallableType($callbackParameters, new MixedType(), false),
7486
$parameters[0]->passedByReference(),
7587
$parameters[0]->isVariadic(),
7688
$parameters[0]->getDefaultValue()

src/Type/Php/ArrayMapFunctionReturnTypeExtension.php

+23-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Type\Accessory\NonEmptyArrayType;
1010
use PHPStan\Type\ArrayType;
1111
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
12+
use PHPStan\Type\IntegerType;
1213
use PHPStan\Type\MixedType;
1314
use PHPStan\Type\NeverType;
1415
use PHPStan\Type\Type;
@@ -44,23 +45,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
4445
);
4546
$arrayType = $scope->getType($functionCall->args[1]->value);
4647
$constantArrays = TypeUtils::getConstantArrays($arrayType);
47-
if (count($constantArrays) > 0) {
48-
$arrayTypes = [];
49-
foreach ($constantArrays as $constantArray) {
50-
$returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
51-
foreach ($constantArray->getKeyTypes() as $keyType) {
52-
$returnedArrayBuilder->setOffsetValueType(
53-
$keyType,
54-
$valueType
55-
);
48+
49+
if (!isset($functionCall->args[2])) {
50+
if (count($constantArrays) > 0) {
51+
$arrayTypes = [];
52+
foreach ($constantArrays as $constantArray) {
53+
$returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
54+
foreach ($constantArray->getKeyTypes() as $keyType) {
55+
$returnedArrayBuilder->setOffsetValueType(
56+
$keyType,
57+
$valueType
58+
);
59+
}
60+
$arrayTypes[] = $returnedArrayBuilder->getArray();
5661
}
57-
$arrayTypes[] = $returnedArrayBuilder->getArray();
58-
}
5962

60-
$mappedArrayType = TypeCombinator::union(...$arrayTypes);
61-
} elseif ($arrayType->isArray()->yes()) {
63+
$mappedArrayType = TypeCombinator::union(...$arrayTypes);
64+
} elseif ($arrayType->isArray()->yes()) {
65+
$mappedArrayType = TypeCombinator::intersect(new ArrayType(
66+
$arrayType->getIterableKeyType(),
67+
$valueType
68+
), ...TypeUtils::getAccessoryTypes($arrayType));
69+
}
70+
} else {
6271
$mappedArrayType = TypeCombinator::intersect(new ArrayType(
63-
$arrayType->getIterableKeyType(),
72+
new IntegerType(),
6473
$valueType
6574
), ...TypeUtils::getAccessoryTypes($arrayType));
6675
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ public function dataFileAsserts(): iterable
502502
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1870.php');
503503
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5562.php');
504504
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5615.php');
505+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array_map_multiple.php');
505506
}
506507

507508
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace ArrayMapMultiple;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
public function doFoo(int $i, string $s): void
11+
{
12+
$result = array_map(function ($a, $b) {
13+
assertType('int', $a);
14+
assertType('string', $b);
15+
16+
return rand(0, 1) ? $a : $b;
17+
}, ['foo' => $i], ['bar' => $s]);
18+
assertType('array<int, int|string>&nonEmpty', $result);
19+
}
20+
21+
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

+23
Original file line numberDiff line numberDiff line change
@@ -820,4 +820,27 @@ public function testBug5609(): void
820820
$this->analyse([__DIR__ . '/data/bug-5609.php'], []);
821821
}
822822

823+
public function dataArrayMapMultiple(): array
824+
{
825+
return [
826+
[true],
827+
[false],
828+
];
829+
}
830+
831+
/**
832+
* @dataProvider dataArrayMapMultiple
833+
* @param bool $checkExplicitMixed
834+
*/
835+
public function testArrayMapMultiple(bool $checkExplicitMixed): void
836+
{
837+
$this->checkExplicitMixed = $checkExplicitMixed;
838+
$this->analyse([__DIR__ . '/data/array_map_multiple.php'], [
839+
[
840+
'Parameter #1 $callback of function array_map expects callable(1|2, \'bar\'|\'foo\'): mixed, Closure(int, int): void given.',
841+
58,
842+
],
843+
]);
844+
}
845+
823846
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace ArrayMapMultipleCallTest;
4+
5+
/**
6+
* @template TKey as array-key
7+
* @template TValue
8+
*/
9+
class Collection
10+
{
11+
/**
12+
* @var array<TKey, TValue>
13+
*/
14+
protected $items = [];
15+
16+
/**
17+
* @param array<TKey, TValue> $items
18+
* @return void
19+
*/
20+
public function __construct($items)
21+
{
22+
$this->items = $items;
23+
}
24+
25+
/**
26+
* @template TMapValue
27+
*
28+
* @param callable(TValue, TKey): TMapValue $callback
29+
* @return self<TKey, TMapValue>
30+
*/
31+
public function map(callable $callback)
32+
{
33+
$keys = array_keys($this->items);
34+
35+
$items = array_map($callback, $this->items, $keys);
36+
37+
return new self(array_combine($keys, $items));
38+
}
39+
40+
/**
41+
* @return array<TKey, TValue>
42+
*/
43+
public function all()
44+
{
45+
return $this->items;
46+
}
47+
}
48+
49+
class Foo
50+
{
51+
52+
public function doFoo(): void
53+
{
54+
array_map(function (int $a, string $b) {
55+
56+
}, [1, 2], ['foo', 'bar']);
57+
58+
array_map(function (int $a, int $b) {
59+
60+
}, [1, 2], ['foo', 'bar']);
61+
}
62+
63+
}

0 commit comments

Comments
 (0)