Skip to content

fix: generic sealed class-string match exhaustiveness for ::class comparisons#5367

Closed
mhert wants to merge 1 commit intophpstan:2.1.xfrom
mhert:fix-sealed-class-match-exhaustiveness
Closed

fix: generic sealed class-string match exhaustiveness for ::class comparisons#5367
mhert wants to merge 1 commit intophpstan:2.1.xfrom
mhert:fix-sealed-class-match-exhaustiveness

Conversation

@mhert
Copy link
Copy Markdown
Contributor

@mhert mhert commented Mar 31, 2026

Summary

GenericClassStringType::tryRemove() did not correctly handle @phpstan-sealed hierarchies when the sealed class has generic type parameters (@template). Match expressions on $foo::class falsely reported "Match expression does not handle remaining values" even when all allowed subtypes were covered.

Follow-up to #5305

Motivation

The fix in #5305 passed $generic (e.g. FooCov<string>) directly to TypeCombinator::remove(), which couldn't correctly subtract sealed subtypes from a generic type. This meant sealed exhaustiveness still failed when the sealed class had template parameters.

Changes

  • src/Type/Generic/GenericClassStringType.php — In the sealed-aware removal path of tryRemove(), construct a plain ObjectType (with the existing subtracted type carried over) instead of passing the generic type directly to TypeCombinator::remove(). This allows sealed subtraction to work correctly regardless of template parameters.

Test Cases

  • tests/PHPStan/Rules/Comparison/data/match-generic-sealed-class-string.php — Two scenarios:
    • Covariant template sealed hierarchy (FooCov<T> sealed to BarCov|BazCov) — exhaustive match produces no error
    • Invariant template with covariant param (FooInv<T> sealed to BarInv|BazInv) — exhaustive match produces no error
  • tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php — Added testGenericSealedClassStringMatch() rule test method.

@ondrejmirtes
Copy link
Copy Markdown
Member

Please open a bug report first.

GenericClassStringType::tryRemove() passed the GenericObjectType directly to
TypeCombinator::remove(), but GenericObjectType::isSuperTypeOf(plain ObjectType)
returns Maybe rather than Yes, so the removal was silently skipped. Strips the
generic parameters before delegating to TypeCombinator::remove() so the existing
sealed subtraction logic in ObjectType::changeSubtractedType() can handle it.
@mhert mhert force-pushed the fix-sealed-class-match-exhaustiveness branch from 74421a4 to 2092cf3 Compare March 31, 2026 14:35
@mhert mhert changed the title fix: sealed class-string match exhaustiveness for ::class comparisons fix: generic sealed class-string match exhaustiveness for ::class comparisons Mar 31, 2026
@ondrejmirtes
Copy link
Copy Markdown
Member

Superseded by #5369

@mhert mhert deleted the fix-sealed-class-match-exhaustiveness branch March 31, 2026 15:39
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.

2 participants