Re-add complement of a dropped self-condition to boolean-decomposition holder types#5943
Open
phpstan-bot wants to merge 1 commit into
Open
Re-add complement of a dropped self-condition to boolean-decomposition holder types#5943phpstan-bot wants to merge 1 commit into
phpstan-bot wants to merge 1 commit into
Conversation
…n holder types
- In `ConditionalExpressionHolderHelper::processBooleanConditionalTypes()`, a condition narrowing the holder's *target* expression itself is dropped (holders fire by matching other expressions). That dropped condition restricted the target to a subset of its values, so the resulting holder over-narrowed the target when only the remaining conditions later hold.
- Now, when such a self-condition is dropped, the values it excluded (the complement of its type within the target's scope type) are unioned back into the holder type, so the holder stays sound.
- Fixes `!(isset($a['foo']) && !is_array($a['foo']))` followed by `array_key_exists('foo', $a)` inferring `$a['foo']` as `array` instead of the correct `array|null` (the `isset` non-null self-condition was silently dropped).
- The same helper backs both `BooleanAndHandler` and `BooleanOrHandler`, and the `is_*` arm is not special-cased, so the mirror `is_int`/`||` cases are fixed by the same change.
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
After
if (isset($a['foo']) && !is_array($a['foo'])) return false;, PHPStan believed that oncearray_key_exists('foo', $a)is true,$a['foo']must be anarray. Butisset()is false both when the key is missing and when its value isnull, so the value can still benull. This produced a falseidentical.alwaysFalseerror on$a['foo'] === null. The fix makes PHPStan inferarray|nullthere.Changes
src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php: whenprocessBooleanConditionalTypes()drops a condition that targets the holder's own expression, the complement of that condition's type (within the target's scope type) is now unioned into the holder type.is_int(and otheris_*narrowing functions): same bug, fixed by the same change — theis_*arm is not special-cased. Covered by a test.||(BooleanOr): shares the same helper. Its truthy form (!isset(...) || is_array(...)) already yielded the soundmixed(no false positive), so it needed no separate fix.isset($o->p): noarray_key_exists-style existence re-check exists for typed properties, so there is no parallel unsound path.Root cause
Boolean conditions are decomposed into "conditional expression holders" of the form if [conditions on other expressions] then [target] is [type]. A condition that narrows the target expression itself cannot be tracked as an antecedent (holders fire by matching other expressions), so it is dropped. For
isset($a['foo'])the narrowing splits into$ahaving offset'foo'(kept) and$a['foo']being non-null (dropped, same expression as the target). The remaining holder — if$ahas offset'foo'then$a['foo']isarray— then fired onarray_key_exists()(which only proves key existence, not non-null), wrongly forcingarrayand droppingnull.The fix recognises that a dropped self-condition restricted the target to a subset of its values; without it the holder must also allow the excluded values. It unions the complement (
target scope typeminusdropped condition type, herenull) back into the holder type, yieldingarray|null.Test
tests/PHPStan/Analyser/nsrt/bug-14874.php: the reported reproducer asserts$a['foo']isarray<mixed, mixed>|nullinside thearray_key_existsbranch, plus anis_intmirror assertingint|null. Both fail without the fix (inferringarray/int) and pass with it.Fixes phpstan/phpstan#14874