diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 3993085894..c210bc9d01 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4851,12 +4851,24 @@ public function processClosureScope( ); } - public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope): self + public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope, Node\Stmt\Foreach_ $stmt): self { + $keyVarExprString = $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) + ? '$' . $stmt->keyVar->name + : null; + $valueVarExprString = $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) + ? '$' . $stmt->valueVar->name + : null; + $expressionTypes = $this->expressionTypes; foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) { if (!isset($expressionTypes[$variableExprString])) { - $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); + if ($variableExprString === $keyVarExprString || $variableExprString === $valueVarExprString) { + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); + continue; + } + + $expressionTypes[$variableExprString] = $variableTypeHolder; continue; } @@ -4869,7 +4881,12 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope $nativeTypes = $this->nativeExpressionTypes; foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) { if (!isset($nativeTypes[$variableExprString])) { - $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); + if ($variableExprString === $keyVarExprString || $variableExprString === $valueVarExprString) { + $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); + continue; + } + + $nativeTypes[$variableExprString] = $variableTypeHolder; continue; } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 31dde8718b..66c78f8c7d 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1119,8 +1119,7 @@ private function processStmtNode( } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { $finalScope = $scope; } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { - $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope); - // get types from finalScope, but don't create new variables + $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope, $stmt); } if (!$isIterableAtLeastOnce->no()) { diff --git a/tests/PHPStan/Analyser/ForeachLoopNoScopePollutionTest.php b/tests/PHPStan/Analyser/ForeachLoopNoScopePollutionTest.php new file mode 100644 index 0000000000..8ddbc5f661 --- /dev/null +++ b/tests/PHPStan/Analyser/ForeachLoopNoScopePollutionTest.php @@ -0,0 +1,36 @@ +> */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-loop-no-scope-pollution.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/foreachLoopNoScopePollution.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/foreach-loop-no-scope-pollution.php b/tests/PHPStan/Analyser/data/foreach-loop-no-scope-pollution.php new file mode 100644 index 0000000000..a948eba6a5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/foreach-loop-no-scope-pollution.php @@ -0,0 +1,115 @@ + 'foo', 'bar' => 19]; + + foreach ($items as $key => $item) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType("17|'bar'", $key); + assertNativeType("17|'bar'", $key); + assertVariableCertainty(TrinaryLogic::createMaybe(), $key); + + assertType("19|'foo'", $item); + assertNativeType("19|'foo'", $item); + assertVariableCertainty(TrinaryLogic::createMaybe(), $item); + + assertType('int<0, 1>', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int<0, 1>', $b); + assertNativeType('int', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('int<0, 1>', $c); + assertNativeType('int', $c); + assertVariableCertainty(TrinaryLogic::createYes(), $c); + } + + /** @param int $b */ + public function loopThatMightIterateAtLeastOnce(int $a, $b): void + { + $items = []; + if (rand(0, 1)) { + $items[17] = 'foo'; + } + if (rand(0, 1)) { + $items['bar'] = 19; + } + + foreach ($items as $key => $item) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType("17|'bar'", $key); + assertNativeType("17|'bar'", $key); + assertVariableCertainty(TrinaryLogic::createMaybe(), $key); + + assertType("19|'foo'", $item); + assertNativeType("19|'foo'", $item); + assertVariableCertainty(TrinaryLogic::createMaybe(), $item); + + assertType('int', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int', $b); + assertNativeType('mixed', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('int<0, 1>', $c); + assertNativeType('int', $c); + assertVariableCertainty(TrinaryLogic::createMaybe(), $c); + } + + /** @param int $b */ + public function loopThatNeverIterates(int $a, $b): void + { + $items = []; + + foreach ($items as $key => $item) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('*ERROR*', $key); + assertNativeType('*ERROR*', $key); + assertVariableCertainty(TrinaryLogic::createNo(), $key); + + assertType('*ERROR*', $item); + assertNativeType('*ERROR*', $item); + assertVariableCertainty(TrinaryLogic::createNo(), $item); + + assertType('int', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int', $b); + assertNativeType('mixed', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('*ERROR*', $c); + assertNativeType('*ERROR*', $c); + assertVariableCertainty(TrinaryLogic::createNo(), $c); + } + +} diff --git a/tests/PHPStan/Analyser/foreachLoopNoScopePollution.neon b/tests/PHPStan/Analyser/foreachLoopNoScopePollution.neon new file mode 100644 index 0000000000..3ee516d3be --- /dev/null +++ b/tests/PHPStan/Analyser/foreachLoopNoScopePollution.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithAlwaysIterableForeach: false diff --git a/tests/PHPStan/Analyser/nsrt/foreach-loop.php b/tests/PHPStan/Analyser/nsrt/foreach-loop.php new file mode 100644 index 0000000000..d3ef9e1004 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/foreach-loop.php @@ -0,0 +1,115 @@ + 'foo', 'bar' => 19]; + + foreach ($items as $key => $item) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType("17|'bar'", $key); + assertNativeType("17|'bar'", $key); + assertVariableCertainty(TrinaryLogic::createYes(), $key); + + assertType("19|'foo'", $item); + assertNativeType("19|'foo'", $item); + assertVariableCertainty(TrinaryLogic::createYes(), $item); + + assertType('int<0, 1>', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int<0, 1>', $b); + assertNativeType('int', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('int<0, 1>', $c); + assertNativeType('int', $c); + assertVariableCertainty(TrinaryLogic::createYes(), $c); + } + + /** @param int $b */ + public function loopThatMightIterateAtLeastOnce(int $a, $b): void + { + $items = []; + if (rand(0, 1)) { + $items[17] = 'foo'; + } + if (rand(0, 1)) { + $items['bar'] = 19; + } + + foreach ($items as $key => $item) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType("17|'bar'", $key); + assertNativeType("17|'bar'", $key); + assertVariableCertainty(TrinaryLogic::createMaybe(), $key); + + assertType("19|'foo'", $item); + assertNativeType("19|'foo'", $item); + assertVariableCertainty(TrinaryLogic::createMaybe(), $item); + + assertType('int', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int', $b); + assertNativeType('mixed', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('int<0, 1>', $c); + assertNativeType('int', $c); + assertVariableCertainty(TrinaryLogic::createMaybe(), $c); + } + + /** @param int $b */ + public function loopThatNeverIterates(int $a, $b): void + { + $items = []; + + foreach ($items as $key => $item) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('*ERROR*', $key); + assertNativeType('*ERROR*', $key); + assertVariableCertainty(TrinaryLogic::createNo(), $key); + + assertType('*ERROR*', $item); + assertNativeType('*ERROR*', $item); + assertVariableCertainty(TrinaryLogic::createNo(), $item); + + assertType('int', $a); + assertNativeType('int', $a); + assertVariableCertainty(TrinaryLogic::createYes(), $a); + + assertType('int', $b); + assertNativeType('mixed', $b); + assertVariableCertainty(TrinaryLogic::createYes(), $b); + + assertType('*ERROR*', $c); + assertNativeType('*ERROR*', $c); + assertVariableCertainty(TrinaryLogic::createNo(), $c); + } + +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 9a2346eb77..db6fc019cd 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -525,10 +525,6 @@ public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array 'Variable $val might not be defined.', 20, ], - [ - 'Variable $test might not be defined.', - 21, - ], [ 'Variable $key might not be defined.', 32,