Skip to content

Commit ebc5354

Browse files
Fix array + array inference (#4944)
1 parent 4285599 commit ebc5354

File tree

3 files changed

+75
-6
lines changed

3 files changed

+75
-6
lines changed

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,18 +1499,37 @@ public function getPlusTypeFromTypes(Expr $left, Expr $right, Type $leftType, Ty
14991499
$keyType = TypeCombinator::union(...$keyTypes);
15001500
}
15011501

1502+
$leftIterableValueType = $leftType->getIterableValueType();
15021503
$arrayType = new ArrayType(
15031504
$keyType,
1504-
TypeCombinator::union($leftType->getIterableValueType(), $rightType->getIterableValueType()),
1505+
TypeCombinator::union($leftIterableValueType, $rightType->getIterableValueType()),
15051506
);
15061507

15071508
$accessories = [];
1508-
foreach ($leftType->getConstantArrays() as $type) {
1509-
foreach ($type->getKeyTypes() as $i => $offsetType) {
1510-
if ($type->isOptionalKey($i)) {
1509+
if ($leftCount > 0) {
1510+
// Use the first constant array as a reference to list potential offsets.
1511+
// We only need to check the first array because we're looking for offsets that exist in ALL arrays.
1512+
$constantArray = $leftConstantArrays[0];
1513+
foreach ($constantArray->getKeyTypes() as $offsetType) {
1514+
if (!$leftType->hasOffsetValueType($offsetType)->yes()) {
15111515
continue;
15121516
}
1513-
$valueType = $type->getValueTypes()[$i];
1517+
1518+
$valueType = $leftType->getOffsetValueType($offsetType);
1519+
$accessories[] = new HasOffsetValueType($offsetType, $valueType);
1520+
}
1521+
}
1522+
1523+
if ($rightCount > 0) {
1524+
// Use the first constant array as a reference to list potential offsets.
1525+
// We only need to check the first array because we're looking for offsets that exist in ALL arrays.
1526+
$constantArray = $rightConstantArrays[0];
1527+
foreach ($constantArray->getKeyTypes() as $offsetType) {
1528+
if (!$rightType->hasOffsetValueType($offsetType)->yes()) {
1529+
continue;
1530+
}
1531+
1532+
$valueType = TypeCombinator::union($leftIterableValueType, $rightType->getOffsetValueType($offsetType));
15141533
$accessories[] = new HasOffsetValueType($offsetType, $valueType);
15151534
}
15161535
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2768,7 +2768,7 @@ public static function dataBinaryOperations(): array
27682768
'[1, 2, 3] + [4, 5, 6]',
27692769
],
27702770
[
2771-
'non-empty-array<int>',
2771+
'non-empty-array<int>&hasOffsetValue(0, int)&hasOffsetValue(1, int)&hasOffsetValue(2, int)',
27722772
'$arrayOfUnknownIntegers + [1, 2, 3]',
27732773
],
27742774
[
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug13552;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
interface MyInterface {
8+
public function doThing(): bool;
9+
10+
/**
11+
* @return array<string, string>
12+
*/
13+
public function getArray(): array;
14+
}
15+
16+
function test_addition(MyInterface $i): void {
17+
$x = $i->doThing() ? ['thing' => 'do'] : [];
18+
assertType("array{}|array{thing: 'do'}", $x);
19+
20+
$x += $i->getArray();
21+
assertType('array<string, string>', $x);
22+
23+
$x = $x ?: ['test' => 'string'];
24+
}
25+
26+
function more_test(MyInterface $i): void {
27+
$x = $i->doThing() ? ['thing' => 'do', 'always_here' => true] : ['always_here' => 42];
28+
assertType("array{always_here: 42}|array{thing: 'do', always_here: true}", $x);
29+
30+
$a = $i->getArray() + $x;
31+
assertType("non-empty-array<string, 42|string|true>&hasOffsetValue('always_here', 42|string|true)", $a);
32+
assertType('true', isset($a['always_here']));
33+
34+
$b = $x + $i->getArray();
35+
assertType("non-empty-array<string, 42|string|true>&hasOffsetValue('always_here', 42|true)", $b);
36+
assertType('true', isset($b['always_here']));
37+
}
38+
39+
/**
40+
* @param array{thing?: 'do', always_here: 42|true} $x
41+
*/
42+
function more_test_2(MyInterface $i, array $x): void {
43+
$a = $i->getArray() + $x;
44+
assertType("non-empty-array<string, 42|string|true>&hasOffsetValue('always_here', 42|string|true)", $a);
45+
assertType('true', isset($a['always_here']));
46+
47+
$b = $x + $i->getArray();
48+
assertType("non-empty-array<string, 42|string|true>&hasOffsetValue('always_here', 42|true)", $b);
49+
assertType('true', isset($b['always_here']));
50+
}

0 commit comments

Comments
 (0)