Skip boolean conditional holders whose target is an operand of the condition-side arm with an independent sibling operand#6060
Open
phpstan-bot wants to merge 1 commit into
Conversation
…ndition-side arm with an independent sibling operand - Pass the condition-side arm expression into `ConditionalExpressionHolderHelper::processBooleanConditionalTypes()` from `BooleanAndHandler` so the helper can tell which expressions the arm's truth depends on. - When building a `BooleanAnd` false-context conditional holder, skip a target that is an operand of the condition-side arm whenever that arm also narrows an independent sibling operand (neither an ancestor nor a descendant of the target). Example: for `$needle !== null && in_array($needle, $haystack)`, the arm's truthy narrowing carries `$haystack` being non-empty, which is necessary but not sufficient for `in_array()` to be true — firing `$needle === null` from `$haystack` being non-empty alone is unsound. - The existing sound narrowings where the sibling operand refers to the same access path (e.g. `isset($data['k'])` narrowing both `$data` and `$data['k']`) are preserved, because such an operand does capture the arm's truth. - Probed the `BooleanOr` true-context mirror: it only under-narrows there (no false positive), so no change was needed in `BooleanOrHandler`.
Contributor
|
Will be fixed by #5983 I think |
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
$needle !== null && in_array($needle, $haystack)wrongly narrowed$needletonullin a laterelseif/ifbranch, producing a falsenotIdentical.alwaysFalse(andidentical.alwaysTrue) on a subsequent$needle !== null/$needle === nullcheck. This regressed in 2.2.3 as part ofthe
#14807/#14908boolean-conditional-holder work.Reaching the
elsebranch of!(A && B)does not imply$needleisnull: the&&is also false when$needleis a non-null string that simply is not in$haystack. This fix stops the unsound narrowing.Changes
src/Analyser/ExprHandler/BooleanAndHandler.php: pass the condition-side armexpression (the opposite arm from the holder side) into each
processBooleanConditionalTypes()call.src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php:?Expr $conditionSideExprparameter and injectExprPrinter;holder, skip a target that is an operand of that arm if the arm also narrows
an independent sibling operand (via
conditionSideHasIndependentOperand()/
collectExpressionStrings()).BooleanOrtrue-context path(
src/Analyser/ExprHandler/BooleanOrHandler.php): it is merely conservativethere (misses some narrowing) and never produces a false positive for this
shape, so it was intentionally left unchanged.
Root cause
The
BooleanAndfalse-context builds conditional expression holders such as"if
in_array($needle, $haystack)is true then$needle === null". The arm'struth cannot be represented directly, so it is decomposed into per-expression
narrowings of the arm's truthy specification. For
in_array()that truthynarrowing carries two operands:
$needle(the value) and$haystack(beingnon-empty). When the holder for target
$needledrops$needle's ownself-condition, only the sibling
$haystack: non-emptycondition survived — and$haystackbeing non-empty is necessary but not sufficient for the arm to betrue. The holder
{$haystack: non-empty} ⟹ {$needle: null}therefore firedwhenever
$haystackwas later narrowed to non-empty (e.g.elseif ($haystack !== [])),collapsing
$needletonull.The fix identifies exactly this situation: the target is an operand of the
condition-side arm and the surviving conditions come from a structurally
independent sibling operand. Cases where the sibling refers to the same access
path — e.g.
isset($data['k'])narrowing both the container$dataand theelement
$data['k']— are kept, because the container narrowing genuinelycaptures the arm's truth.
Test
tests/PHPStan/Analyser/nsrt/bug-14966.php— the reported reproducer plusvariants: plain
arrayhaystack, anintneedle, and the strictin_array(..., true)form (which shares the same root cause and is alsofixed). Each asserts the needle stays
string|null/int|nullin the branchreached when the
&&is false. All assertions fail before the fix (needle iswrongly
null) and pass after it.isset()/array_key_exists()conditional-holder regression tests(
bug-10644,bug-14874,bug-6202,negated-boolean-and-conditional-holders,boolean-and-conditional-holders-mixed-context,bug-14828) continue to pass,confirming the sound narrowings are preserved.
Fixes phpstan/phpstan#14966