From c06bad037907aa827a87a883662e834176578af2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 15 Feb 2026 12:14:56 +0100 Subject: [PATCH 1/2] Add reproducer for 13984 --- ...isonOperatorsConstantConditionRuleTest.php | 13 +++++++ .../Rules/Comparison/data/bug-13984.php | 36 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13984.php diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 4c18f9e01e..818bed70ae 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -15,6 +15,8 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain = true; + private bool $polluteScopeWithAlwaysIterableForeach = true; + protected function getRule(): Rule { return new NumberComparisonOperatorsConstantConditionRule( @@ -23,6 +25,11 @@ protected function getRule(): Rule ); } + protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool + { + return $this->polluteScopeWithAlwaysIterableForeach; + } + public function testBug8277(): void { $this->analyse([__DIR__ . '/data/bug-8277.php'], []); @@ -269,6 +276,12 @@ public function testBug3387(): void $this->analyse([__DIR__ . '/data/bug-3387.php'], []); } + public function testBug13984(): void + { + $this->polluteScopeWithAlwaysIterableForeach = false; + $this->analyse([__DIR__ . '/data/bug-13984.php'], []); + } + public function testBug13874(): void { $this->analyse([__DIR__ . '/data/bug-13874.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13984.php b/tests/PHPStan/Rules/Comparison/data/bug-13984.php new file mode 100644 index 0000000000..4c098c1e69 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13984.php @@ -0,0 +1,36 @@ + $list */ +function acceptList(array $list): bool { + if (count($list) < 1) { + return false; + } + + $compare = ['a', 'b', 'c']; + + foreach($list as $key => $item) { + foreach ($compare as $k => $v) { + if ($item === $v && $v !== 'a') { + unset($list[$key]); + } + } + } + + if (count($list) > 0) { + return true; + } + + return false; +} + +assert(acceptList($list) === true); From 4d67d4e6b79961347389c30ac0cf0ee3c31d31b7 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 15 Feb 2026 13:19:36 +0100 Subject: [PATCH 2/2] Try --- src/Analyser/MutatingScope.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index db31ce7669..8bdf7894e6 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3956,7 +3956,9 @@ public function mergeWith(?self $otherScope): self $expr = $expressionTypeHolder->getExpr(); - return $expr instanceof Variable || $expr instanceof VirtualNode; + return $expr instanceof Variable + || $expr instanceof FuncCall + || $expr instanceof VirtualNode; }; $mergedExpressionTypes = array_filter($mergedExpressionTypes, $filter);