Skip to content

Narrow value type instead of unsetting key in ConstantArrayType::tryRemove for HasOffsetValueType#5776

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-mgcaphb
Open

Narrow value type instead of unsetting key in ConstantArrayType::tryRemove for HasOffsetValueType#5776
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-mgcaphb

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

ConstantArrayType::tryRemove treated HasOffsetValueType the same as HasOffsetType by unsetting the key entirely. This was incorrect because HasOffsetValueType carries 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 the HasOffsetValueType handling out of the combined HasOffsetType || HasOffsetValueType branch. The new logic checks valueTypeToRemove->isSuperTypeOf(currentValueType):

    • yes → unset the key (same as before, 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
  • src/Type/Accessory/HasOffsetValueType.php: Added tryRemove() method (replacing NonRemoveableTypeTrait) to handle removing another HasOffsetValueType with the same offset by narrowing the value type. This fixes the analogous case for IntersectionTypes containing HasOffsetValueType.

  • tests/PHPStan/Type/TypeCombinatorTest.php: Added 6 test cases to dataRemove:

    • ConstantArrayType with partial value overlap (mandatory key)
    • ConstantArrayType with partial value overlap (optional key)
    • ConstantArrayType with full value overlap on optional key
    • ConstantArrayType with partial overlap on multi-key array
    • HasOffsetValueType removing another HasOffsetValueType (standalone)
    • IntersectionType with HasOffsetValueType narrowing through intersection

Root cause

The root cause was ConstantArrayType::tryRemove treating HasOffsetValueType and HasOffsetType identically with a single instanceof check. Both were routed to unsetOffset(), which removes the key entirely. For HasOffsetType this is correct (removing "has key X" means the key is absent). For HasOffsetValueType this is wrong when the value type only partially overlaps — the key should remain with its value type narrowed.

Probed analogous cases

  • UnionType::tryRemove — delegates to TypeCombinator::remove per inner type, no fix needed
  • IntersectionType::tryRemove — delegates via intersectTypes, now benefits from the HasOffsetValueType::tryRemove fix
  • HasOffsetType::tryRemove — uses NonRemoveableTypeTrait, correct (no value type to narrow)
  • ObjectShapeType::tryRemove with HasPropertyType — no HasPropertyValueType exists, not analogous

Test

Added 6 test cases to TypeCombinatorTest::dataRemove covering mandatory keys, optional keys, full overlap, partial overlap, multi-key arrays, standalone HasOffsetValueType, and IntersectionType narrowing. All 3 failing cases now pass with the fix.

Fixes phpstan/phpstan#14711

…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.
Comment thread src/Type/Constant/ConstantArrayType.php
@VincentLanglet VincentLanglet self-assigned this May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ConstantArrayType::tryRemove incorrectly handle HasOffsetValueType

2 participants