Skip to content

Commit 607b822

Browse files
committed
infer HasOffsetValueType for last occuring string-keys
1 parent 04d9ad8 commit 607b822

File tree

3 files changed

+57
-15
lines changed

3 files changed

+57
-15
lines changed

src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
use PHPStan\Type\TypeCombinator;
2424
use PHPStan\Type\TypeUtils;
2525
use function array_keys;
26-
use function array_values;
2726
use function count;
2827
use function in_array;
28+
use function is_int;
29+
use function is_string;
2930

3031
#[AutowiredService]
3132
final class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -93,10 +94,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
9394
continue;
9495
}
9596

96-
$offsetTypes[$keyType->getValue()] = new HasOffsetType($keyType);
97+
$offsetValueType = $constantArray->getOffsetValueType($keyType);
98+
$offsetTypes[$keyType->getValue()] = [false, $offsetValueType];
9799
}
98100
}
99101

102+
if ($keyTypes === []) {
103+
foreach ($offsetTypes as [&$generalize, $offsetType]) {
104+
$generalize = true;
105+
}
106+
unset($generalize);
107+
}
108+
100109
if ($newArrayBuilder === null) {
101110
foreach (TypeUtils::getAccessoryTypes($argType) as $accessoryType) {
102111
if (
@@ -107,8 +116,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
107116
}
108117

109118
$offsetType = $accessoryType->getOffsetType();
110-
$offsetTypes[$offsetType->getValue()] = new HasOffsetType($offsetType);
111-
119+
$offsetValueType = $argType->getOffsetValueType($offsetType);
120+
$offsetTypes[$offsetType->getValue()] = [false, $offsetValueType];
112121
}
113122

114123
continue;
@@ -164,7 +173,32 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
164173
$arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType());
165174
}
166175
if ($offsetTypes !== []) {
167-
$arrayType = TypeCombinator::intersect($arrayType, ...array_values($offsetTypes));
176+
$knownOffsetValues = [];
177+
foreach ($offsetTypes as $key => [$generalize, $offsetType]) {
178+
if (is_int($key)) {
179+
// int keys will be appended and renumbered.
180+
// at this point we can't reason about them, because unknown arrays are in the mix.
181+
continue;
182+
}
183+
$keyType = new ConstantStringType($key);
184+
185+
if (!$generalize && is_string($key)) {
186+
// the last string-keyed offset will overwrite previous values
187+
$hasOffsetType = new HasOffsetValueType(
188+
$keyType,
189+
$offsetType,
190+
);
191+
} else {
192+
$hasOffsetType = new HasOffsetType(
193+
$keyType,
194+
);
195+
}
196+
197+
$knownOffsetValues[] = $hasOffsetType;
198+
}
199+
if ($knownOffsetValues !== []) {
200+
$arrayType = TypeCombinator::intersect($arrayType, ...$knownOffsetValues);
201+
}
168202
}
169203

