Fix exhaustive match checking for final class objects - #11683
Fix exhaustive match checking for final class objects#11683Piyush Kulkarni (piy-ushk) wants to merge 4 commits into
Conversation
|
🔒 Automated review in progress — Rich Chiodo (@rchiodo) is auto-reviewing this PR. |
| isInstantiableClass(valueSubtypeExpanded) && | ||
| isSameWithoutLiteralValue(subjectSubtypeExpanded, valueSubtypeExpanded) | ||
| ) { | ||
| if (ClassType.isFinal(subjectSubtypeExpanded) || !subjectSubtypeExpanded.priv.includeSubclasses) { |
There was a problem hiding this comment.
Issue · Please address or respond
This condition exceeds the repository's enforced 120-character Prettier formatting limit. Please wrap the clauses before merging.
| if (ClassType.isFinal(subjectSubtypeExpanded) || !subjectSubtypeExpanded.priv.includeSubclasses) { | ||
| return undefined; | ||
| } | ||
| } |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
Add regression coverage showing that a final class-object match is exhaustive and an equivalent non-final class remains non-exhaustive. This analyzer behavior change currently has no test specification.
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. Result: Summary: The targeted `typeEvaluator6` tests could not run because no trusted sandbox image was configured. The PR adds no tests, and existing match tests do not cover exhaustive value matching of final class objects. The new behavior therefore needs direct regression coverage. Test runs: 1 not run
|
|
Thanks for the review Rich Chiodo (@rchiodo) I've pushed an update addressing both pieces of feedback:
|
| isInstantiableClass(valueSubtypeExpanded) && | ||
| isSameWithoutLiteralValue(subjectSubtypeExpanded, valueSubtypeExpanded) | ||
| ) { | ||
| if ( |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
Could this guard account for final classes with metaclasses that override __eq__? A value pattern compares with ==, not identity, so type(inst) can fail to match the class at runtime even when its nominal type is exact; removing it here could incorrectly suppress an exhaustive-match diagnostic. Please add coverage and retain the subtype when equality semantics are nonstandard.
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. Result: Summary: The PR adds targeted coverage for final and non-final class-object match exhaustiveness. No tests executed because the trusted verification sandbox could not start: no image is configured for `microsoft/pyright`. The committed test was confirmed as new, but runtime behavior remains unverified. Test runs: 1 not run
|
Rich Chiodo (rchiodo)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
|
@microsoft-github-policy-service agree |
|
Thanks for pointing that out, Rich Chiodo (@rchiodo) great catch . I didn't consider the case where a custom metaclass overrides I've pushed another update that ensures we only remove the subtype if it has standard equality semantics (its effective metaclass is the built-in I also added regression tests for the custom metaclass case to ensure that it correctly remains non-exhaustive. |
| const isStandardEquality = | ||
| !metaclass || | ||
| (isClass(metaclass) && | ||
| ClassType.isBuiltIn(metaclass, ['type', 'ABCMeta', 'EnumMeta'])); |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
This allowlist also rejects class Meta(type): pass, so final classes using the inherited type.__eq__ remain incorrectly non-exhaustive. Resolve __eq__ through the metaclass MRO and accept it when inherited unchanged from type; add a no-override custom-metaclass regression to confirm the behavior.
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. Result: Summary: The PR adds targeted coverage for final, non-final, and custom-metaclass class-object matches. No test could execute because the trusted sandbox image for `microsoft/pyright` is not configured and local execution was unauthorized. The new test appears to cover all introduced behavior, but runtime verification was impossible. Test runs: 1 not run
|
|
Again, Thank you for the review. |
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. Result: Summary: Verification could not run because no trusted sandbox image is configured for `microsoft/pyright`, and local execution was not authorized. The PR adds targeted coverage for final classes, non-final classes, overridden metaclass equality, and inherited standard equality. No test result can be claimed. Test runs: 1 not run
|
Bill Schnurr (bschnurr)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
What was the issue?
In Python, checking type(inst) for exhaustiveness is challenging because class types are "open" by default—meaning inst could be an instance of a subclass, not necessarily the exact base class. Therefore, Pyright correctly flags that subclasses are left "unhandled" if you simply check for the exact class itself.
However, Pyright was not properly narrowing class types when matched against case statements if those classes were explicitly marked as Final (@Final) (which guarantees they cannot have subclasses). While type(inst) is NS.A worked correctly with Final (@Final) classes, the match statement was lacking this Final (@Final) check in its narrowing logic.
The Fix
I added logic to packages/pyright-internal/src/analyzer/patternMatching.ts to check if a class object pattern match involves a Final (@Final) class. Now, if you match against a Final (@Final) class in a match statement, Pyright correctly removes that exact type from the unhandled options, leading to properly passing exhaustiveness checks.