Skip to content

Commit a41c7be

Browse files
phpstan-botclaude
andcommitted
Fix Shopware regression by merging composed values with generalized int/float
Replace shouldUnionExistingItemType (which used unionValues parameter) with mergeComposedValueWithExistingItemType that does key-by-key merging of composed and existing array item types. Integer and float values are generalized during merge to avoid growing unions (e.g. count: 0|1|2|... → int) that would destabilize loop widening and cause types to degrade to mixed. This fixes the Shopware ConnectionProfiler regression where array types degraded to non-empty-array<..., mixed> while preserving the bug-8270 fix where modifying one element's sub-key with a non-constant array key now correctly reflects both modified and unmodified element types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d301c50 commit a41c7be

177 files changed

Lines changed: 66300 additions & 18 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use PHPStan\Type\Constant\ConstantStringType;
5454
use PHPStan\Type\ConstantTypeHelper;
5555
use PHPStan\Type\ErrorType;
56+
use PHPStan\Type\GeneralizePrecision;
5657
use PHPStan\Type\IntegerRangeType;
5758
use PHPStan\Type\MixedType;
5859
use PHPStan\Type\ObjectType;
@@ -987,15 +988,14 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
987988
$isLastDimFetchInChain = $i === 0;
988989
$isFirstDimFetchInChain = $i === $lastOffsetIndex;
989990

990-
$unionValues = $isLastDimFetchInChain;
991991
if (
992992
!$isLastDimFetchInChain
993993
&& $isFirstDimFetchInChain
994994
&& $offsetType !== null
995995
) {
996-
$unionValues = $this->shouldUnionExistingItemType($offsetValueType, $valueToWrite);
996+
$valueToWrite = $this->mergeComposedValueWithExistingItemType($offsetValueType, $valueToWrite);
997997
}
998-
$valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues);
998+
$valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $isLastDimFetchInChain);
999999
}
10001000

