Skip to content

Commit f21b218

Browse files
vokuondrejmirtes
authored andcommitted
support array-shapes in "array_merge"
1 parent bd0b492 commit f21b218

File tree

4 files changed

+116
-17
lines changed

4 files changed

+116
-17
lines changed

src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
use PHPStan\Reflection\ParametersAcceptorSelector;
99
use PHPStan\Type\Accessory\NonEmptyArrayType;
1010
use PHPStan\Type\ArrayType;
11+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1112
use PHPStan\Type\GeneralizePrecision;
1213
use PHPStan\Type\Type;
1314
use PHPStan\Type\TypeCombinator;
1415
use PHPStan\Type\TypeUtils;
15-
use PHPStan\Type\UnionType;
1616

1717
class ArrayMergeFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
1818
{
@@ -30,20 +30,33 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3030

3131
$keyTypes = [];
3232
$valueTypes = [];
33+
$returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
34+
$returnedArrayBuilderFilled = false;
3335
$nonEmpty = false;
3436
foreach ($functionCall->args as $arg) {
3537
$argType = $scope->getType($arg->value);
38+
3639
if ($arg->unpack) {
3740
$argType = $argType->getIterableValueType();
38-
if ($argType instanceof UnionType) {
39-
foreach ($argType->getTypes() as $innerType) {
40-
$argType = $innerType;
41+
}
42+
43+
$arrays = TypeUtils::getConstantArrays($argType);
44+
if (count($arrays) > 0) {
45+
foreach ($arrays as $constantArray) {
46+
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
47+
$returnedArrayBuilderFilled = true;
48+
49+
$returnedArrayBuilder->setOffsetValueType(
50+
is_numeric($keyType->getValue()) ? null : $keyType,
51+
$constantArray->getValueTypes()[$i]
52+
);
4153
}
4254
}
43-
}
4455

45-
$keyTypes[] = TypeUtils::generalizeType($argType->getIterableKeyType(), GeneralizePrecision::moreSpecific());
46-
$valueTypes[] = $argType->getIterableValueType();
56+
} else {
57+
$keyTypes[] = TypeUtils::generalizeType($argType->getIterableKeyType(), GeneralizePrecision::moreSpecific());
58+
$valueTypes[] = $argType->getIterableValueType();
59+
}
4760

4861
if (!$argType->isIterableAtLeastOnce()->yes()) {
4962
continue;
@@ -52,10 +65,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
5265
$nonEmpty = true;
5366
}
5467

55-
$arrayType = new ArrayType(
56-
TypeCombinator::union(...$keyTypes),
57-
TypeCombinator::union(...$valueTypes)
58-
);
68+
if (count($keyTypes) > 0) {
69+
$arrayType = new ArrayType(
70+
TypeCombinator::union(...$keyTypes),
71+
TypeCombinator::union(...$valueTypes)
72+
);
73+
74+
if ($returnedArrayBuilderFilled) {
75+
$arrayType = TypeCombinator::union($returnedArrayBuilder->getArray(), $arrayType);
76+
}
77+
} elseif ($returnedArrayBuilderFilled) {
78+
$arrayType = $returnedArrayBuilder->getArray();
79+
} else {
80+
$arrayType = new ArrayType(
81+
TypeCombinator::union(...$keyTypes),
82+
TypeCombinator::union(...$valueTypes)
83+
);
84+
}
5985

6086
if ($nonEmpty) {
6187
$arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5153,31 +5153,44 @@ public function dataArrayFunctions(): array
51535153
'array_values($generalStringKeys)',
51545154
],
51555155
[
5156-
'array<int|non-empty-string, stdClass>&nonEmpty',
5156+
"array('foo' => stdClass, 0 => stdClass)",
51575157
'array_merge($stringOrIntegerKeys)',
51585158
],
51595159
[
51605160
'array<int|string, DateTimeImmutable|int>',
51615161
'array_merge($generalStringKeys, $generalDateTimeValues)',
51625162
],
51635163
[
5164-
'array<int|string, int|stdClass>&nonEmpty',
5164+
'array<0|string, int|stdClass>&nonEmpty',
51655165
'array_merge($generalStringKeys, $stringOrIntegerKeys)',
51665166
],
51675167
[
5168-
'array<int|string, int|stdClass>&nonEmpty',
5168+
'array<0|string, int|stdClass>&nonEmpty',
51695169
'array_merge($stringOrIntegerKeys, $generalStringKeys)',
51705170
],
51715171
[
5172-
'array<int|non-empty-string, \'foo\'|stdClass>&nonEmpty',
5172+
"array('foo' => stdClass, 'bar' => stdClass, 0 => stdClass)",
51735173
'array_merge($stringKeys, $stringOrIntegerKeys)',
51745174
],
51755175
[
5176-
'array<int|non-empty-string, \'foo\'|stdClass>&nonEmpty',
5176+
"array('foo' => 1, 'bar' => 2, 0 => 2, 1 => 3)",
5177+
"array_merge(['foo' => 4, 'bar' => 5], ...[['foo' => 1, 'bar' => 2], [2, 3]])",
5178+
],
5179+
[
5180+
"array('foo' => 1, 'foo2' => stdClass)",
5181+
'array_merge([\'foo\' => new stdClass()], ...[[\'foo2\' => new stdClass()], [\'foo\' => 1]])',
5182+
],
5183+
5184+
[
5185+
"array('foo' => 1, 'foo2' => stdClass)",
5186+
'array_merge([\'foo\' => new stdClass()], ...[[\'foo2\' => new stdClass()], [\'foo\' => 1]])',
5187+
],
5188+
[
5189+
"array('foo' => 'foo', 0 => stdClass, 'bar' => stdClass)",
51775190
'array_merge($stringOrIntegerKeys, $stringKeys)',
51785191
],
51795192
[
5180-
'array<int|non-empty-string, 2|4|\'a\'|\'b\'|\'green\'|\'red\'|\'trapezoid\'>&nonEmpty',
5193+
"array('color' => 'green', 0 => 2, 1 => 4, 2 => 'a', 3 => 'b', 'shape' => 'trapezoid', 4 => 4)",
51815194
'array_merge(array("color" => "red", 2, 4), array("a", "b", "color" => "green", "shape" => "trapezoid", 4))',
51825195
],
51835196
[

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ public function dataFileAsserts(): iterable
134134
yield from $this->gatherAssertTypes(__DIR__ . '/data/throw-expr.php');
135135
}
136136

137+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-merge.php');
138+
137139
yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php');
138140

139141
if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace NonEmptyArray;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param array{foo: '1', bar: '2', lall: '3', 2: '2', 3: '3'} $array1
12+
* @param array{foo: '1', bar: '4', lall2: '3', 2: '4', 3: '6'} $array2
13+
*/
14+
public function arrayMergeArrayShapes($array1, $array2): void
15+
{
16+
assertType("array('foo' => '1', 'bar' => '2', 'lall' => '3', 0 => '2', 1 => '3')", array_merge($array1));
17+
assertType("array('foo' => '1', 'bar' => '2', 'lall' => '3', 0 => '2', 1 => '3')", array_merge([], $array1));
18+
assertType("array('foo' => '1', 'bar' => '2', 'lall' => '3', 0 => '2', 1 => '3')", array_merge($array1, []));
19+
assertType("array('foo' => '1', 'bar' => '2', 'lall' => '3', 0 => '2', 1 => '3', 2 => '2', 3 => '3')", array_merge($array1, $array1));
20+
assertType("array('foo' => '1', 'bar' => '4', 'lall' => '3', 0 => '2', 1 => '3', 'lall2' => '3', 2 => '4', 3 => '6')", array_merge($array1, $array2));
21+
assertType("array('foo' => '1', 'bar' => '2', 'lall2' => '3', 0 => '4', 1 => '6', 'lall' => '3', 2 => '2', 3 => '3')", array_merge($array2, $array1));
22+
assertType("array('foo' => 3, 'bar' => '2', 'lall2' => '3', 0 => '4', 1 => '6', 'lall' => '3', 2 => '2', 3 => '3')", array_merge($array2, $array1, ['foo' => 3]));
23+
assertType("array('foo' => 3, 'bar' => '2', 'lall2' => '3', 0 => '4', 1 => '6', 'lall' => '3', 2 => '2', 3 => '3')", array_merge($array2, $array1, ...[['foo' => 3]]));
24+
}
25+
26+
/**
27+
* @param int[] $array1
28+
* @param string[] $array2
29+
*/
30+
public function arrayMergeSimple($array1, $array2): void
31+
{
32+
assertType("array<int>", array_merge($array1, $array1));
33+
assertType("array<int|string>", array_merge($array1, $array2));
34+
assertType("array<int|string>", array_merge($array2, $array1));
35+
}
36+
37+
/**
38+
* @param array<int, int|string> $array1
39+
* @param array<int, bool|float> $array2
40+
*/
41+
public function arrayMergeUnionType($array1, $array2): void
42+
{
43+
assertType("array<int, int|string>", array_merge($array1, $array1));
44+
assertType("array<int, bool|float|int|string>", array_merge($array1, $array2));
45+
assertType("array<int, bool|float|int|string>", array_merge($array2, $array1));
46+
}
47+
48+
/**
49+
* @param array<int, array{bar: '2'}|array{foo: '1'}> $array1
50+
* @param array<int, array{bar: '3'}|array{foo: '2'}> $array2
51+
*/
52+
public function arrayMergeUnionTypeArrayShapes($array1, $array2): void
53+
{
54+
assertType("array<int, array('bar' => '2')|array('foo' => '1')>", array_merge($array1, $array1));
55+
assertType("array<int, array('bar' => '2'|'3')|array('foo' => '1'|'2')>", array_merge($array1, $array2));
56+
assertType("array<int, array('bar' => '2'|'3')|array('foo' => '1'|'2')>", array_merge($array2, $array1));
57+
}
58+
}

0 commit comments

Comments
 (0)