Skip to content

Preserve conditional expression holders across more scope merges - #6385

Open
ondrejmirtes wants to merge 3 commits into
2.3.xfrom
dependent-types-merge-survival
Open

Preserve conditional expression holders across more scope merges#6385
ondrejmirtes wants to merge 3 commits into
2.3.xfrom
dependent-types-merge-survival

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Keeps conditional-expression holders alive across two more merge situations:

  • Satisfied consequent — a one-sided holder is preserved when the other branch's flat type for the holder's target already satisfies the holder's consequent. The consequent then holds on both paths under the guard, so applying it later is sound.
  • Loop-body reassignment — a certainty-bearing holder for a variable that is reassigned inside a loop body is re-created at the loop-convergence merge, so a repeated flag check inside the loop restores the variable's definedness.

Regression tests added; the full NodeScopeResolverTest stays green and a local issue-bot run confirms the two fixes with no regressions.

Closes phpstan/phpstan#9628
Closes phpstan/phpstan#6830

🤖 Generated with Claude Code

ondrejmirtes and others added 2 commits September 5, 2026 10:11
… in the other branch

An if-merge intersects conditional expression holders by exact key, so a
holder recorded only in one branch is dropped even when the other branch's
flat state for the holder's target already satisfies the consequent - a
subtype with at-least-as-strong certainty. In that case the consequent holds
on both paths whenever the guard matches, so firing the holder later is
sound and the holder can survive the merge alongside the vacuous-guard
rescue.

Closes phpstan/phpstan#9628

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGhnJQpUWRRpg6H5LuWJkA
… across merges

Loop convergence merges the body-end scope with the loop-entry scope, and
both sides often carry a Yes-certainty holder for the same target under the
same guard - just with different consequent types (the pre-loop assignment's
type on one side, the body reassignment's type on the other). The exact-key
intersection dropped both, losing the guard-implies-defined fact and
producing "might not be defined" next to "if condition is always true".

Whichever side a merged path came from, the guard matching later implies one
of the recorded consequents, so a holder with the union of the consequent
types under their shared certainty holds on every merged path.

Closes phpstan/phpstan#6830

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGhnJQpUWRRpg6H5LuWJkA
@phpstan-bot

Copy link
Copy Markdown
Collaborator

You've opened the pull request against the latest branch 2.3.x. PHPStan 2.3 is not going to be released for months. If your code is relevant on 2.2.x and you want it to be released sooner, please rebase your pull request and change its target to 2.2.x.

An ErrorType consequent (e.g. an access to an undefined property) is a
subtype of everything, so it looked "already satisfied" and its holder was
preserved across the merge. Applying it tracked the expression and hid the
underlying error - a false negative on undefined-property access. Skip
holders whose consequent or other-branch state is an ErrorType, and use
early exit in the preservation loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGhnJQpUWRRpg6H5LuWJkA
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.

PHPStan fails to infer that null was implicity subtracted Variable inside loop might not be defined.

2 participants