Skip to content

Record variable definedness as conditional expressions - #6384

Open
ondrejmirtes wants to merge 4 commits into
2.3.xfrom
dependent-types-certainty-holders
Open

Record variable definedness as conditional expressions#6384
ondrejmirtes wants to merge 4 commits into
2.3.xfrom
dependent-types-certainty-holders

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Records variable definedness — not only type — as conditional expressions in three cases:

  • Short-circuit operand assignment — when a boolean-typed value is assigned, an assignment that must have executed for the value to be truthy is recorded as "if the boolean is true then the variable is defined". So if ($bool) later restores the variable's certainty.
  • try / catch — at the join, when a catch clause binds a catch variable, the try-assigned variables' definedness is recorded as conditional on the catch variable being undefined. So !isset($e) restores them.
  • Constant-string concat-assign$s .= '<constant>' now remaps the holders that target $s through the concatenation instead of invalidating them.

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

Closes phpstan/phpstan#11109
Closes phpstan/phpstan#6608
Closes phpstan/phpstan#9854

🤖 Generated with Claude Code

ondrejmirtes and others added 4 commits September 5, 2026 10:24
…l on the assigned boolean

When a boolean && / || RHS contains an assignment inside an operand, a
truthy && (or falsey ||) result guarantees every short-circuit operand was
evaluated. Diff the RHS walk's truthy/falsey continuation scope against the
merged after-RHS scope for variables whose certainty is Yes there but only
Maybe after the merge, and record conditional expression holders so that
narrowing the assigned variable later restores the certainty and type.

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

At a try/catch join where the catch variable is untracked on the non-catch
path and certainly defined at the end of the catch body, its later
definedness tells the joined paths apart. Record conditional expression
holders with a certainty-No (and, for scopes where any variable can exist,
a maybe-defined-null) condition on the catch variable, restoring the
non-catch certainty and type of variables the join demoted to
maybe-defined.

To let such holders fire, the isset()-style falsey specification in
MutatingScope::applySpecifiedTypes() now publishes the certainty change
into the specified-expressions batch - rescuing the waiting holders around
unsetExpression()'s invalidation, which would otherwise drop them at the
very specification they wait for. A second catch clause merging into the
join drops the holders via the conditional-expressions intersection, so
multi-catch stays conservative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGhnJQpUWRRpg6H5LuWJkA
The definedness-holder changes added three `(string)` casts on array keys
documented as strings - load-bearing under strict_types because numeric-string
keys become ints at runtime. Suppress them inline like ScopeOps does instead
of growing the baseline counts. Also sort the TryCatchHandler imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGhnJQpUWRRpg6H5LuWJkA
`$var .= '<single constant string>'` used to invalidate every
conditional-expression holder mentioning $var, so a format string built
across `if (!empty($target))` lost its correlation with $target and a later
`empty($target)` re-check could not select the matching string. Appending a
fixed suffix is injective, so holders whose target or condition on $var is a
single constant string are remapped through the append instead; everything
else still falls back to invalidation.

Co-Authored-By: Claude Opus 4.8 <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.

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.

Variable $var might not be defined v2 sprintf placeholders and values with if statement Falsy "variable might not be defined"

2 participants