10011001
if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) {
@@ -1092,32 +1092,74 @@ private function isSameVariable(Expr $a, Expr $b): bool
10921092

10931093
/**
10941094
* When modifying a nested array dimension with a non-constant key,
1095-
* check if the composed value changes any existing constant-array
1096-
* key values. If it does, the existing item type should be unioned
1097-
* because unmodified elements still have their original types.
1095+
* merge the composed value with the existing item type. For keys
1096+
* where the value changed, the result uses the union of old and new
1097+
* values, because unmodified elements still have their original types.
1098+
* Integer and float values are generalized to avoid growing unions
1099+
* through loop iterations. Returns the composed value unchanged
1100+
* if no merge is needed.
10981101
*/
1099-
private function shouldUnionExistingItemType(Type $offsetValueType, Type $composedValue): bool
1102+
private function mergeComposedValueWithExistingItemType(Type $offsetValueType, Type $composedValue): Type
11001103
{
11011104
$existingItemType = $offsetValueType->getIterableValueType();
11021105

11031106
if (!$existingItemType->isConstantArray()->yes() || !$composedValue->isConstantArray()->yes()) {
1104-
return false;
1107+
return $composedValue;
11051108
}
11061109

1107-
foreach ($existingItemType->getConstantArrays() as $existingArray) {
1108-
foreach ($existingArray->getKeyTypes() as $i => $keyType) {
1109-
if ($composedValue->hasOffsetValueType($keyType)->no()) {
1110-
continue;
1111-
}
1112-
$existingValue = $existingArray->getValueTypes()[$i];
1113-
$newValue = $composedValue->getOffsetValueType($keyType);
1114-
if (!$newValue->isSuperTypeOf($existingValue)->yes()) {
1115-
return true;
1110+
$existingArrays = $existingItemType->getConstantArrays();
1111+
1112+
$mergedArrays = [];
1113+
foreach ($composedValue->getConstantArrays() as $composedArray) {
1114+
$keyTypes = $composedArray->getKeyTypes();
1115+
$valueTypes = $composedArray->getValueTypes();
1116+
$changed = false;
1117+
1118+
foreach ($existingArrays as $existingArray) {
1119+
foreach ($existingArray->getKeyTypes() as $ei => $existingKeyType) {
1120+
foreach ($keyTypes as $ci => $composedKeyType) {
1121+
if ($composedKeyType->getValue() !== $existingKeyType->getValue()) {
1122+
continue;
1123+
}
1124+
$existingValue = $existingArray->getValueTypes()[$ei];
1125+
$newValue = $valueTypes[$ci];
1126+
1127+
if (!$newValue->isSuperTypeOf($existingValue)->yes()) {
1128+
// Generalize integer/float values to avoid growing unions
1129+
// through loop iterations (e.g. count: 0|1|2|... → int)
1130+
if (
1131+
($existingValue->isInteger()->yes() && $newValue->isInteger()->yes())
1132+
|| ($existingValue->isFloat()->yes() && $newValue->isFloat()->yes())
1133+
) {
1134+
$valueTypes[$ci] = TypeCombinator::union(
1135+
$existingValue->generalize(GeneralizePrecision::lessSpecific()),
1136+
$newValue->generalize(GeneralizePrecision::lessSpecific()),
1137+
);
1138+
} else {
1139+
$valueTypes[$ci] = TypeCombinator::union($existingValue, $newValue);
1140+
}
1141+
$changed = true;
1142+
}
1143+
1144+
break;
1145+
}
11161146
}
11171147
}
1148+
1149+
if ($changed) {
1150+
$mergedArrays[] = new ConstantArrayType(
1151+
$keyTypes,
1152+
$valueTypes,
1153+
$composedArray->getNextAutoIndexes(),
1154+
$composedArray->getOptionalKeys(),
1155+
$composedArray->isList(),
1156+
);
1157+
} else {
1158+
$mergedArrays[] = $composedArray;
1159+
}
11181160
}
11191161

1120-
return false;
1162+
return TypeCombinator::union(...$mergedArrays);
11211163
}
11221164

11231165
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace AnonymousClassNameInTrait;
4+
5+
class Foo
6+
{
7+
8+
use FooTrait;
9+
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace AnonymousClassNameSameLine;
4+
5+
$foo = new class {}; $bar = new class {}; $baz = new class {}; die;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace AnonymousClassName;
4+
5+
function () {
6+
$foo = new class () {
7+
8+
/** @var Foo */
9+
public $fooProperty;
10+
11+
/**
12+
* @return Foo
13+
*/
14+
public function doFoo()
15+
{
16+
'inside';
17+
}
18+
};
19+
20+
'outside';
21+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace AnonymousFunction;
4+
5+
function () {
6+
$integer = 1;
7+
function (string $str, ...$arr) use ($integer, $bar) {
8+
die;
9+
};
10+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace ArrayAccesable;
4+
5+
class Foo implements \ArrayAccess
6+
{
7+
8+
public function __construct()
9+
{
10+
die;
11+
}
12+
13+
/**
14+
* @return string[]
15+
*/
16+
public function returnArrayOfStrings(): array
17+
{
18+
19+
}
20+
21+
/**
22+
* @return mixed
23+
*/
24+
public function returnMixed()
25+
{
26+
27+
}
28+
29+
/**
30+
* @return self|int[]
31+
*/
32+
public function returnSelfWithIterableInt(): self
33+
{
34+
35+
}
36+
37+
#[\ReturnTypeWillChange]
38+
public function offsetExists($offset)
39+
{
40+
41+
}
42+
43+
public function offsetGet($offset): int
44+
{
45+
46+
}
47+
48+
#[\ReturnTypeWillChange]
49+
public function offsetSet($offset, $value)
50+
{
51+
52+
}
53+
54+
#[\ReturnTypeWillChange]
55+
public function offsetUnset($offset)
56+
{
57+
58+
}
59+
60+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
function (\stdClass $obj) {
3+
/** @var mixed $array */
4+
$array = getMixed();
5+
[$a, $b, [$c]] = $array;
6+
list($aList, $bList, list($cList)) = $array;
7+
8+
$constantArray = [1, 'foo', [true]];
9+
[$int, $string, [$bool, $nestedNever], $never] = $constantArray;
10+
list($intList, $stringList, list($boolList, $nestedNeverList), $neverList) = $constantArray;
11+
12+
$unionArray = $foo ? [1, 2, 3] : [4, 'bar'];
13+
[$u1, $u2, $u3] = $unionArray;
14+
15+
foreach ([[1, [false]]] as [$foreachInt, [$foreachBool, $foreachNestedNever], $foreachNever]) {
16+
17+
}
18+
19+
foreach ([[1, [false]]] as list($foreachIntList, list($foreachBoolList, $foreachNestedNeverList), $foreachNeverList)) {
20+
21+
}
22+
23+
foreach ([$unionArray] as [$foreachU1, $foreachU2, $foreachU3]) {
24+
25+
}
26+
27+
/** @var string[] $stringArray */
28+
$stringArray = getStringArray();
29+
[$firstStringArray, $secondStringArray, [$thirdStringArray], $fourthStringArray] = $stringArray;
30+
list($firstStringArrayList, $secondStringArrayList, list($thirdStringArrayList), $fourthStringArrayList) = $stringArray;
31+
32+
foreach ($stringArray as [$firstStringArrayForeach, $secondStringArrayForeach, [$thirdStringArrayForeach], $fourthStringArrayForeach]) {
33+
34+
}
35+
36+
foreach ($stringArray as list($firstStringArrayForeachList, $secondStringArrayForeachList, list($thirdStringArrayForeachList), $fourthStringArrayForeachList)) {
37+
38+
}
39+
40+
/** @var int $dayInt */
41+
$dayInt = getInt($dayInt);
42+
$dateArray = ['d' => $dayInt];
43+
[$dateArray['Y'], $dateArray['m']] = explode('-', '2018-12-19');
44+
45+
/** @var int $firstIntElement */
46+
$firstIntElement = getInt();
47+
/** @var int $secondIntElement */
48+
$secondIntElement = getInt();
49+
$intArrayForRewritingFirstElement = [$firstIntElement, $secondIntElement];
50+
[$intArrayForRewritingFirstElement[0]] = explode('*', '');
51+
52+
[$newArray['newKey']] = [new stdClass(), new stdClass()];
53+
54+
[$obj[0]] = ['error', 'error-error'];
55+
56+
$constantAssocArray = [1, 'foo', 'key' => true, 'value' => '123'];
57+
['key' => $assocKey, 0 => $assocOne, 1 => $assocFoo, 'non-existent' => $assocNonExistent] = $constantAssocArray;
58+
59+
$fooKey = 'key';
60+
/** @var string $stringKey */
61+
$stringKey = getString();
62+
/** @var mixed $mixedKey */
63+
$mixedKey = getMixed();
64+
[$fooKey => $dynamicAssocKey, $stringKey => $dynamicAssocStrings, $mixedKey => $dynamicAssocMixed] = $constantAssocArray;
65+
66+
foreach ([$constantAssocArray] as [$fooKey => $dynamicAssocKeyForeach, $stringKey => $dynamicAssocStringsForeach, $mixedKey => $dynamicAssocMixedForeach]) {
67+
68+
}
69+
70+
/** @var iterable<array<string>> $iterableOverStringArrays */
71+
$iterableOverStringArrays = doFoo();
72+
foreach ($iterableOverStringArrays as [$stringFromIterable]) {
73+
74+
}
75+
76+
/** @var string $stringWithVarAnnotation */
77+
[$stringWithVarAnnotation] = doFoo();
78+
79+
/** @var string $stringWithVarAnnotationInForeach */
80+
foreach (doFoo() as [$stringWithVarAnnotationInForeach]) {
81+
82+
}
83+
84+
die;
85+
};

0 commit comments

Comments
 (0)