From f125b7385d8ef29cd6c28d8a933209f3ee1f38f5 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:59:04 +0000 Subject: [PATCH] Fix multi-dimensional array shape wrongly inferred with hasOffsetValue - When assigning to a nested array with a non-constant outer key like $matrix[$size - 1][8] = 3, the HasOffsetValueType(8, 3) from the inner dimension was incorrectly propagated to ALL entries of the outer array - Added logic in AssignHandler::produceArrayDimFetchAssignValueToWrite() to strip HasOffsetValueType from the inner value when the outer offset is non-constant and the inner dimension was not a tracked expression (i.e., not from a foreach binding) - New regression test in tests/PHPStan/Analyser/nsrt/bug-10089.php Fixes phpstan/phpstan#10089 --- src/Analyser/ExprHandler/AssignHandler.php | 27 ++++++++++++++++++- tests/PHPStan/Analyser/nsrt/bug-10089.php | 31 ++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-10089.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 10b1f135eb..cc9952d423 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -55,6 +55,7 @@ use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticTypeFactory; @@ -969,6 +970,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $offsetValueTypeStack[] = $offsetValueType; } + $previousIterationUsedExistingBranch = false; foreach (array_reverse($offsetTypes) as $i => [$offsetType]) { /** @var Type $offsetValueType */ $offsetValueType = array_pop($offsetValueTypeStack); @@ -1009,8 +1011,31 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } + $previousIterationUsedExistingBranch = true; } else { - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + $innerValueToWrite = $valueToWrite; + if ( + $i > 0 + && !$previousIterationUsedExistingBranch + && $offsetType !== null + && $offsetType->getConstantScalarValues() === [] + && $innerValueToWrite instanceof IntersectionType // @phpstan-ignore phpstanApi.instanceofType + ) { + $filteredTypes = []; + $hasRemovedOffsetValue = false; + foreach ($innerValueToWrite->getTypes() as $innerType) { + if ($innerType instanceof HasOffsetValueType) { + $hasRemovedOffsetValue = true; + continue; + } + $filteredTypes[] = $innerType; + } + if ($hasRemovedOffsetValue && $filteredTypes !== []) { + $innerValueToWrite = TypeCombinator::intersect(...$filteredTypes); + } + } + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $innerValueToWrite, $i === 0); + $previousIterationUsedExistingBranch = false; } if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-10089.php b/tests/PHPStan/Analyser/nsrt/bug-10089.php new file mode 100644 index 0000000000..f11699a02b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10089.php @@ -0,0 +1,31 @@ +, 0|3>>', $matrix); + + for ($i = 0; $i <= $size; $i++) { + assertType('0|3', $matrix[$i][8]); + } + + return $matrix; + } + +}