diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5659a107bc..3993085894 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4900,6 +4900,68 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope ); } + public function processAlwaysIterableForScopeWithoutPollute(self $finalScope, self $initScope): self + { + $expressionTypes = $this->expressionTypes; + $initScopeExpressionTypes = $initScope->expressionTypes; + foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($expressionTypes[$variableExprString])) { + if (isset($initScopeExpressionTypes[$variableExprString])) { + $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); + continue; + } + + $expressionTypes[$variableExprString] = $variableTypeHolder; + continue; + } + + $expressionTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), + $variableTypeHolder->getType(), + $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()), + ); + } + + $nativeTypes = $this->nativeExpressionTypes; + $initScopeNativeExpressionTypes = $initScope->nativeExpressionTypes; + foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) { + if (!isset($nativeTypes[$variableExprString])) { + if (isset($initScopeNativeExpressionTypes[$variableExprString])) { + $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType()); + continue; + } + + $nativeTypes[$variableExprString] = $variableTypeHolder; + continue; + } + + $nativeTypes[$variableExprString] = new ExpressionTypeHolder( + $variableTypeHolder->getExpr(), + $variableTypeHolder->getType(), + $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()), + ); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + [], + [], + [], + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + public function generalizeWith(self $otherScope): self { $variableTypeHolders = $this->generalizeVariableTypeHolders( diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 1a6a069d28..31dde8718b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1410,7 +1410,7 @@ private function processStmtNode( } } else { if (!$this->polluteScopeWithLoopInitialAssignments) { - $finalScope = $finalScope->mergeWith($scope); + $finalScope = $scope->processAlwaysIterableForScopeWithoutPollute($finalScope, $initScope); } } diff --git a/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php b/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php new file mode 100644 index 0000000000..9b07f9ab2b --- /dev/null +++ b/tests/PHPStan/Analyser/ForLoopNoScopePollutionTest.php @@ -0,0 +1,36 @@ +> */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/for-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__ . '/forLoopNoScopePollution.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php b/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php new file mode 100644 index 0000000000..e7c0bef9ff --- /dev/null +++ b/tests/PHPStan/Analyser/data/for-loop-no-scope-pollution.php @@ -0,0 +1,107 @@ +', $i); + assertNativeType('int<10, max>', $i); + assertVariableCertainty(TrinaryLogic::createMaybe(), $i); + + assertType('int', $j); + assertNativeType('int', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + 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 + { + $j = 0; + for ($i = 0, $j = 10; $i < rand(0, 1); $i++, $j--) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('int<0, max>', $i); + assertNativeType('int<0, max>', $i); + assertVariableCertainty(TrinaryLogic::createMaybe(), $i); + + assertType('int', $j); + assertNativeType('int', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + 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 + { + $j = 0; + for ($i = 0, $j = 10; $i > 10; $i++, $j--) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('*ERROR*', $i); + assertNativeType('*ERROR*', $i); + assertVariableCertainty(TrinaryLogic::createNo(), $i); + + assertType('0', $j); + assertNativeType('0', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + 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/forLoopNoScopePollution.neon b/tests/PHPStan/Analyser/forLoopNoScopePollution.neon new file mode 100644 index 0000000000..47811f500e --- /dev/null +++ b/tests/PHPStan/Analyser/forLoopNoScopePollution.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithLoopInitialAssignments: false diff --git a/tests/PHPStan/Analyser/nsrt/for-loop.php b/tests/PHPStan/Analyser/nsrt/for-loop.php new file mode 100644 index 0000000000..f15cdeb811 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/for-loop.php @@ -0,0 +1,107 @@ +', $i); + assertNativeType('int<10, max>', $i); + assertVariableCertainty(TrinaryLogic::createYes(), $i); + + assertType('int', $j); + assertNativeType('int', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + 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 + { + $j = 0; + for ($i = 0, $j = 10; $i < rand(0, 1); $i++, $j--) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('int<0, max>', $i); + assertNativeType('int<0, max>', $i); + assertVariableCertainty(TrinaryLogic::createYes(), $i); + + assertType('int', $j); + assertNativeType('int', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + 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 + { + $j = 0; + for ($i = 0, $j = 10; $i > 10; $i++, $j--) { + $a = rand(0, 1); + $b = rand(0, 1); + $c = rand(0, 1); + } + + assertType('0', $i); + assertNativeType('0', $i); + assertVariableCertainty(TrinaryLogic::createYes(), $i); + + assertType('10', $j); + assertNativeType('10', $j); + assertVariableCertainty(TrinaryLogic::createYes(), $j); + + 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 94f0b0ffeb..9a2346eb77 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1068,4 +1068,13 @@ public function testBug10228(): void $this->analyse([__DIR__ . '/data/bug-10228.php'], []); } + public function testBug9550(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9550.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-9550.php b/tests/PHPStan/Rules/Variables/data/bug-9550.php new file mode 100644 index 0000000000..ab260ad44c --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9550.php @@ -0,0 +1,32 @@ + 0; --$l) { + $vStr = mb_substr($vStrLonger, 0, $l); + if ($vStr !== $vStrLonger) { + $vStrLonger = $vStr; + $vStr = mb_substr($vStr, 0, $l - 3); + $withThreeDots = true; + } else { + $vStrLonger = $vStr; + } + $vStr = str_replace(["\0", "\t", "\n", "\r"], ['\0', '\t', '\n', '\r'], $vStr); + if (mb_strlen($vStr) <= $maxUtf8Length - ($withThreeDots ? 3 : 0)) { + break; + } + } + + return '\'' . $vStr . '\'' . ($withThreeDots ? '...' : ''); + } +}