Skip to content

Record conditional expressions for more assignment shapes - #6381

Open
ondrejmirtes wants to merge 5 commits into
2.2.xfrom
dependent-types-assign-holders
Open

Record conditional expressions for more assignment shapes#6381
ondrejmirtes wants to merge 5 commits into
2.2.xfrom
dependent-types-assign-holders

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Records conditional expressions for three more assignment shapes:

  • Overlapping ternary arms — a $v = $c ? A : B assignment now records per-direction implications derived from the value that is unique to one arm, instead of only when the two arm types are mutually disjoint.
  • in_array() result$flag = in_array($needle, [literals]) records the reverse holder "if $needle is one of the constant haystack values then $flag is true", so narrowing the needle later re-narrows the flag.
  • Derived assignments — after $b = expr($a) where the right side reads a variable that carries conditional holders, the holders are re-evaluated and re-recorded for the assigned variable, so the dependency chain is not severed.

Also adds a regression test for #12620 (already fixed) and removes two conditions in PHPStan's own source that the new precision proves dead.

Based on 2.2.x. Regression tests added for each issue; a local issue-bot run shows the three fixes plus one collateral fix (#11824) and no regressions.

Closes phpstan/phpstan#7905
Closes phpstan/phpstan#9752
Closes phpstan/phpstan#13054

🤖 Generated with Claude Code

ondrejmirtes and others added 5 commits September 5, 2026 10:14
Both statement orders are covered because the historical false positive
was order-dependent.

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

The conditional expressions recorded for a ternary assignment used to
require the two arm types to be mutually disjoint. Now each direction is
handled independently: the part of an arm's type that the other arm
cannot produce proves that arm was taken, so that arm's condition
narrowings are recorded guarded by the remainder type. The value also
proves the arm expression avoided every concrete value of the other
arm's type, so narrowings like array_key_first($arr) !== null implying
a non-empty $arr are projected as well.

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

Assigning $var = in_array($needle, [...]) already records that a true
$var narrows the needle to the haystack's values. Now the reverse
implication is recorded too: when the haystack's always-present values
are known constants, a needle later narrowed to (a subset of) those
values forces $var to true, because an identical value cannot be missed
by either loose or strict comparison. Through the conditional-expression
fixed point this also unlocks narrowings guarded by $var itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGhnJQpUWRRpg6H5LuWJkA
A variable assigned from a pure expression reading a variable that
existing conditional expressions describe now receives conditional
expressions of its own: the right-hand side is re-evaluated under each
source holder's conditions and consequent, so e.g. the per-key value
implications recorded by foreach over a constant array survive
$functionToCall = 'fetch' . $functionName.

The added precision proves two conditions in PHPStan's own source
always-true/always-false; they are removed here to keep self-analysis
clean.

Co-Authored-By: Claude Fable 5 <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.

1 participant