170204
return $arrayType;

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4697,11 +4697,11 @@ public static function dataArrayFunctions(): array
46974697
'array_merge($generalStringKeys, $generalDateTimeValues)',
46984698
],
46994699
[
4700-
"non-empty-array<1|string, int|stdClass>&hasOffset('foo')&hasOffset(1)",
4700+
"non-empty-array<1|string, int|stdClass>&hasOffsetValue('foo', stdClass)",
47014701
'array_merge($generalStringKeys, $stringOrIntegerKeys)',
47024702
],
47034703
[
4704-
"non-empty-array<1|string, int|stdClass>&hasOffset('foo')&hasOffset(1)",
4704+
"non-empty-array<1|string, int|stdClass>&hasOffset('foo')",
47054705
'array_merge($stringOrIntegerKeys, $generalStringKeys)',
47064706
],
47074707
[

tests/PHPStan/Analyser/nsrt/array-merge-const-non-const.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,26 @@
55
use function PHPStan\Testing\assertType;
66

77
function doFoo(array $post): void {
8-
assertType("non-empty-array&hasOffset('a')&hasOffset('b')", array_merge(['a' => 1, 'b' => false], $post));
8+
assertType("non-empty-array&hasOffset('a')&hasOffset('b')", array_merge(['a' => 1, 'b' => false, 10 => 99], $post));
99
}
1010

1111
function doBar(array $array): void {
12-
assertType("non-empty-array&hasOffset('a')&hasOffset('b')", array_merge($array, ['a' => 1, 'b' => false]));
12+
assertType("non-empty-array&hasOffsetValue('a', 1)&hasOffsetValue('b', false)", array_merge($array, ['a' => 1, 'b' => false, 10 => 99]));
1313
}
1414

1515
function doFooBar(array $array): void {
16-
assertType("non-empty-array&hasOffset('a')&hasOffset('b')&hasOffset('c')", array_merge(['c' => 'd'], $array, ['a' => 1, 'b' => false, 'c' => 'e']));
16+
assertType("non-empty-array&hasOffset('x')&hasOffsetValue('a', 1)&hasOffsetValue('b', false)&hasOffsetValue('c', 'e')", array_merge(['c' => 'd', 'x' => 'y'], $array, ['a' => 1, 'b' => false, 'c' => 'e']));
1717
}
1818

1919
function doFooInts(array $array): void {
20-
assertType("non-empty-array&hasOffset('a')&hasOffset('c')&hasOffset(1)&hasOffset(3)", array_merge([1 => 'd'], $array, ['a' => 1, 3 => false, 'c' => 'e']));
20+
assertType("non-empty-array&hasOffsetValue('a', 1)&hasOffsetValue('c', 'e')", array_merge([1 => 'd'], $array, ['a' => 1, 3 => false, 'c' => 'e']));
2121
}
2222

2323
/**
2424
* @param array<string> $array
2525
*/
2626
function floatKey(array $array): void {
27-
assertType("non-empty-array<string>&hasOffset('a')&hasOffset('c')&hasOffset(3)&hasOffset(4)", array_merge([4.23 => 'd'], $array, ['a' => '1', 3 => 'false', 'c' => 'e']));
27+
assertType("non-empty-array<string>&hasOffsetValue('a', '1')&hasOffsetValue('c', 'e')", array_merge([4.23 => 'd'], $array, ['a' => '1', 3 => 'false', 'c' => 'e']));
2828
}
2929

3030
function doOptKeys(array $array, array $arr2): void {
@@ -38,17 +38,25 @@ function doOptKeys(array $array, array $arr2): void {
3838
* @param array{a?: 1, b: 2} $array
3939
*/
4040
function doOptShapeKeys(array $array, array $arr2): void {
41-
assertType("non-empty-array&hasOffset('b')", array_merge($arr2, $array));
41+
assertType("non-empty-array&hasOffsetValue('b', 2)", array_merge($arr2, $array));
4242
}
4343

4444
function hasOffsetKeys(array $array, array $arr2): void {
4545
if (array_key_exists('b', $array)) {
46-
assertType("non-empty-array&hasOffset('b')", array_merge($arr2, $array));
46+
assertType("non-empty-array&hasOffsetValue('b', mixed)", array_merge($arr2, $array));
4747
}
4848
}
4949

5050
function hasOffsetValueKeys(array $array, array $arr2): void {
5151
$array['b'] = 123;
5252

53-
assertType("non-empty-array&hasOffset('b')", array_merge($arr2, $array));
53+
assertType("non-empty-array&hasOffsetValue('b', 123)", array_merge($arr2, $array));
54+
}
55+
56+
/**
57+
* @param array{a?: 1, b?: 2} $allOptional
58+
*/
59+
function doAllOptional(array $allOptional, array $arr2): void {
60+
assertType("array", array_merge($arr2, $allOptional));
61+
assertType("array", array_merge($allOptional, $arr2));
5462
}

0 commit comments

Comments
 (0)