diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a43ba35dda5..75dbfa339be 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3877,7 +3877,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope $this->getNamespace(), $expressionTypes, $nativeTypes, - $this->conditionalExpressions, + $this->intersectConditionalExpressions($finalScope->conditionalExpressions), $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, diff --git a/tests/PHPStan/Analyser/Bug14446Test.php b/tests/PHPStan/Analyser/Bug14446Test.php new file mode 100644 index 00000000000..84a421583e6 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug14446Test.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/bug-14446.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/bug-14446.neon b/tests/PHPStan/Analyser/bug-14446.neon new file mode 100644 index 00000000000..58913854694 --- /dev/null +++ b/tests/PHPStan/Analyser/bug-14446.neon @@ -0,0 +1,8 @@ +parameters: + polluteScopeWithAlwaysIterableForeach: false + +services: + - + class: PHPStan\Rules\ForeachLoop\OverwriteVariablesWithForeachRule + tags: + - phpstan.rules.rule diff --git a/tests/PHPStan/Analyser/data/bug-14446.php b/tests/PHPStan/Analyser/data/bug-14446.php new file mode 100644 index 00000000000..da6f9daa262 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-14446.php @@ -0,0 +1,70 @@ + 0 ? [1] : []; + foreach ($items as $item) { + $current = false; + } + } + + assertType('bool', $initial); + var_dump($initial === true); +} + +/** + * @param mixed $value + */ +function testForeachKeyOverwrite($value): void { + if (is_array($value) && $value !== []) { + $hasOnlyStringKey = true; + foreach (array_keys($value) as $key) { + if (is_int($key)) { + $hasOnlyStringKey = false; + break; + } + } + + assertType('bool', $hasOnlyStringKey); + + if ($hasOnlyStringKey) { + // $key should not be in scope here with polluteScopeWithAlwaysIterableForeach: false + // Second foreach should not report "Foreach overwrites $key with its key variable" + foreach ($value as $key => $element) { + assertType('(int|string)', $key); + } + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index aa632593c79..3c928fefd1c 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -20,6 +20,8 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain = true; + private bool $polluteScopeWithAlwaysIterableForeach = true; + protected function getRule(): Rule { return new StrictComparisonOfDifferentTypesRule( @@ -36,6 +38,11 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool + { + return $this->polluteScopeWithAlwaysIterableForeach; + } + public function testStrictComparison(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; @@ -1184,4 +1191,10 @@ public function testBug13421(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13421.php'], []); } + public function testBug14446(): void + { + $this->polluteScopeWithAlwaysIterableForeach = false; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-14446.php'], []); + } + }