From cd265d1f6026a063d329bf52b0a8505b2e3f9442 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:44:10 +0000 Subject: [PATCH 1/2] Fix incorrect type narrowing to *NEVER* for array after foreach assignment - Added hasOffsetValueType check in AssignHandler to prevent using setExistingOffsetValueType when offset is known to not exist in the array - When the offset definitely doesn't exist (hasOffsetValueType returns no), fall through to setOffsetValueType which correctly widens the key type - Updated bug-13270b-php8 test expectation to reflect more precise type inference (hasOffsetValue instead of hasOffset) - New regression test in tests/PHPStan/Analyser/nsrt/bug-13786.php --- src/Analyser/ExprHandler/AssignHandler.php | 1 + .../PHPStan/Analyser/nsrt/bug-13270b-php8.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-13786.php | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13786.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 1a1ed14bae..df779e633e 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -991,6 +991,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $offsetType !== null && $arrayDimFetch !== null && $scope->hasExpressionType($arrayDimFetch)->yes() + && !$offsetValueType->hasOffsetValueType($offsetType)->no() ) { $hasOffsetType = null; if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php b/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php index e2d9e4ea0b..ecab6997b8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php @@ -19,7 +19,7 @@ public function parseData(array $data): array if (!array_key_exists('priceWithVat', $data['price'])) { $data['price']['priceWithVat'] = null; } - assertType("non-empty-array&hasOffset('priceWithVat')", $data['price']); + assertType("non-empty-array&hasOffsetValue('priceWithVat', mixed)", $data['price']); if (!array_key_exists('priceWithoutVat', $data['price'])) { $data['price']['priceWithoutVat'] = null; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13786.php b/tests/PHPStan/Analyser/nsrt/bug-13786.php new file mode 100644 index 0000000000..840bdf9dea --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13786.php @@ -0,0 +1,20 @@ + $arr */ + +/** @var non-empty-list<'a'|'b'|'c'> $cols */ + +$total = [ ]; +foreach ($arr as $id => $dummy) { + $total[$id] = [ ]; + foreach ($cols as $col) { + $total[$id][$col] = '0'; + } + assertType("non-empty-array<'a'|'b'|'c'|'d', '0'>", $total[$id]); + $total[$id]['d'] = '0'; + assertType("non-empty-array<'a'|'b'|'c'|'d', '0'>&hasOffsetValue('d', '0')", $total[$id]); +} From 1b7551cc98e4907bd828321ebc04fadf6074893e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 21 Mar 2026 09:23:18 +0100 Subject: [PATCH 2/2] more asserts --- tests/PHPStan/Analyser/nsrt/bug-13786.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13786.php b/tests/PHPStan/Analyser/nsrt/bug-13786.php index 840bdf9dea..0c0e56925e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13786.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13786.php @@ -18,3 +18,6 @@ $total[$id]['d'] = '0'; assertType("non-empty-array<'a'|'b'|'c'|'d', '0'>&hasOffsetValue('d', '0')", $total[$id]); } + +$total[$id]['e'] = '1'; +assertType("non-empty-array<'a'|'b'|'c'|'d'|'e', '0'|'1'>&hasOffsetValue('e', '1')", $total[$id]);