Record conditional definedness guards when merging if branches with a side-effect-free condition#5875
Closed
phpstan-bot wants to merge 1 commit into
Closed
Conversation
… a side-effect-free condition - Add `MutatingScope::createConditionalDefinednessExpressions()`, which records that a variable defined only in one `if` branch is defined again whenever the same branch condition holds. The condition is reconstructed from the expressions it narrowed: "type guards" (defined in both branches with a type differing from the merged type, e.g. `$s` narrowed to `'banana'`, or an array gaining a `hasOffset` accessory) and "existence guards" (made certain only in our branch, e.g. `$a['x']` via `isset()`). - All guards are combined into a single `ConditionalExpressionHolder`, so reusing only part of an `&&` condition does not wrongly imply the variable is defined. At least one type guard is required so untracked superglobal accesses (empty other-branch scope) cannot create spurious guards. - Gate the new behavior behind a `createConditionalDefinedness` flag on `mergeWith()`, set by the `if` handler only when the condition has no impure points and there are no elseif branches — a deterministic condition is what guarantees a later identical `if` picks the same branch. This keeps non-deterministic conditions (`rand() && $x === 1`) sound. - Works uniformly for `isset()`, `array_key_exists()`, `instanceof`, `=== `, `!== null`, property comparisons, `is_*()`, etc., and for both `if`/`if-else`, because it builds on the generic branch-merge guard mechanism rather than special-casing functions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When the same condition is used in two separate
ifblocks and the first one defines a variable, PHPStan wrongly reported "Variable might not be defined" in the second block:The same happened with
$_GET['something'] == 'banana',$s === 'banana', etc. This PR makes PHPStan remember that a variable defined inside anifis defined again whenever the same deterministic condition holds, so the second block no longer reports a false positive.Changes
src/Analyser/MutatingScope.phpcreateConditionalDefinednessExpressions()that, when two branches are merged, records aConditionalExpressionHolderfor each variable defined only in one branch: "this variable is defined whenever these guards hold again".$snarrowed to'banana', an array gaining ahasOffsetValueaccessory,$xnarrowed to1);Yes) only in our branch (e.g.$a['x']made certain byisset($a['x'])).&&condition later does not prove the variable defined. At least one type guard is required, which filters out spurious guards created when the other branch's scope tracks nothing for an expression (e.g. an untracked superglobal access in the==example).mergeWith()gets a new optionalbool $createConditionalDefinedness = falseparameter that enables this pass (off by default, so all other merge sites are unaffected).src/Analyser/NodeScopeResolver.phpIf_handler passescreateConditionalDefinedness = trueto the branch merges only when the condition has no impure points and the statement has noelseifbranches.Root cause
PHPStan already tracked "conditional definedness" through the branch-merge guard machinery, but only when the discriminating guard was an expression with a disjoint type across branches and
Yescertainty in both (e.g. the boolean variable in$b = isset(...); if ($b) {...}). For an inlineisset($a['x'])the discriminator is the existence of$a['x'](Yesin the truthy branch, absent in the falsey branch), which the existingcreateConditionalExpressions()could not use as a guard — and the array'shasOffsetnarrowing is a subtype of the falsey branch's plain array type, so it was rejected by thetheirExprIsSuperTypeOfGuardskip added in #5848. As a result no conditional was recorded and the secondifre-reported the variable as possibly undefined.The fix adds a dedicated pass that accepts existence guards and subtype "type guards", combining all of them into one condition. Soundness relies on two gates:
if ($a)afterif ($a && $b)does not match), andrand(0, 1) === 1 && $x === 1does not falsely prove definedness when only$x === 1is reused.Because it builds on the generic merge mechanism, the fix applies uniformly to
isset(),array_key_exists(),instanceof,===/!==,!== null, property comparisons,is_*()checks, and bothifandif/else.Test
tests/PHPStan/Analyser/nsrt/bug-7718.phpusesassertVariableCertainty()to cover:Yes):isset()twice,===on a string twice,===on an int twice,issetwith anelsebranch,array_key_exists()twice,instanceoftwice,!== nulltwice.Maybe): reusing only part of an&&condition (if ($x === 1 && $y === 2)thenif ($x === 1)), a non-deterministic condition (rand(0, 1) === 1 && $x === 1), and a different condition ($s === 'a'then$s === 'b').The test fails before the fix (the positive cases report
Maybe) and passes after it. The full test suite (make tests), PHPStan self-analysis (make phpstan) and coding standards (make cs) all pass.Fixes phpstan/phpstan#7718