From 73870d54d127f75d3ac29325186c4ccf272a15fc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:21:35 +0000 Subject: [PATCH] Fix deep nested array dim assignment marking keys as optional - Added recursive handling in ArrayType::setExistingOffsetValueType() for arrays whose item type is itself an array with constant array values (3+ nesting levels) - New regression test in tests/PHPStan/Analyser/nsrt/bug-13637.php - The root cause was that setExistingOffsetValueType() only handled the case where the item type was directly a constant array, falling through to a naive union for deeper nesting which re-introduced intermediate states with optional keys Closes https://github.com/phpstan/phpstan/issues/13637 --- src/Type/ArrayType.php | 18 +++++++++ tests/PHPStan/Analyser/nsrt/bug-13637.php | 45 +++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13637.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 288caefdc6..2976a325b5 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -410,6 +410,24 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T } } + if ( + $this->itemType->isArray()->yes() + && $valueType->isArray()->yes() + && $this->itemType->getIterableValueType()->isConstantArray()->yes() + && $valueType->getIterableValueType()->isConstantArray()->yes() + ) { + $newItemType = $this->itemType->setExistingOffsetValueType( + $valueType->getIterableKeyType(), + $valueType->getIterableValueType(), + ); + if ($newItemType !== $this->itemType) { + return new self( + $this->keyType, + $newItemType, + ); + } + } + return new self( $this->keyType, TypeCombinator::union($this->itemType, $valueType), diff --git a/tests/PHPStan/Analyser/nsrt/bug-13637.php b/tests/PHPStan/Analyser/nsrt/bug-13637.php new file mode 100644 index 0000000000..7c2bdd3cf7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13637.php @@ -0,0 +1,45 @@ +>> +*/ +function doesNotWork() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j +1; + $l = $i * 3; + $final[$i][$j][$k]['abc'] = $i; + $final[$i][$j][$k]['def'] = $i; + $final[$i][$j][$k]['ghi'] = $i; + + assertType("array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}", $final[$i][$j][$k]); + } + + return $final; +} + +/** +* @return array> +*/ +function thisWorks() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j +1; + $l = $i * 3; + $final[$i][$j]['abc'] = $i; + $final[$i][$j]['def'] = $i; + $final[$i][$j]['ghi'] = $i; + + assertType("array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}", $final[$i][$j]); + } + + return $final; +}