From ce6e5e215da163f966d10ce2fcd501eb62f5772b Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 05:45:07 +0000 Subject: [PATCH 1/3] Fix deeply nested array assignments marking keys as optional in loops When building arrays with 3+ levels of nesting inside loops (e.g. $arr[$i][$j][$k]['key'] = value), keys like 'def' and 'ghi' were incorrectly marked as optional in the inferred type, while 2-level nesting worked correctly. The root cause was in ArrayType::setExistingOffsetValueType's fallback path. For non-constant array item types, it performed a raw union with the stale item type from loop stabilization, re-introducing intermediate type variants where keys were missing. The existing special path handled this correctly when the item type was a constant array (the 2-level case), but not when it was a general array type wrapping constant arrays (the 3+ level case). The fix adds a recursive path for when both the item type and value type are arrays whose inner value types are constant arrays. This delegates to the inner level's setExistingOffsetValueType, where the existing constant-array special path correctly handles per-key updates and optionality. Closes phpstan/phpstan#13637 --- src/Type/ArrayType.php | 18 ++++++++ .../Analyser/AnalyserIntegrationTest.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-13637.php | 43 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13637.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index f6550a5845..13e7fafc52 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/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index cf56a34adc..4a8a3d14a6 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -991,7 +991,7 @@ public function testBug7581(): void public function testBug7903(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7903.php'); - $this->assertCount(24, $errors); + $this->assertCount(23, $errors); } public function testBug7901(): void diff --git a/tests/PHPStan/Analyser/nsrt/bug-13637.php b/tests/PHPStan/Analyser/nsrt/bug-13637.php new file mode 100644 index 0000000000..4b9eed835d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13637.php @@ -0,0 +1,43 @@ +>> + */ +function doesNotWork() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j + 1; + $final[$i][$j][$k]['abc'] = $i; + $final[$i][$j][$k]['def'] = $i; + $final[$i][$j][$k]['ghi'] = $i; + } + + assertType("non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>", $final); + + return $final; +} + +/** + * @return array> + */ +function thisWorks() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j + 1; + $final[$i][$j]['abc'] = $i; + $final[$i][$j]['def'] = $i; + $final[$i][$j]['ghi'] = $i; + } + + assertType("non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>", $final); + + return $final; +} From e39c73bab9c5298afe74fb74e0cdd5efa8ff5cb7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 17 Feb 2026 06:36:58 +0000 Subject: [PATCH 2/3] Fix CI failures [claude-ci-fix] Automated fix attempt 1 for CI failures. --- src/Type/ArrayType.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 13e7fafc52..4f8ff8fbc7 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -411,10 +411,10 @@ 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() + $this->itemType->getArrays() !== [] + && $valueType->getArrays() !== [] + && $this->itemType->getIterableValueType()->getConstantArrays() !== [] + && $valueType->getIterableValueType()->getConstantArrays() !== [] ) { $newItemType = $this->itemType->setExistingOffsetValueType( $valueType->getIterableKeyType(), From 469c803de6d5279c4a3b6042d4b99e8c2d921ec7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 17 Feb 2026 07:07:40 +0000 Subject: [PATCH 3/3] Fix CI failures [claude-ci-fix] Automated fix attempt 2 for CI failures. --- src/Type/ArrayType.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 4f8ff8fbc7..13e7fafc52 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -411,10 +411,10 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T } if ( - $this->itemType->getArrays() !== [] - && $valueType->getArrays() !== [] - && $this->itemType->getIterableValueType()->getConstantArrays() !== [] - && $valueType->getIterableValueType()->getConstantArrays() !== [] + $this->itemType->isArray()->yes() + && $valueType->isArray()->yes() + && $this->itemType->getIterableValueType()->isConstantArray()->yes() + && $valueType->getIterableValueType()->isConstantArray()->yes() ) { $newItemType = $this->itemType->setExistingOffsetValueType( $valueType->getIterableKeyType(),