diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e910d88ba01..dc6323b7a32 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1442,7 +1442,7 @@ public function processStmtNode( $originalStorage = $storage; $storage = $originalStorage->duplicate(); $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep()); - $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $beforeCondBooleanType = $scope->getType($stmt->cond)->toBoolean(); $condScope = $condResult->getFalseyScope(); if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) { if (!$this->polluteScopeWithLoopInitialAssignments) { @@ -1493,7 +1493,7 @@ public function processStmtNode( $alwaysIterates = false; $neverIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); + $condBooleanType = $bodyScopeMaybeRan->getType($stmt->cond)->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); $neverIterates = $condBooleanType->isFalse()->yes(); } @@ -1591,7 +1591,7 @@ public function processStmtNode( $alwaysIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean(); + $condBooleanType = $bodyScope->getType($stmt->cond)->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); } @@ -1662,7 +1662,7 @@ public function processStmtNode( // only the last condition expression is relevant whether the loop continues // see https://www.php.net/manual/en/control-structures.for.php if ($condExpr === $lastCondExpr) { - $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); + $condTruthiness = $condResultScope->getType($condExpr)->toBoolean(); $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); } diff --git a/tests/PHPStan/Analyser/Bug14522Test.php b/tests/PHPStan/Analyser/Bug14522Test.php new file mode 100644 index 00000000000..7f2e4a61933 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug14522Test.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/bug-14522.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/bug-14522.neon b/tests/PHPStan/Analyser/bug-14522.neon new file mode 100644 index 00000000000..c551b84f1f6 --- /dev/null +++ b/tests/PHPStan/Analyser/bug-14522.neon @@ -0,0 +1,2 @@ +parameters: + treatPhpDocTypesAsCertain: false diff --git a/tests/PHPStan/Analyser/data/bug-14522.php b/tests/PHPStan/Analyser/data/bug-14522.php new file mode 100644 index 00000000000..1514d050458 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-14522.php @@ -0,0 +1,53 @@ + + */ +function getBackoffTime(int $retryCount, int $maxBackoff): int +{ + $retryCount = max(0, $retryCount); + $maxBackoff = max(1, $maxBackoff); + + $total = 0; + for ($i = 0; $i <= $retryCount; ++$i) { + $total += min(2 ** $i, $maxBackoff); + } + assertType('int<1, max>', $total); + return $total; +} + +/** @param int<0, max> $n */ +function simpleForLoopAlwaysEnters(int $n): void +{ + $total = 0; + for ($i = 0; $i <= $n; $i++) { + $total++; + } + assertType('int<1, max>', $total); +} + +function forLoopWithMaxAlwaysEnters(int $n): void +{ + $n = max(0, $n); + $total = 0; + for ($i = 0; $i <= $n; $i++) { + $total++; + } + assertType('int<1, max>', $total); +} + +function whileLoopAlwaysEnters(int $n): void +{ + $n = max(0, $n); + $i = 0; + $total = 0; + while ($i <= $n) { + $total++; + $i++; + } + assertType('int<1, max>', $total); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14522.php b/tests/PHPStan/Analyser/nsrt/bug-14522.php new file mode 100644 index 00000000000..ccd55a84473 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14522.php @@ -0,0 +1,73 @@ + + */ +function getBackoffTime(int $retryCount, int $maxBackoff): int +{ + $retryCount = max(0, $retryCount); + $maxBackoff = max(1, $maxBackoff); + + $total = 0; + for ($i = 0; $i <= $retryCount; ++$i) { + $total += min(2 ** $i, $maxBackoff); + } + assertType('int<1, max>', $total); + return $total; +} + +function simpleForLoopAlwaysEnters(int $n): void +{ + $n = max(0, $n); + $total = 0; + for ($i = 0; $i <= $n; $i++) { + $total++; + } + assertType('int<1, max>', $total); +} + +function forLoopNeverEnters(): void +{ + $total = 0; + for ($i = 0; $i < 0; $i++) { + $total++; + } + assertType('0', $total); +} + +function forLoopMaybeEnters(int $n): void +{ + $total = 0; + for ($i = 0; $i < $n; $i++) { + $total++; + } + assertType('int<0, max>', $total); +} + +function whileLoopAlwaysEnters(int $n): void +{ + $n = max(0, $n); + $i = 0; + $total = 0; + while ($i <= $n) { + $total++; + $i++; + } + assertType('int<1, max>', $total); +} + +function whileLoopMaybeEnters(int $n): void +{ + $i = 0; + $total = 0; + while ($i < $n) { + $total++; + $i++; + } + assertType('int<0, max>', $total); +}