|
53 | 53 | use PHPStan\Type\Constant\ConstantStringType; |
54 | 54 | use PHPStan\Type\ConstantTypeHelper; |
55 | 55 | use PHPStan\Type\ErrorType; |
| 56 | +use PHPStan\Type\GeneralizePrecision; |
56 | 57 | use PHPStan\Type\IntegerRangeType; |
57 | 58 | use PHPStan\Type\MixedType; |
58 | 59 | use PHPStan\Type\ObjectType; |
@@ -987,15 +988,14 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar |
987 | 988 | $isLastDimFetchInChain = $i === 0; |
988 | 989 | $isFirstDimFetchInChain = $i === $lastOffsetIndex; |
989 | 990 |
|
990 | | - $unionValues = $isLastDimFetchInChain; |
991 | 991 | if ( |
992 | 992 | !$isLastDimFetchInChain |
993 | 993 | && $isFirstDimFetchInChain |
994 | 994 | && $offsetType !== null |
995 | 995 | ) { |
996 | | - $unionValues = $this->shouldUnionExistingItemType($offsetValueType, $valueToWrite); |
| 996 | + $valueToWrite = $this->mergeComposedValueWithExistingItemType($offsetValueType, $valueToWrite); |
997 | 997 | } |
998 | | - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues); |
| 998 | + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $isLastDimFetchInChain); |
999 | 999 | } |
1000 | 1000 |
|
1001 | 1001 | if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { |
@@ -1092,32 +1092,74 @@ private function isSameVariable(Expr $a, Expr $b): bool |
1092 | 1092 |
|
1093 | 1093 | /** |
1094 | 1094 | * 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. |
1098 | 1101 | */ |
1099 | | - private function shouldUnionExistingItemType(Type $offsetValueType, Type $composedValue): bool |
| 1102 | + private function mergeComposedValueWithExistingItemType(Type $offsetValueType, Type $composedValue): Type |
1100 | 1103 | { |
1101 | 1104 | $existingItemType = $offsetValueType->getIterableValueType(); |
1102 | 1105 |
|
1103 | 1106 | if (!$existingItemType->isConstantArray()->yes() || !$composedValue->isConstantArray()->yes()) { |
1104 | | - return false; |
| 1107 | + return $composedValue; |
1105 | 1108 | } |
1106 | 1109 |
|
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 | + } |
1116 | 1146 | } |
1117 | 1147 | } |
| 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 | + } |
1118 | 1160 | } |
1119 | 1161 |
|
1120 | | - return false; |
| 1162 | + return TypeCombinator::union(...$mergedArrays); |
1121 | 1163 | } |
1122 | 1164 |
|
1123 | 1165 | } |
0 commit comments