Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Analyser/StmtHandler/ForeachHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@ private function tryProcessUnrolledConstantArrayForeach(
$allBodyScopes = [];
$allChainScopes = [];
$allBreakScopes = [];
$allTerminatedScopes = [];

$bodyContext = $context->enterUnrolledForeach($totalKeys);

Expand Down Expand Up @@ -807,6 +808,7 @@ private function tryProcessUnrolledConstantArrayForeach(
}

// no later iteration runs, the loop is left only through its break statements
$allTerminatedScopes[] = $bodyResult->getScope();
$chainScope = null;
break;
}
Expand Down Expand Up @@ -853,6 +855,14 @@ private function tryProcessUnrolledConstantArrayForeach(
$endScope = $endScope === null ? $breakScope : $endScope->mergeWith($breakScope);
}

// the code after the loop is unreachable - keep what the terminated
// iterations narrowed instead of falling back to the generic loop pass
if ($endScope === null) {
foreach ($allTerminatedScopes as $terminatedScope) {
$endScope = $endScope === null ? $terminatedScope : $endScope->mergeWith($terminatedScope);
}
}

// 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)
Expand Down
61 changes: 61 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15390.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug15390;

use function PHPStan\Testing\assertType;

enum Status
{
case A;
case B;
case C;
}

function f(Status $status): void
{
foreach ([Status::A, Status::B, Status::C] as $s) {
if ($status === $s) {
return;
}
}

assertType('*NEVER*', $status);
}

function g(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;
}

function h(Status $status): void
{
foreach ([Status::A, Status::B] as $s) {
if ($status === $s) {
return;
}
}

assertType('Bug15390\Status::C', $status);
}

/** @param 1|2|3 $i */
function i(int $i): void
{
foreach ([1, 2, 3] as $v) {
if ($i === $v) {
throw new \Exception();
}
}

assertType('*NEVER*', $i);
}
15 changes: 15 additions & 0 deletions turbo-ext/src/ForeachHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,7 @@ class ForeachHandler
zv::Arr allBodyScopes = zv::Arr::empty();
zv::Arr allChainScopes = zv::Arr::empty();
zv::Arr allBreakScopes = zv::Arr::empty();
zv::Arr allTerminatedScopes = zv::Arr::empty();

zv::Val bodyContext = pt_statement_context_enter_unrolled_foreach(context, totalKeys);
if (UNEXPECTED(bodyContext.isUndef())) return false;
Expand Down Expand Up @@ -1635,6 +1636,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;
allTerminatedScopes.push(zv::Ref(terminatedScope));
}
chainScope = zv::Val::null();
break;
}
Expand Down Expand Up @@ -1692,6 +1699,14 @@ class ForeachHandler
if (UNEXPECTED(!ptlh::mergeOrTake(endScope, entry.value().deref().raw()))) return false;
}

// the code after the loop is unreachable - keep what the terminated
// iterations narrowed instead of falling back to the generic loop pass
if (endScope.isNull()) {
for (auto entry : zv::ArrRef(allTerminatedScopes.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)
Expand Down
Loading