From 3372181e7c2930bbaabf44c8983e3f84c49703a1 Mon Sep 17 00:00:00 2001 From: schlndh Date: Fri, 24 Oct 2025 13:45:12 +0200 Subject: [PATCH 1/2] fix null coalesce false positive for multi-dimensional array in loop --- src/Type/ArrayType.php | 24 +++++++++++++++++-- .../Analyser/AnalyserIntegrationTest.php | 2 +- .../Rules/Variables/NullCoalesceRuleTest.php | 5 ++++ .../data/bug-nullCoalesceMultiDimLoop.php | 23 ++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-nullCoalesceMultiDimLoop.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 643a535752..f97ac2a014 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -375,13 +375,33 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { if ($this->itemType->isConstantArray()->yes() && $valueType->isConstantArray()->yes()) { - $newItemType = $this->itemType; + $newItemTypes = []; + foreach ($valueType->getConstantArrays() as $constArray) { - foreach ($constArray->getKeyTypes() as $keyType) { + $newItemType = $this->itemType; + $optionalKeyTypes = []; + foreach ($constArray->getKeyTypes() as $i => $keyType) { $newItemType = $newItemType->setExistingOffsetValueType($keyType, $constArray->getOffsetValueType($keyType)); + + if (!$constArray->isOptionalKey($i)) { + continue; + } + + $optionalKeyTypes[] = $keyType; + } + $newItemTypes[] = $newItemType; + + if ($optionalKeyTypes === []) { + continue; + } + + foreach ($optionalKeyTypes as $keyType) { + $newItemType = $newItemType->unsetOffset($keyType); } + $newItemTypes[] = $newItemType; } + $newItemType = TypeCombinator::union(...$newItemTypes); if ($newItemType !== $this->itemType) { return new self( $this->keyType, diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 8d32d02ba3..34daffc9ab 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -983,7 +983,7 @@ public function testBug7581(): void public function testBug7903(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7903.php'); - $this->assertCount(23, $errors); + $this->assertCount(24, $errors); } public function testBug7901(): void diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 318bd3b275..cb1a58a0de 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -337,6 +337,11 @@ public function testBug12553(): void $this->analyse([__DIR__ . '/data/bug-12553.php'], []); } + public function testBugMultiDimLoop(): void + { + $this->analyse([__DIR__ . '/data/bug-nullCoalesceMultiDimLoop.php'], []); + } + public function testIssetAfterRememberedConstructor(): void { $this->analyse([__DIR__ . '/data/isset-after-remembered-constructor.php'], [ diff --git a/tests/PHPStan/Rules/Variables/data/bug-nullCoalesceMultiDimLoop.php b/tests/PHPStan/Rules/Variables/data/bug-nullCoalesceMultiDimLoop.php new file mode 100644 index 0000000000..952a3ac9da --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-nullCoalesceMultiDimLoop.php @@ -0,0 +1,23 @@ + $rows + */ +function foo(array $rows): mixed +{ + $itemMap = []; + + foreach ($rows as $row) { + $x = $row[1]; + $month = $row[2]; + + $itemMap[$x][$month]['foo'] ??= 5; + $itemMap[$x][$month]['bar'] ??= 5; + + $itemMap[$x][$month]['amount'] ??= 0.0; + } + + return $itemMap; +} From 50cbd453254de493623759d295245fa49d053c01 Mon Sep 17 00:00:00 2001 From: schlndh Date: Fri, 24 Oct 2025 15:02:21 +0200 Subject: [PATCH 2/2] add NSRT test --- ...ug-optionalArrayKeyInMultiDimArrayLoop.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-optionalArrayKeyInMultiDimArrayLoop.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-optionalArrayKeyInMultiDimArrayLoop.php b/tests/PHPStan/Analyser/nsrt/bug-optionalArrayKeyInMultiDimArrayLoop.php new file mode 100644 index 0000000000..ba4858db63 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-optionalArrayKeyInMultiDimArrayLoop.php @@ -0,0 +1,24 @@ + $rows + */ +function foo(array $rows): mixed +{ + $itemMap = []; + + foreach ($rows as $row) { + $x = $row[1]; + $month = $row[2]; + + $itemMap[$x][$month]['foo'] ??= 5; + $itemMap[$x][$month]['bar'] ??= 5; + + \PHPStan\Testing\assertType('array{foo: 5, bar: 5, amount?: 0.0}', $itemMap[$x][$month]); + $itemMap[$x][$month]['amount'] ??= 0.0; + } + + return $itemMap; +}