Fix match exhaustiveness and generic preservation for ::class comparisons#5305
Open
mhert wants to merge 2 commits intophpstan:2.1.xfrom
Open
Fix match exhaustiveness and generic preservation for ::class comparisons#5305mhert wants to merge 2 commits intophpstan:2.1.xfrom
mhert wants to merge 2 commits intophpstan:2.1.xfrom
Conversation
Collaborator
|
You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x. |
e046593 to
3b9ff34
Compare
3b9ff34 to
708c2c2
Compare
Collaborator
|
This pull request has been marked as ready for review. |
…ss for sealed classes Closes phpstan/phpstan#12241 When using match($foo::class) on a @phpstan-sealed class hierarchy, PHPStan reported "Match expression does not handle remaining value" even when all allowed subtypes were covered. This happened because GenericClassStringType::tryRemove() did not consult the sealed hierarchy's allowed subtypes when removing a class-string constant. Extended tryRemove() to progressively subtract allowed subtypes from the class-string type. Each match arm removes one subtype until all are exhausted and the type becomes never, making the match exhaustive.
…lass comparison When narrowing a union type like Cat<string>|Dog<string> via $a::class === Cat::class, the generic type parameters were discarded — the narrowed type became plain Cat instead of Cat<string>. This caused method return types to be inferred as mixed instead of the concrete generic parameter. Added narrowTypePreservingGenerics() to TypeSpecifier which preserves generic information through two strategies: intersecting with the current union type (which lets TypeCombinator distribute and pick the matching generic member), and inferring template parameters through the class hierarchy for single generic parent types.
708c2c2 to
23b1556
Compare
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
Two related fixes for
::classcomparison type narrowing in match expressions and if-else conditions.match($foo::class)on a@phpstan-sealedhierarchy falsely reported "Match expression does not handle remaining value" even when all allowed subtypes were covered.Cat<string>|Dog<string>via$a::class === Cat::classdiscarded generic type parameters — the type became plainCatinstead ofCat<string>.Closes phpstan/phpstan#12241
Root Cause
Sealed exhaustiveness:
GenericClassStringType::tryRemove()only handled final classes — it had no awareness of@phpstan-sealedhierarchies. Removing a class-string constant for a non-final sealed subtype was a no-op, so the type was never fully exhausted tonever.Generic preservation: The
$a::class === 'Foo'handler inTypeSpecifier::resolveNormalizedIdentical()constructed a plainObjectTypewithout consulting the current variable type for generic information. The generic parameters from the original union were lost.Fix
Sealed exhaustiveness: Extended
GenericClassStringType::tryRemove()to consultClassReflection::getAllowedSubTypes(). Each match arm progressively subtracts one allowed subtype viaObjectTypesubtraction. When all subtypes are removed, the type becomesnever, making the match exhaustive. Special-cases concrete (non-abstract) sealed parents to avoid self-referential subtraction.Generic preservation: Added
TypeSpecifier::narrowTypePreservingGenerics()which preserves generics through two strategies:TypeCombinator::intersect()distributes over members and picks the matching generic memberinferTemplateTypes()Test Plan
MatchExpressionRuleTest::testSealedClassStringMatch— exhaustive matches produce no error, partial matches report remaining valuensrt/sealed-class-string-match.php— progressive class-string narrowing, concrete parents, sealed interfaces, falsy branch (!==), match expressionsnsrt/class-string-match-preserves-generics.php— generic preservation for unions, single parents, final classes, mirror syntax, match arms with method callsCo-Authored-By: Claude Code