Skip to content

Commit 8862d57

Browse files
herndlmondrejmirtes
authored andcommitted
Improve variadic parameter support in array_merge
1 parent 2a145ba commit 8862d57

File tree

2 files changed

+57
-23
lines changed

2 files changed

+57
-23
lines changed

src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
use PHPStan\Type\NeverType;
1717
use PHPStan\Type\Type;
1818
use PHPStan\Type\TypeCombinator;
19-
use PHPStan\Type\UnionType;
19+
use function array_keys;
20+
use function count;
2021
use function in_array;
2122

2223
class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -36,23 +37,44 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3637
}
3738

3839
$argTypes = [];
40+
$optionalArgTypes = [];
3941
$allConstant = true;
40-
foreach ($args as $i => $arg) {
42+
foreach ($args as $arg) {
4143
$argType = $scope->getType($arg->value);
42-
$argTypes[$i] = $argType;
4344

44-
if (!$arg->unpack && $argType instanceof ConstantArrayType) {
45-
continue;
46-
}
45+
if ($arg->unpack) {
46+
if ($argType instanceof ConstantArrayType) {
47+
$argTypesFound = $argType->getValueTypes();
48+
} else {
49+
$argTypesFound = [$argType->getIterableValueType()];
50+
}
51+
52+
foreach ($argTypesFound as $argTypeFound) {
53+
$argTypes[] = $argTypeFound;
54+
if ($argTypeFound instanceof ConstantArrayType) {
55+
continue;
56+
}
57+
$allConstant = false;
58+
}
4759

48-
$allConstant = false;
60+
if (!$argType->isIterableAtLeastOnce()->yes()) {
61+
// unpacked params can be empty, making them optional
62+
$optionalArgTypesOffset = count($argTypes) - 1;
63+
foreach (array_keys($argTypesFound) as $key) {
64+
$optionalArgTypes[] = $optionalArgTypesOffset + $key;
65+
}
66+
}
67+
} else {
68+
$argTypes[] = $argType;
69+
if (!$argType instanceof ConstantArrayType) {
70+
$allConstant = false;
71+
}
72+
}
4973
}
5074

5175
if ($allConstant) {
5276
$newArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
53-
foreach ($args as $i => $arg) {
54-
$argType = $argTypes[$i];
55-
77+
foreach ($argTypes as $argType) {
5678
if (!$argType instanceof ConstantArrayType) {
5779
throw new ShouldNotHappenException();
5880
}
@@ -78,22 +100,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
78100
$keyTypes = [];
79101
$valueTypes = [];
80102
$nonEmpty = false;
81-
foreach ($args as $i => $arg) {
82-
$argType = $argTypes[$i];
83-
84-
if ($arg->unpack) {
85-
$argType = $argType->getIterableValueType();
86-
if ($argType instanceof UnionType) {
87-
foreach ($argType->getTypes() as $innerType) {
88-
$argType = $innerType;
89-
}
90-
}
91-
}
92-
103+
foreach ($argTypes as $key => $argType) {
93104
$keyTypes[] = $argType->getIterableKeyType();
94105
$valueTypes[] = $argType->getIterableValueType();
95106

96-
if (!$argType->isIterableAtLeastOnce()->yes()) {
107+
if (in_array($key, $optionalArgTypes, true) || !$argType->isIterableAtLeastOnce()->yes()) {
97108
continue;
98109
}
99110

tests/PHPStan/Analyser/data/array-merge.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,26 @@ function foo(): void
1313

1414
assertType('array{foo: 17, 0: \'a\', bar: 19, 1: \'b\', 2: \'b\', 3: \'c\'}', $baz);
1515
}
16+
17+
/**
18+
* @param string[][] $foo
19+
* @param array<non-empty-array<int>> $bar1
20+
* @param non-empty-array<non-empty-array<int>> $bar2
21+
* @param non-empty-array<array<int>> $bar3
22+
*/
23+
function unpackingArrays(array $foo, array $bar1, array $bar2, array $bar3): void
24+
{
25+
assertType('array<string>', array_merge([], ...$foo));
26+
assertType('array<int>', array_merge([], ...$bar1));
27+
assertType('non-empty-array<int>', array_merge([], ...$bar2));
28+
assertType('array<int>', array_merge([], ...$bar3));
29+
}
30+
31+
function unpackingConstantArrays(): void
32+
{
33+
assertType('array{}', array_merge([], ...[]));
34+
assertType('array{17}', array_merge([], [17]));
35+
assertType('array{17}', array_merge([], ...[[17]]));
36+
assertType('array{foo: \'bar\', bar: \'baz2\', 0: 17}', array_merge(['foo' => 'bar', 'bar' => 'baz1'], ['bar' => 'baz2', 17]));
37+
assertType('array{foo: \'bar\', bar: \'baz2\', 0: 17}', array_merge(['foo' => 'bar', 'bar' => 'baz1'], ...[['bar' => 'baz2', 17]]));
38+
}

0 commit comments

Comments
 (0)