diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index dea3ecfb40b..1a8bd7997c4 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -714,6 +714,7 @@ private function tryProcessUnrolledConstantArrayForeach( $allBodyScopes = []; $allChainScopes = []; $allBreakScopes = []; + $terminatedEndScopes = []; $bodyContext = $context->enterUnrolledForeach($totalKeys); @@ -797,6 +798,7 @@ private function tryProcessUnrolledConstantArrayForeach( } // no later iteration runs, the loop is left only through its break statements + $terminatedEndScopes[] = $bodyResult->getScope(); $chainScope = null; break; } @@ -843,6 +845,13 @@ private function tryProcessUnrolledConstantArrayForeach( $endScope = $endScope === null ? $breakScope : $endScope->mergeWith($breakScope); } + if ($endScope === null) { + // the code after the loop is unreachable, keep the narrowings that made the last iteration terminate + foreach ($terminatedEndScopes as $terminatedEndScope) { + $endScope = $endScope === null ? $terminatedEndScope : $endScope->mergeWith($terminatedEndScope); + } + } + // Unsealed shapes describe zero-or-more additional entries beyond the // explicit keys. Run the scope-generalizing loop on top of the // unrolled explicit iterations so body-scope variables (e.g. counters) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 88c83cac563..030b42a0049 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '28c29b8'; + public const EXPECTED_EXTENSION_VERSION = 'edc7d19'; private static bool $active = false; diff --git a/tests/PHPStan/Analyser/nsrt/bug-15390.php b/tests/PHPStan/Analyser/nsrt/bug-15390.php new file mode 100644 index 00000000000..013f7736515 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15390.php @@ -0,0 +1,90 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug15390; + +use function PHPStan\Testing\assertType; + +enum Status +{ + case A; + case B; + case C; +} + +function allCases(Status $status): void +{ + foreach ([Status::A, Status::B, Status::C] as $s) { + if ($status === $s) { + return; + } + } + + assertType('*NEVER*', $status); +} + +function someCases(Status $status): void +{ + foreach ([Status::A, Status::B] as $s) { + if ($status === $s) { + return; + } + } + + assertType('Bug15390\Status::C', $status); +} + +function keyed(Status $status): int +{ + foreach ([1 => Status::A, 2 => Status::B, 3 => Status::C] as $value => $s) { + if ($status === $s) { + return $value; + } + } + + assertType('*NEVER*', $status); + + return 0; +} + +function inArray(Status $status): int +{ + foreach ([1 => [Status::A, Status::B], 2 => [Status::C]] as $value => $statuses) { + if (in_array($status, $statuses, true)) { + return $value; + } + } + + assertType('*NEVER*', $status); + + return 0; +} + +/** + * @param 1|2|3 $i + */ +function integers(int $i): void +{ + foreach ([1, 2, 3] as $s) { + if ($i === $s) { + return; + } + } + + assertType('*NEVER*', $i); +} + +function withBreak(Status $status): void +{ + foreach ([Status::A, Status::B, Status::C] as $s) { + if ($status === Status::B) { + break; + } + if ($status === $s) { + return; + } + } + + assertType('Bug15390\Status::B', $status); +} diff --git a/turbo-ext/src/ForeachHandler.cpp b/turbo-ext/src/ForeachHandler.cpp index 49ca2f9827f..643368371bb 100644 --- a/turbo-ext/src/ForeachHandler.cpp +++ b/turbo-ext/src/ForeachHandler.cpp @@ -1494,6 +1494,7 @@ class ForeachHandler zv::Arr allBodyScopes = zv::Arr::empty(); zv::Arr allChainScopes = zv::Arr::empty(); zv::Arr allBreakScopes = zv::Arr::empty(); + zv::Arr terminatedEndScopes = zv::Arr::empty(); zv::Val bodyContext = pt_statement_context_enter_unrolled_foreach(context, totalKeys); if (UNEXPECTED(bodyContext.isUndef())) return false; @@ -1626,6 +1627,12 @@ class ForeachHandler } // no later iteration runs, the loop is left only through its break statements + { + zv::Val hold; + zval *terminatedScope = pt_internal_statement_result_scope(bodyResult.raw(), hold); + if (UNEXPECTED(terminatedScope == NULL)) return false; + terminatedEndScopes.push(zv::Ref(terminatedScope)); + } chainScope = zv::Val::null(); break; } @@ -1683,6 +1690,13 @@ class ForeachHandler if (UNEXPECTED(!ptlh::mergeOrTake(endScope, entry.value().deref().raw()))) return false; } + if (endScope.isNull()) { + // the code after the loop is unreachable, keep the narrowings that made the last iteration terminate + for (auto entry : zv::ArrRef(terminatedEndScopes.raw())) { + if (UNEXPECTED(!ptlh::mergeOrTake(endScope, entry.value().deref().raw()))) return false; + } + } + // Unsealed shapes describe zero-or-more additional entries beyond the // explicit keys. Run the scope-generalizing loop on top of the // unrolled explicit iterations so body-scope variables (e.g. counters)