Skip non-discriminating guards in createConditionalExpressions even when target is not tracked in the other scope#5676
Open
phpstan-bot wants to merge 1 commit into
Conversation
… when target is not tracked in the other scope - In `MutatingScope::createConditionalExpressions`, the filter that skips non-discriminating guards previously required the target expression to exist in the other scope's expression types. When the target was absent (e.g. `$options['multiple']` not explicitly narrowed in the truthy branch), the filter did not fire, allowing guards like `$instructions` (an unrelated mutable variable) to create spurious conditional expression holders. - Add an additional check: if the guard variable's type in our scope is definitively a supertype of its type in the other scope (`.yes()`), the guard is non-discriminating regardless of the target's presence, so skip it. - The original check (requiring both target and guard in the other scope, using `!.no()`) is preserved for cases where the target IS tracked, to avoid regressions with `maybe` supertype results (e.g. `Event` vs `OrderInterface`). - Also fixes the same bug with strict comparison (`!==` instead of `!=`).
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 an array shape's keys are accessed in conditions (
$options['multiple'],$options['total']) and an unrelated mutable variable ($instructions) is modified in if-branches, PHPStan could incorrectly narrow the array key's type through a chain of conditional expression holders. After three successive if-blocks,$options['multiple']was narrowed from0|1|2to1despite no condition warranting that narrowing.Changes
MutatingScope::createConditionalExpressions(src/Analyser/MutatingScope.php, line ~3750) to also skip guards when the guard variable's type is definitively a supertype (.yes()) of its type in the other scope, even when the target expression is not tracked in the other scope.!.no()match) is preserved as the first branch of an OR condition, ensuring no regressions for cases likeEvent/OrderInterfacewhereisSuperTypeOfreturnsmaybe.Analogous cases probed
!==): Also broken, fixed by the same change.&&condition instead of||: Not broken (different guard structure).Root cause
createConditionalExpressionscreates conditional expression holders linking variables that differ between merged branches. When$instructions(an array that gets elements appended) and$options['multiple']both differed between branches, a conditional was created: "when$instructionsisarray{'foo'}then$options['multiple']is1".This conditional was not filtered out because the filter at line 3750 required the target expression (
$options['multiple']) to exist in the other scope's expression types. Since$options['multiple']was not explicitly narrowed in the truthy branch (only the full$optionsarray was tracked), the filter didn't fire.Later, a chain reaction in
filterBySpecifiedTypesadded$instructionstospecifiedExpressionsvia another conditional, and Pass 2'sisSuperTypeOfcheck matched the guardarray{'foo'}againstarray{0: 'foo', 1: 'bar'|'baz', 2?: 'baz'}due toConstantArrayType's width subtyping (fewer keys = more general = supertype). This incorrectly triggered the conditional, narrowing$options['multiple']to1.The fix adds a target-independent guard check: if the guard type is definitively a supertype of the other scope's type for that variable, the guard cannot distinguish between branches and should be skipped.
Test
tests/PHPStan/Analyser/nsrt/bug-14595.phpwith two test functions:arrayAppendGuard: reproduces the original bug with!=comparisonstrictComparisonGuard: reproduces the same bug with!==comparison$options['multiple']remains0|1|2after all three if-blocks.Fixes phpstan/phpstan#14595