Skip to content

Keep narrowed types after an unrolled foreach whose iterations all terminate - #6716

Open
janedbal wants to merge 4 commits into
phpstan:2.3.xfrom
janedbal:fix-foreach-enum-never
Open

janedbal wants to merge 4 commits into
phpstan:2.3.xfrom
janedbal:fix-foreach-enum-never

Conversation

@janedbal

@janedbal janedbal commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Closes phpstan/phpstan#15390

A foreach over a constant array that returns for every value no longer narrows the subject after the loop. This is a regression in 2.3.0 from 003d601 (#6420).

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

\PHPStan\dumpType($status); // 2.2.17: *NEVER*, 2.3.0: Status

When every unrolled iteration terminates, the unrolled processing sets the chain scope to null. Without a break, the end scope stays null and ForeachHandler uses the scope of the generic final pass. That scope holds the subject with its full type.

  • ForeachHandler now collects the end scope of each iteration that terminates. It merges these scopes into the end scope when the loop has no other exit.
  • turbo-ext/src/ForeachHandler.cpp gets the same change.
  • tests/PHPStan/Analyser/nsrt/bug-15390.php is the regression test. It fails without the change in ForeachHandler.php and passes with it.

— Claude

…rminate

When every unrolled iteration of a constant-array foreach ends in return
or throw (for example a loop that returns for each enum case), no
iteration reaches the back edge and the unrolled end scope was null.
ForeachHandler then used the scope of the generic final pass, so the
code after the loop saw the subject with its full type instead of never.

The unrolled processing now uses the end scope of the terminating
iteration when the loop has no other exit. This restores the 2.2
behaviour, which 003d601 changed.
…rminate in turbo-ext

Port cfd7d138b to the native ForeachHandler. The native
tryProcessUnrolledConstantArrayForeach() now collects the scope of each
iteration that terminates. When the loop has no chain scope and no break
scope, it merges these scopes into the end scope, in the same order as the
PHP twin.
@janedbal
janedbal marked this pull request as ready for review October 9, 2026 11:10
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Type is not narrowed to never after a foreach over a constant array that returns for every value (regression in 2.3.0)

2 participants