diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 89b53c46c2..68f1d28a89 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -939,7 +939,9 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $offsetValueTypeStack[] = $offsetValueType; } - foreach (array_reverse($offsetTypes) as $i => [$offsetType]) { + $reversedOffsetTypes = array_reverse($offsetTypes); + $lastOffsetIndex = count($reversedOffsetTypes) - 1; + foreach ($reversedOffsetTypes as $i => [$offsetType]) { /** @var Type $offsetValueType */ $offsetValueType = array_pop($offsetValueTypeStack); if ( @@ -980,7 +982,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } else { - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + $unionValues = $i === 0; + if (!$unionValues && $i === $lastOffsetIndex && $offsetType !== null) { + $unionValues = $this->shouldUnionExistingItemType($offsetValueType, $valueToWrite); + } + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues); } if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { @@ -1029,4 +1035,34 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar return [$valueToWrite, $additionalExpressions]; } + /** + * When modifying a nested array dimension with a non-constant key, + * check if the composed value changes any existing constant-array + * key values. If it does, the existing item type should be unioned + * because unmodified elements still have their original types. + */ + private function shouldUnionExistingItemType(Type $offsetValueType, Type $composedValue): bool + { + $existingItemType = $offsetValueType->getIterableValueType(); + + if (!$existingItemType->isConstantArray()->yes() || !$composedValue->isConstantArray()->yes()) { + return false; + } + + foreach ($existingItemType->getConstantArrays() as $existingArray) { + foreach ($existingArray->getKeyTypes() as $i => $keyType) { + $existingValue = $existingArray->getValueTypes()[$i]; + if ($composedValue->hasOffsetValueType($keyType)->no()) { + continue; + } + $newValue = $composedValue->getOffsetValueType($keyType); + if (!$newValue->isSuperTypeOf($existingValue)->yes()) { + return true; + } + } + } + + return false; + } + } diff --git a/tests/PHPStan/Analyser/nsrt/bug-8270.php b/tests/PHPStan/Analyser/nsrt/bug-8270.php new file mode 100644 index 0000000000..fef323a8a8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8270.php @@ -0,0 +1,47 @@ + $list */ + $list = []; + $list[0]['test'] = true; + + foreach ($list as $item) { + assertType('array{test: bool, value: int}', $item); + if ($item['test']) { + assertType('true', $item['test']); + echo $item['value']; + } + } +}; + +function () { + $list = []; + + for ($i = 0; $i < 10; $i++) { + $list[] = [ + 'test' => false, + 'value' => rand(), + ]; + } + + if ($list === []) { + return; + } + + $k = array_key_first($list); + assertType('int<0, max>', $k); + $list[$k]['test'] = true; + + foreach ($list as $item) { + assertType('array{test: bool, value: int<0, max>}', $item); + if ($item['test']) { + echo $item['value']; + } + } +}; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11679.php b/tests/PHPStan/Rules/Arrays/data/bug-11679.php index 463362516a..42002a389f 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-11679.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-11679.php @@ -31,7 +31,7 @@ public function sayHello(int $index): bool assertType('array', $this->arr); if (!isset($this->arr[$index]['foo'])) { $this->arr[$index]['foo'] = true; - assertType('non-empty-array', $this->arr); + assertType('non-empty-array', $this->arr); } assertType('array', $this->arr); return $this->arr[$index]['foo']; // PHPStan does not realize 'foo' is set diff --git a/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php index 08ee797f08..39abc8a79b 100644 --- a/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php +++ b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php @@ -18,7 +18,7 @@ public function doFoo(array $percentageIntervals, array $changes): void assertType('non-empty-array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); $intervalResults[$key]['itemsCount'] += $itemsCount; - assertType('non-empty-array', $intervalResults); + assertType('non-empty-array', $intervalResults); assertType('array{itemsCount: (array|float|int), interval: mixed}', $intervalResults[$key]); } else { assertType('array', $intervalResults);