Skip boolean conditional expression holders when the antecedent side is an unsplittable compound boolean#5983
Open
phpstan-bot wants to merge 1 commit into
Conversation
…is an unsplittable compound boolean - `ConditionalExpressionHolderHelper::processBooleanConditionalTypes()` now returns no holders when the condition (antecedent) side is a compound boolean whose asserted truth value is a disjunction (`||` asserted true, or `&&` asserted false). Such a side's per-expression narrowing is only a necessary consequence of its truth value, not a sufficient one, so using it as a guard fires the holder unsoundly. - Generalized the existing `isUnsplittableCompoundHolderSide()` into `isUnsplittableCompoundSide()` and applied it to the antecedent side with the opposite polarity (`!$holderSideIsNegated`), mirroring the consequent-side check that already existed. - Threaded the condition-side expression into `processBooleanConditionalTypes()` from both `BooleanAndHandler` and `BooleanOrHandler` call sites. - The `BooleanOr` true-context path receives the same fix for symmetry; a standalone reproducer for that path could not be constructed, but the check lives in the shared helper and is polarity-correct for both operators.
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
Follow-up to #14807 / #5848. When an intermediate
ifsits between two
ifs that re-assert the same enum condition, the narrowingsfrom the first
ifleaked into the later block, producing two false positives:identical.alwaysFalseon$flags->flagA === false, andfunction.alreadyNarrowedTypeonin_array($kind, [...], true)(a regressionintroduced in 2.2.3 by the #14807 fix).
The fix stops the boolean-decomposition machinery from creating an unsound
conditional expression holder whose guard under-approximates a compound
condition.
Changes
src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.phpprocessBooleanConditionalTypes()now bails out early and builds no holderswhen the condition (antecedent) side is an unsplittable compound boolean.
isUnsplittableCompoundHolderSide()toisUnsplittableCompoundSide()and reused it for the antecedent side with the opposite polarity, since the
antecedent's asserted truth value is the negation of the consequent's.
?Expr $conditionSideExprparameter to carry the antecedent expression.src/Analyser/ExprHandler/BooleanAndHandler.phpandsrc/Analyser/ExprHandler/BooleanOrHandler.phpprocessBooleanConditionalTypes()call.Analogous cases probed
BooleanOrtrue context: the same helper handles it, so the fix is appliedsymmetrically. I could not construct a standalone reproducer that leaks via the
BooleanOrpath (with no fix at all it already inferred correct types), so noBooleanOr-specific regression test was kept, but the shared check ispolarity-correct for both operators.
LogicalAnd/LogicalOr(and/orkeywords): already covered becauseisUnsplittableCompoundSide()and the handlers match those node types too.Root cause
processBooleanConditionalTypes()builds holders of the form"IF
<antecedent narrowing>THEN<consequent narrowing>". The antecedentnarrowing is the per-expression narrowing that a boolean operand produces when it
is asserted true/false. For an operand whose asserted truth value is a
disjunction (
$a || $basserted true, or$a && $basserted false), thatper-expression narrowing is only a necessary consequence of the operand being
true, not a sufficient one.
In the repro, the intermediate condition
$forced === false && ((grade===One && extra===false) || (cond && grade!==Three))produced the holder
{$grade ∈ {One,Two}} ⇒ {$forced = true}from theBooleanAndfalse context (antecedent = the(... || ...)disjunction, whosetruth narrows
$gradetoOne|Two). Because$grade ∈ {One,Two}does notimply the disjunction is true, re-asserting
$grade !== Grade::Threein the laterifwrongly set$forced = true, which then chained through the sound{$forced = true} ⇒ {$flags->flagA = true, $kind = K1|K2}holders and leakedthose narrowings.
The existing code already refused to split an unsplittable compound on the
consequent side; the same reasoning applies to the antecedent side, and
that is exactly the symmetric check this PR adds.
Test
tests/PHPStan/Analyser/nsrt/bug-14908.php— asserts that inside the finalif ($grade !== Grade::Three)the leaked expressions stay wide(
$flags->flagAisbool,$kindisKind,$forcedisbool), and thatthe genuinely-sound narrowing (
$forced === truereally implying the firstbranch) still fires.
tests/PHPStan/Rules/Comparison/data/bug-14908.php— the verbatim playgroundreproducer, wired into both
StrictComparisonOfDifferentTypesRuleTest(
identical.alwaysFalse) andImpossibleCheckTypeFunctionCallRuleTest(
function.alreadyNarrowedType), each expecting no errors.All three tests fail before the fix and pass after it.
Fixes phpstan/phpstan#14908