diff --git a/src/Analyser/InternalStatementResult.php b/src/Analyser/InternalStatementResult.php index 1358380115..d4d03c2405 100644 --- a/src/Analyser/InternalStatementResult.php +++ b/src/Analyser/InternalStatementResult.php @@ -9,6 +9,8 @@ final class InternalStatementResult { + private bool $endReachable; + /** * @param InternalStatementExitPoint[] $exitPoints * @param InternalThrowPoint[] $throwPoints @@ -24,8 +26,10 @@ public function __construct( private array $impurePoints, private array $endStatements = [], private ?VariableFlow $variableFlow = null, + ?bool $endReachable = null, ) { + $this->endReachable = $endReachable ?? !$isAlwaysTerminating; foreach ($exitPoints as $exitPoint) { $this->scope = $this->scope->addTemplateArgumentConstraints($exitPoint->getScope()->getTemplateArgumentConstraints()); } @@ -39,6 +43,30 @@ public function getVariableFlow(): ?VariableFlow return $this->variableFlow; } + /** + * Whether execution can reach the end of the statements. Unlike isAlwaysTerminating(), + * filterOutLoopExitPoints() does not reset it: a loop body left only through + * break or continue still cannot reach its end. + */ + public function isEndReachable(): bool + { + return $this->endReachable; + } + + /** + * The scope the next iteration of a loop starts from: the end of the body when + * it is reachable, merged with the body's continue statements. Null when the body + * never reaches the next iteration. + */ + public function getLoopBackEdgeScope(): ?MutatingScope + { + $backEdge = $this->endReachable ? $this->scope : null; + foreach ($this->getExitPointsByType(Stmt\Continue_::class) as $continueExitPoint) { + $backEdge = $backEdge === null ? $continueExitPoint->getScope() : $backEdge->mergeWith($continueExitPoint->getScope()); + } + return $backEdge; + } + public function toPublic(): StatementResult { return new StatementResult( @@ -81,14 +109,14 @@ public function filterOutLoopExitPoints(): self $num = $statement->num; if (!$num instanceof Int_) { - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow, endReachable: false); } if ($num->value !== 1) { continue; } - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow, endReachable: false); } return $this; diff --git a/src/Analyser/StmtHandler/DoWhileHandler.php b/src/Analyser/StmtHandler/DoWhileHandler.php index b70ec50cc6..4124c17bcb 100644 --- a/src/Analyser/StmtHandler/DoWhileHandler.php +++ b/src/Analyser/StmtHandler/DoWhileHandler.php @@ -4,7 +4,6 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Break_; -use PhpParser\Node\Stmt\Continue_; use PhpParser\Node\Stmt\Do_; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; @@ -72,12 +71,8 @@ public function processStmt( $scope->pushExpressionResultStorage($storage); try { $bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints(); - $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } - $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); + $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); + $finalScope = $backEdgeScope === null ? $finalScope : $backEdgeScope->mergeWith($finalScope); foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } @@ -88,10 +83,16 @@ public function processStmt( $replayPassStorage = $storage; $replayPassResult = $bodyScopeResult; } - $bodyScope = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false))->getTruthyScope(); + if ($backEdgeScope !== null) { + $bodyScope = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $backEdgeScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false))->getTruthyScope(); + } } finally { $scope->popExpressionResultStorage(); } + if ($backEdgeScope === null) { + $bodyScope = $prevScope; + break; + } if ($bodyScope->equals($prevScope)) { break; } @@ -120,10 +121,9 @@ public function processStmt( } else { $bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); } - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } + $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); + $backEdgeDead = $backEdgeScope === null; + $bodyScope = $backEdgeScope ?? $bodyScopeResult->getScope(); // the condition is processed once on the post-body scope; its result // answers both the always-iterates check below and the falsey post-loop @@ -138,16 +138,16 @@ public function processStmt( $alwaysIterates = $condBooleanType->isTrue()->yes(); } - if ($alwaysIterates) { + if ($alwaysIterates || $backEdgeDead) { $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0; } else { $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); } - $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); + $finalScope = $alwaysTerminating || $backEdgeDead ? $finalScope : $bodyScope->mergeWith($finalScope); if ($finalScope === null) { $finalScope = $scope; } - if (!$alwaysTerminating) { + if (!$alwaysTerminating && !$backEdgeDead) { $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); @@ -162,7 +162,7 @@ public function processStmt( $breakExitPoints = $bodyScopeResult->getExitPointsByType(Break_::class); if (count($breakExitPoints) > 0) { - $breakScope = $alwaysIterates ? null : $finalScope; + $breakScope = $alwaysIterates || $backEdgeDead ? null : $finalScope; foreach ($breakExitPoints as $breakExitPoint) { $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope()); } diff --git a/src/Analyser/StmtHandler/ForHandler.php b/src/Analyser/StmtHandler/ForHandler.php index 7c55a9c0a9..45bdb70b06 100644 --- a/src/Analyser/StmtHandler/ForHandler.php +++ b/src/Analyser/StmtHandler/ForHandler.php @@ -12,7 +12,6 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Break_; -use PhpParser\Node\Stmt\Continue_; use PhpParser\Node\Stmt\For_; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; @@ -200,10 +199,12 @@ public function processStmt( $bodyScope = $nodeScopeResolver->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false))->getTruthyScope(); } $bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); + if ($backEdgeScope === null) { + $bodyScope = $prevScope; + break; } + $bodyScope = $backEdgeScope; foreach ($stmt->loop as $loopExpr) { $exprResult = $nodeScopeResolver->processExprNode($stmt, $loopExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createTopLevel(resolveTemplateArguments: false)); @@ -244,10 +245,9 @@ public function processStmt( } $finalScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); - $finalScope = $finalScopeResult->getScope(); - foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); - } + $backEdgeScope = $finalScopeResult->getLoopBackEdgeScope(); + $backEdgeDead = $backEdgeScope === null; + $finalScope = $backEdgeScope ?? $finalScopeResult->getScope(); $loopScope = $finalScope; foreach ($stmt->loop as $loopExpr) { @@ -271,14 +271,14 @@ public function processStmt( $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); if (count($breakExitPoints) > 0) { - $breakScope = $alwaysIterates->yes() ? null : $finalScope; + $breakScope = $alwaysIterates->yes() || $backEdgeDead ? null : $finalScope; foreach ($breakExitPoints as $breakExitPoint) { $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope()); } $finalScope = $breakScope; } - if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { + if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating() || ($backEdgeDead && count($breakExitPoints) === 0)) { if ($nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { $finalScope = $initScope; } else { diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index 44b7efd059..6271e4cd4e 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -232,10 +232,12 @@ static function () use ($condResult, $emptyArrayType): Type { try { $bodyScope = $this->enterForeach($nodeScopeResolver, $bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); + if ($backEdgeScope === null) { + $bodyScope = $prevScope; + break; } + $bodyScope = $backEdgeScope; } finally { $scope->popExpressionResultStorage(); } @@ -278,7 +280,7 @@ static function () use ($condResult, $emptyArrayType): Type { $finalPassContext = $unrolledTotalKeys !== null ? $context->enterUnrolledForeach($unrolledTotalKeys) : $context; $finalScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $finalPassContext)->filterOutLoopExitPoints(); } - $finalScope = $finalScopeResult->getScope(); + $finalScope = $finalScopeResult->isEndReachable() ? $finalScopeResult->getScope() : null; $scopesWithIterableValueType = []; $keyVarExpr = null; @@ -299,7 +301,7 @@ static function () use ($condResult, $emptyArrayType): Type { $trackingExpr = $originalKeyVarExpr ?? $originalValueExpr; $continueExitPointHasUnoriginalKeyType = false; - if ($trackingExpr !== null) { + if ($trackingExpr !== null && $finalScope !== null) { if ($finalScope->hasExpressionType($trackingExpr)->yes()) { $scopesWithIterableValueType[] = $finalScope; } else { @@ -309,7 +311,7 @@ static function () use ($condResult, $emptyArrayType): Type { foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $continueScope = $continueExitPoint->getScope(); - $finalScope = $continueScope->mergeWith($finalScope); + $finalScope = $finalScope === null ? $continueScope : $continueScope->mergeWith($finalScope); if ($trackingExpr === null || !$continueScope->hasExpressionType($trackingExpr)->yes()) { $continueExitPointHasUnoriginalKeyType = true; continue; @@ -318,8 +320,9 @@ static function () use ($condResult, $emptyArrayType): Type { } $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); foreach ($breakExitPoints as $breakExitPoint) { - $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + $finalScope = $finalScope === null ? $breakExitPoint->getScope() : $breakExitPoint->getScope()->mergeWith($finalScope); } + $finalScope ??= $finalScopeResult->getScope(); if ($unrolledEndScope !== null) { $finalScope = $unrolledEndScope; @@ -628,7 +631,7 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop } /** - * @return array{bodyScope: MutatingScope, endScope: MutatingScope, totalKeys: int}|null + * @return array{bodyScope: MutatingScope, endScope: MutatingScope|null, totalKeys: int}|null */ private function tryProcessUnrolledConstantArrayForeach( NodeScopeResolver $nodeScopeResolver, @@ -755,14 +758,22 @@ private function tryProcessUnrolledConstantArrayForeach( $bodyContext, )->filterOutLoopExitPoints(); - $iterEndScope = $bodyResult->getScope(); - foreach ($bodyResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $iterEndScope = $iterEndScope->mergeWith($continueExitPoint->getScope()); - } + $iterEndScope = $bodyResult->getLoopBackEdgeScope(); foreach ($bodyResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $allBreakScopes[] = $breakExitPoint->getScope(); } + if ($iterEndScope === null) { + if ($isOptional) { + // the key may be missing, the next iteration then starts from the previous one + continue; + } + + // no later iteration runs, the loop is left only through its break statements + $chainScope = null; + break; + } + if ($isOptional) { $chainScope = $iterEndScope->mergeWith($chainScope); } else { @@ -774,11 +785,15 @@ private function tryProcessUnrolledConstantArrayForeach( for ($i = 1, $c = count($entryScopes); $i < $c; $i++) { $arrayBodyScope = $arrayBodyScope->mergeWith($entryScopes[$i]); } - if (count($entryScopes) === 1) { + if (count($entryScopes) === 1 && $chainScope !== null) { $arrayBodyScope = $arrayBodyScope->mergeWith($chainScope); } $allBodyScopes[] = $arrayBodyScope; + if ($chainScope === null) { + continue; + } + $allChainScopes[] = $chainScope; } @@ -791,13 +806,14 @@ private function tryProcessUnrolledConstantArrayForeach( $bodyScope = $bodyScope->mergeWith($allBodyScopes[$i]); } - $endScope = $allChainScopes[0]; - for ($i = 1, $c = count($allChainScopes); $i < $c; $i++) { - $endScope = $endScope->mergeWith($allChainScopes[$i]); + $chainEndScope = null; + foreach ($allChainScopes as $chainScope) { + $chainEndScope = $chainEndScope === null ? $chainScope : $chainEndScope->mergeWith($chainScope); } + $endScope = $chainEndScope; foreach ($allBreakScopes as $breakScope) { - $endScope = $endScope->mergeWith($breakScope); + $endScope = $endScope === null ? $breakScope : $endScope->mergeWith($breakScope); } // Unsealed shapes describe zero-or-more additional entries beyond the @@ -805,7 +821,7 @@ private function tryProcessUnrolledConstantArrayForeach( // unrolled explicit iterations so body-scope variables (e.g. counters) // account for the extra iterations while keeping the lower bound // established by the non-optional explicit keys. - if ($hasUnsealed) { + if ($hasUnsealed && $chainEndScope !== null && $endScope !== null) { $loopScope = $endScope; $count = 0; do { @@ -814,13 +830,15 @@ private function tryProcessUnrolledConstantArrayForeach( $iterBodyScope = $loopScope->mergeWith($endScope); $iterBodyScope = $this->enterForeach($nodeScopeResolver, $iterBodyScope, $iterStorage, $originalScope, $stmt, $iterateeType, $nativeIterateeType, new NoopNodeCallback()); $iterBodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $iterBodyScope, $iterStorage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints(); - $loopScope = $iterBodyScopeResult->getScope(); - foreach ($iterBodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $loopScope = $loopScope->mergeWith($continueExitPoint->getScope()); - } + $backEdgeScope = $iterBodyScopeResult->getLoopBackEdgeScope(); foreach ($iterBodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $endScope = $endScope->mergeWith($breakExitPoint->getScope()); } + if ($backEdgeScope === null) { + $loopScope = $prevLoopScope; + break; + } + $loopScope = $backEdgeScope; $bodyScope = $bodyScope->mergeWith($loopScope); if ($loopScope->equals($prevLoopScope)) { break; diff --git a/src/Analyser/StmtHandler/WhileHandler.php b/src/Analyser/StmtHandler/WhileHandler.php index 0dd656935e..469b2c833f 100644 --- a/src/Analyser/StmtHandler/WhileHandler.php +++ b/src/Analyser/StmtHandler/WhileHandler.php @@ -4,7 +4,6 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Break_; -use PhpParser\Node\Stmt\Continue_; use PhpParser\Node\Stmt\While_; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; @@ -100,13 +99,15 @@ public function processStmt( $passCondResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $condRecording, ExpressionContext::createDeep(resolveTemplateArguments: false)); $bodyScope = $passCondResult->getTruthyScope(); $bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints(); - $bodyScope = $bodyScopeResult->getScope(); - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } + $backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope(); } finally { $scope->popExpressionResultStorage(); } + if ($backEdgeScope === null) { + $bodyScope = $prevScope; + break; + } + $bodyScope = $backEdgeScope; // the candidate to replace the final walk when this pass's // entry turns out to be the fixpoint if ($condRecording instanceof RecordingNodeCallback && $bodyRecording instanceof RecordingNodeCallback) { @@ -155,7 +156,9 @@ public function processStmt( $bodyScope = $bodyCondResult->getTruthyScope(); $finalScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); } - $finalScope = $finalScopeResult->getScope(); + $backEdgeScope = $finalScopeResult->getLoopBackEdgeScope(); + $backEdgeDead = $backEdgeScope === null; + $finalScope = $backEdgeScope ?? $finalScopeResult->getScope(); // the loop condition narrows the post-loop scope to its falsey branch; // $finalScope (after the body ran) is a different scope than the condition's // own, so reprocess the condition there rather than re-running its result. @@ -170,15 +173,9 @@ public function processStmt( $alwaysIterates = $condBooleanType->isTrue()->yes(); $neverIterates = $condBooleanType->isFalse()->yes(); } - if (!$alwaysIterates) { - foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); - } - } - $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); if (count($breakExitPoints) > 0) { - $breakScope = $alwaysIterates ? null : $finalScope; + $breakScope = $alwaysIterates || $backEdgeDead ? null : $finalScope; foreach ($breakExitPoints as $breakExitPoint) { $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope()); } @@ -195,12 +192,16 @@ public function processStmt( } else { $isAlwaysTerminating = false; } + if ($backEdgeDead && count($breakExitPoints) === 0) { + $finalScope = null; + } if (!$isIterableAtLeastOnce) { if (!$nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { $condScope = $condScope->mergeWith($scope); } - $finalScope = $finalScope->mergeWith($condScope); + $finalScope = $finalScope === null ? $condScope : $finalScope->mergeWith($condScope); } + $finalScope ??= $finalScopeResult->getScope(); $throwPoints = $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-13959.php b/tests/PHPStan/Analyser/nsrt/bug-13959.php new file mode 100644 index 0000000000..7ca0324947 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13959.php @@ -0,0 +1,34 @@ + $value + */ + public function sayHello(array $value): void + { + assertType('list', $value); + + foreach ($value as $item) { + if ($item instanceof GlobalTagId) { + continue; + } + + if (is_string($item)) { + continue; + } + + throw new InvalidArgumentException('Invalid type'); + } + + assertType('list', $value); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14418.php b/tests/PHPStan/Analyser/nsrt/bug-14418.php new file mode 100644 index 0000000000..f6455ae011 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14418.php @@ -0,0 +1,254 @@ +attempt(); + } catch (\Exception $e) { + continue; + } + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + throw $e; +} + +function forContinue(): int +{ + for ($try = 0; $try <= 3; $try++) { + if (rand(0, 1)) { + $e = 1; + continue; + } + return 2; + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function whileContinue(): int +{ + $i = 0; + while ($i < 3) { + $i++; + if (rand(0, 1)) { + $e = 1; + continue; + } + return 2; + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function doWhileContinue(): int +{ + do { + if (rand(0, 1)) { + $e = 1; + continue; + } + return 2; + } while (rand(0, 1)); + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +/** + * @param list $xs + */ +function foreachContinueMayNotIterate(array $xs): int +{ + foreach ($xs as $x) { + if (rand(0, 1)) { + $e = $x; + continue; + } + return 2; + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $e); + return 1; +} + +function unrolledForeachContinue(): int +{ + foreach ([1, 2] as $x) { + if (rand(0, 1)) { + $e = $x; + continue; + } + return 2; + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function unrolledForeachContinueOuterLoop(): int +{ + foreach ([1, 2, 3] as $a) { + while (rand(0, 1)) { + if (rand(0, 1)) { + $e = $a; + continue 2; + } + return 1; + } + return 2; + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function forBreak(): int +{ + for ($i = 0; $i < 3; $i++) { + if (rand(0, 1)) { + $e = 1; + break; + } + return 2; + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function whileBreakMayNotIterate(int $n): int +{ + while ($n < 3) { + if (rand(0, 1)) { + $e = 1; + break; + } + return 2; + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $e); + return 1; +} + +function whileTrueBreak(): int +{ + while (true) { + if (rand(0, 1)) { + $e = 1; + break; + } + return 2; + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function doWhileBreak(): int +{ + do { + if (rand(0, 1)) { + $e = 1; + break; + } + return 2; + } while (rand(0, 1)); + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function continueOuterLoop(): int +{ + for ($i = 0; $i < 3; $i++) { + while (true) { + if (rand(0, 1)) { + $e = 1; + continue 2; + } + return 2; + } + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function continueFromSwitch(): int +{ + for ($i = 0; $i < 3; $i++) { + switch (rand(0, 1)) { + case 0: + $e = 1; + continue 2; + default: + return 2; + } + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + return $e; +} + +function catchContinueWithFinally(): int +{ + for ($i = 0; $i < 3; $i++) { + try { + if (rand(0, 1)) { + return 1; + } + throw new \Exception(); + } catch (\Exception $e) { + continue; + } finally { + echo 'x'; + } + } + assertVariableCertainty(TrinaryLogic::createYes(), $e); + throw $e; +} + +function endReachable(): void +{ + for ($i = 0; $i < 3; $i++) { + if (rand(0, 1)) { + $e = 1; + continue; + } + } + assertVariableCertainty(TrinaryLogic::createMaybe(), $e); +} + +function continueTypeAfterLoop(): void +{ + $v = 'str'; + for ($try = 0; $try <= 3; $try++) { + if (rand(0, 1)) { + $v = 5; + continue; + } + return; + } + assertType('5', $v); +} + +function loopHeadFromContinue(): void +{ + $v = 'init'; + for ($i = 0; $i < 3; $i++) { + assertType("1|'init'", $v); + if (rand(0, 1)) { + $v = 1; + continue; + } + return; + } + assertType('1', $v); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-1946.php b/tests/PHPStan/Analyser/nsrt/bug-1946.php new file mode 100644 index 0000000000..b3a0d52aa6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1946.php @@ -0,0 +1,23 @@ +', $i); + assertType('1', $i); } } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 5bd15c105c..4d1d7139f2 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3178,4 +3178,20 @@ public function testBug11041(): void $this->analyse([__DIR__ . '/data/bug-11041.php'], []); } + public function testBug1946(): void + { + $this->analyse([__DIR__ . '/data/bug-1946.php'], [ + [ + 'Parameter #1 $string of function strlen expects string, null given.', + 19, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug11919(): void + { + $this->analyse([__DIR__ . '/data/bug-11919.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11919.php b/tests/PHPStan/Rules/Functions/data/bug-11919.php new file mode 100644 index 0000000000..f68e87a36c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11919.php @@ -0,0 +1,24 @@ +analyse([__DIR__ . '/data/bug-8360.php'], []); } + public function testBug14418(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-14418.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14418.php b/tests/PHPStan/Rules/Variables/data/bug-14418.php new file mode 100644 index 0000000000..c89556eb9a --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-14418.php @@ -0,0 +1,18 @@ +