Narrow value type instead of unsetting key in ConstantArrayType::tryRemove for HasOffsetValueType#5776
Open
phpstan-bot wants to merge 1 commit into
Open
Conversation
…Remove` for `HasOffsetValueType`
- `ConstantArrayType::tryRemove` previously treated `HasOffsetValueType` identically
to `HasOffsetType`, unsetting the key entirely. Now it checks the value type overlap:
- `yes` → unset the key (same as `HasOffsetType`)
- `no` → return null (no impact)
- `maybe` → narrow the value type at that key via `TypeCombinator::remove`,
preserving the key's original optionality
- Add `HasOffsetValueType::tryRemove` to handle removing another
`HasOffsetValueType` with the same offset — narrows the value type instead of
returning null. This fixes the analogous case for IntersectionTypes containing
`HasOffsetValueType` (e.g. `array & hasOffsetValue('a', string|int)`).
- Probed other tryRemove implementations (UnionType, IntersectionType, HasOffsetType,
ObjectShapeType) — all either delegate correctly or are not affected.
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
ConstantArrayType::tryRemovetreatedHasOffsetValueTypethe same asHasOffsetTypeby unsetting the key entirely. This was incorrect becauseHasOffsetValueTypecarries a value type constraint — when only part of the value type overlaps, the key should be preserved with a narrowed value type rather than removed.Changes
src/Type/Constant/ConstantArrayType.php: Split theHasOffsetValueTypehandling out of the combinedHasOffsetType || HasOffsetValueTypebranch. The new logic checksvalueTypeToRemove->isSuperTypeOf(currentValueType):yes→ unset the key (same as before, same asHasOffsetType)no→ return null (no impact)maybe→ narrow the value type at that key viaTypeCombinator::remove, preserving the key's original optionalitysrc/Type/Accessory/HasOffsetValueType.php: AddedtryRemove()method (replacingNonRemoveableTypeTrait) to handle removing anotherHasOffsetValueTypewith the same offset by narrowing the value type. This fixes the analogous case forIntersectionTypes containingHasOffsetValueType.tests/PHPStan/Type/TypeCombinatorTest.php: Added 6 test cases todataRemove:ConstantArrayTypewith partial value overlap (mandatory key)ConstantArrayTypewith partial value overlap (optional key)ConstantArrayTypewith full value overlap on optional keyConstantArrayTypewith partial overlap on multi-key arrayHasOffsetValueTyperemoving anotherHasOffsetValueType(standalone)IntersectionTypewithHasOffsetValueTypenarrowing through intersectionRoot cause
The root cause was
ConstantArrayType::tryRemovetreatingHasOffsetValueTypeandHasOffsetTypeidentically with a singleinstanceofcheck. Both were routed tounsetOffset(), which removes the key entirely. ForHasOffsetTypethis is correct (removing "has key X" means the key is absent). ForHasOffsetValueTypethis is wrong when the value type only partially overlaps — the key should remain with its value type narrowed.Probed analogous cases
UnionType::tryRemove— delegates toTypeCombinator::removeper inner type, no fix neededIntersectionType::tryRemove— delegates viaintersectTypes, now benefits from theHasOffsetValueType::tryRemovefixHasOffsetType::tryRemove— usesNonRemoveableTypeTrait, correct (no value type to narrow)ObjectShapeType::tryRemovewithHasPropertyType— noHasPropertyValueTypeexists, not analogousTest
Added 6 test cases to
TypeCombinatorTest::dataRemovecovering mandatory keys, optional keys, full overlap, partial overlap, multi-key arrays, standaloneHasOffsetValueType, andIntersectionTypenarrowing. All 3 failing cases now pass with the fix.Fixes phpstan/phpstan#14711