From dfdaf40fe7136e489559b1106d6421ce8c5d41a2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 11:52:47 +0100 Subject: [PATCH 01/13] Fix incorrect narrowing of nested array after assignment --- src/Analyser/ExprHandler/AssignHandler.php | 14 ++++++++++++-- .../PHPStan/Analyser/nsrt/assign-nested-arrays.php | 3 ++- tests/PHPStan/Rules/Arrays/data/bug-11679.php | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 10b1f135eb4..f76020c2b5f 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -947,10 +947,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $originalValueToWrite = $valueToWrite; $offsetValueTypeStack = [$offsetValueType]; + $overwrites = true; foreach (array_slice($offsetTypes, 0, -1) as [$offsetType, $dimFetch]) { if ($offsetType === null) { $offsetValueType = new ConstantArrayType([], []); - + $overwrites = false; } else { $has = $offsetValueType->hasOffsetValueType($offsetType); if ($has->yes()) { @@ -959,9 +960,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar if (!$scope->hasExpressionType($dimFetch)->yes()) { $offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], [])); } else { + $overwrites = false; $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); } } else { + $overwrites = false; $offsetValueType = new ConstantArrayType([], []); } } @@ -1010,7 +1013,14 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } else { - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + $unionValues = false; + if ($i === 0) { + $unionValues = true; + } elseif ($overwrites === true && $i === count($offsetTypes) - 1) { + $unionValues = true; + } + + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues); } if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { diff --git a/tests/PHPStan/Analyser/nsrt/assign-nested-arrays.php b/tests/PHPStan/Analyser/nsrt/assign-nested-arrays.php index 31cecaa4d1a..3de574894a4 100644 --- a/tests/PHPStan/Analyser/nsrt/assign-nested-arrays.php +++ b/tests/PHPStan/Analyser/nsrt/assign-nested-arrays.php @@ -12,8 +12,9 @@ public function doFoo(int $i) $array = []; $array[$i]['bar'] = 1; - $array[$i]['baz'] = 2; + assertType('non-empty-array', $array); + $array[$i]['baz'] = 2; assertType('non-empty-array', $array); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11679.php b/tests/PHPStan/Rules/Arrays/data/bug-11679.php index 463362516aa..46374b8cb2f 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-11679.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-11679.php @@ -31,7 +31,8 @@ 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('true', $this->arr[$index]['foo']); } assertType('array', $this->arr); return $this->arr[$index]['foo']; // PHPStan does not realize 'foo' is set From 7ffd3de459aae0cc6418ff2aab98bc411cdfa6a4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 11:54:14 +0100 Subject: [PATCH 02/13] Create bug-13857.php --- tests/PHPStan/Analyser/nsrt/bug-13857.php | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13857.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13857.php b/tests/PHPStan/Analyser/nsrt/bug-13857.php new file mode 100644 index 00000000000..737f17763e8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13857.php @@ -0,0 +1,67 @@ + $array + */ +function test(array $array, int $id): void { + $array[$id]['state'] = 'foo'; + // only one element was set to 'foo', not all of them. + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testMaybe(array $array, int $id): void { + $array[$id]['state'] = 'foo'; + // only one element was set to 'foo', not all of them. + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testUnionValue(array $array, int $id): void { + $array[$id]['state'] = 'foo'; + // only one element was set to 'foo', not all of them. + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testUnionArray(array $array, int $id): void { + $array[$id]['state'] = 'foo'; + // only one element was set to 'foo', not all of them. + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testUnionArrayDifferentType(array $array, int $id): void { + $array[$id]['state'] = true; + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testConstantArray(array $array, int $id): void { + $array[$id]['state'] = 'bar'; + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testConstantArrayNonScalarAssign(array $array, int $id, bool $b): void { + $array[$id]['state'] = $b; + assertType("non-empty-array", $array); +} From 9311e604a30cfda91a17cb6bdd269c3e648595bf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 12:13:18 +0100 Subject: [PATCH 03/13] Update AssignHandler.php --- src/Analyser/ExprHandler/AssignHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index f76020c2b5f..977239cb1da 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -947,7 +947,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $originalValueToWrite = $valueToWrite; $offsetValueTypeStack = [$offsetValueType]; - $overwrites = true; + $overwrites = $offsetTypes[array_key_last($offsetTypes)][0] !== null; foreach (array_slice($offsetTypes, 0, -1) as [$offsetType, $dimFetch]) { if ($offsetType === null) { $offsetValueType = new ConstantArrayType([], []); From c5ec96f432ba9c422f1610b1b267bda9f043b6b2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 12:17:51 +0100 Subject: [PATCH 04/13] Update AssignHandler.php --- src/Analyser/ExprHandler/AssignHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 977239cb1da..b5ac6667144 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -938,7 +938,7 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr /** * @param list $dimFetchStack - * @param list $offsetTypes + * @param non-empty-list $offsetTypes * * @return array{Type, list} */ From 44a4a9495f041463d2ba255c3c391adaa2f9d7cc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 12:22:28 +0100 Subject: [PATCH 05/13] Update pr-4390.php --- tests/PHPStan/Analyser/nsrt/pr-4390.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/pr-4390.php b/tests/PHPStan/Analyser/nsrt/pr-4390.php index c318b9b6ee8..7a666f19c24 100644 --- a/tests/PHPStan/Analyser/nsrt/pr-4390.php +++ b/tests/PHPStan/Analyser/nsrt/pr-4390.php @@ -13,6 +13,6 @@ function (string $s): void { } } - assertType('non-empty-array, non-empty-array, string>>', $locations); - assertType('non-empty-array, string>', $locations[0]); + assertType('non-empty-array, array, string>>', $locations); // could be 'non-empty-array, non-empty-array, string>>' + assertType('array, string>', $locations[0]); // could be 'non-empty-array, string>' }; From fd70215da5dbfabc1d55da8eea69c58445eecb9d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 14:50:49 +0100 Subject: [PATCH 06/13] Revert "Update pr-4390.php" This reverts commit ab2ace493df7202cb58ce4f341e5761db7d3d946. --- tests/PHPStan/Analyser/nsrt/pr-4390.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/pr-4390.php b/tests/PHPStan/Analyser/nsrt/pr-4390.php index 7a666f19c24..c318b9b6ee8 100644 --- a/tests/PHPStan/Analyser/nsrt/pr-4390.php +++ b/tests/PHPStan/Analyser/nsrt/pr-4390.php @@ -13,6 +13,6 @@ function (string $s): void { } } - assertType('non-empty-array, array, string>>', $locations); // could be 'non-empty-array, non-empty-array, string>>' - assertType('array, string>', $locations[0]); // could be 'non-empty-array, string>' + assertType('non-empty-array, non-empty-array, string>>', $locations); + assertType('non-empty-array, string>', $locations[0]); }; From 9c7a3e43167cf46ebd57fa21c6449f8aa7b48f0a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 16:37:32 +0100 Subject: [PATCH 07/13] fix --- src/Analyser/ExprHandler/AssignHandler.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index b5ac6667144..9cc5f98c0ce 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1016,7 +1016,15 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $unionValues = false; if ($i === 0) { $unionValues = true; - } elseif ($overwrites === true && $i === count($offsetTypes) - 1) { + } elseif ( + $overwrites === true + && $i === count($offsetTypes) - 1 + && + ( + $originalValueToWrite->isConstantScalarValue()->yes() + || !$offsetValueType->getIterableValueType()->isSuperTypeOf($valueToWrite)->yes() + ) + ) { $unionValues = true; } From 674fdb8578e00dfc50a8e87107dc67e81f1d8551 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 16:59:41 +0100 Subject: [PATCH 08/13] add regression test --- tests/PHPStan/Analyser/nsrt/bug-10089.php | 44 +++++++++++++++++++ ...rictComparisonOfDifferentTypesRuleTest.php | 5 +++ 2 files changed, 49 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-10089.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-10089.php b/tests/PHPStan/Analyser/nsrt/bug-10089.php new file mode 100644 index 00000000000..21122cdfc33 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10089.php @@ -0,0 +1,44 @@ +, non-empty-array> + assertType('list>', $matrix); + + $matrix[$size - 1][8] = 3; + + // non-empty-array&hasOffsetValue(8, 3)> + assertType('non-empty-list, 0|3>>', $matrix); + + for ($i = 0; $i <= $size; $i++) { + if ($matrix[$i][8] === 0) { + // ... + } + if ($matrix[8][$i] === 0) { + // ... + } + if ($matrix[$size - 1 - $i][8] === 0) { + // ... + } + } + + return $matrix; + } + +} + + diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 2450cb19b37..d5e7f59fd81 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1046,6 +1046,11 @@ public function testBug13282(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13282.php'], []); } + public function testBug10089(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10089.php'], []); + } + public function testBug11609(): void { $this->analyse([__DIR__ . '/data/bug-11609.php'], [ From 6a0b72a47a0464d6a21dc792e6d1d8867822f399 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 17:14:38 +0100 Subject: [PATCH 09/13] Update AssignHandler.php --- src/Analyser/ExprHandler/AssignHandler.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 9cc5f98c0ce..24e2f6e29f0 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -947,11 +947,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $originalValueToWrite = $valueToWrite; $offsetValueTypeStack = [$offsetValueType]; - $overwrites = $offsetTypes[array_key_last($offsetTypes)][0] !== null; + $overwritesExistingOffset = $offsetTypes[array_key_last($offsetTypes)][0] !== null; foreach (array_slice($offsetTypes, 0, -1) as [$offsetType, $dimFetch]) { if ($offsetType === null) { $offsetValueType = new ConstantArrayType([], []); - $overwrites = false; + $overwritesExistingOffset = false; } else { $has = $offsetValueType->hasOffsetValueType($offsetType); if ($has->yes()) { @@ -960,11 +960,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar if (!$scope->hasExpressionType($dimFetch)->yes()) { $offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], [])); } else { - $overwrites = false; + $overwritesExistingOffset = false; $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); } } else { - $overwrites = false; + $overwritesExistingOffset = false; $offsetValueType = new ConstantArrayType([], []); } } @@ -1017,7 +1017,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar if ($i === 0) { $unionValues = true; } elseif ( - $overwrites === true + $overwritesExistingOffset === true && $i === count($offsetTypes) - 1 && ( From 1e002a79b7c99f8597a16e383e11d9a58c66eeda Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 17:55:01 +0100 Subject: [PATCH 10/13] Update AssignHandler.php --- src/Analyser/ExprHandler/AssignHandler.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 24e2f6e29f0..2f151eabb1b 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -947,11 +947,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $originalValueToWrite = $valueToWrite; $offsetValueTypeStack = [$offsetValueType]; - $overwritesExistingOffset = $offsetTypes[array_key_last($offsetTypes)][0] !== null; + $generalizeOnWrite = $offsetTypes[array_key_last($offsetTypes)][0] !== null; foreach (array_slice($offsetTypes, 0, -1) as [$offsetType, $dimFetch]) { if ($offsetType === null) { $offsetValueType = new ConstantArrayType([], []); - $overwritesExistingOffset = false; + $generalizeOnWrite = false; } else { $has = $offsetValueType->hasOffsetValueType($offsetType); if ($has->yes()) { @@ -960,11 +960,11 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar if (!$scope->hasExpressionType($dimFetch)->yes()) { $offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], [])); } else { - $overwritesExistingOffset = false; + $generalizeOnWrite = false; $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); } } else { - $overwritesExistingOffset = false; + $generalizeOnWrite = false; $offsetValueType = new ConstantArrayType([], []); } } @@ -1017,7 +1017,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar if ($i === 0) { $unionValues = true; } elseif ( - $overwritesExistingOffset === true + $generalizeOnWrite && $i === count($offsetTypes) - 1 && ( From 8aa457616cb80babebe33dab8eab716f384158ed Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 18:01:14 +0100 Subject: [PATCH 11/13] Update AssignHandler.php --- src/Analyser/ExprHandler/AssignHandler.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 2f151eabb1b..fdd8e462b3b 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1013,6 +1013,8 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } else { + // when $unionValues=false the array-item type will be replaced with $valueToWrite + // when $unionValues=true the existing array item-type will be union with $valueToWrite $unionValues = false; if ($i === 0) { $unionValues = true; From 1af4f7847707ae7e52d5a199501e1558a58b2ded Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 18:02:33 +0100 Subject: [PATCH 12/13] Update AssignHandler.php --- src/Analyser/ExprHandler/AssignHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index fdd8e462b3b..9d68c3a82dc 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1013,7 +1013,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } else { - // when $unionValues=false the array-item type will be replaced with $valueToWrite + // when $unionValues=false the array item-type will be replaced with $valueToWrite // when $unionValues=true the existing array item-type will be union with $valueToWrite $unionValues = false; if ($i === 0) { From d1ff83b8531bfdbf7106070430fd3dbbc266e88a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Mar 2026 18:03:22 +0100 Subject: [PATCH 13/13] Update AssignHandler.php --- src/Analyser/ExprHandler/AssignHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 9d68c3a82dc..1baaa08905a 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1014,7 +1014,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } else { // when $unionValues=false the array item-type will be replaced with $valueToWrite - // when $unionValues=true the existing array item-type will be union with $valueToWrite + // when $unionValues=true the existing array item-type will be union'ed with $valueToWrite -> type gets wider $unionValues = false; if ($i === 0) { $unionValues = true;