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
9 changes: 9 additions & 0 deletions src/Analyser/StmtHandler/ForeachHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,7 @@ private function tryProcessUnrolledConstantArrayForeach(
$allBodyScopes = [];
$allChainScopes = [];
$allBreakScopes = [];
$terminatedEndScopes = [];

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

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '28c29b8';
public const EXPECTED_EXTENSION_VERSION = 'edc7d19';

private static bool $active = false;

Expand Down
90 changes: 90 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15390.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php // lint >= 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);
}
14 changes: 14 additions & 0 deletions turbo-ext/src/ForeachHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
Expand Down
Loading