[ty] Fix narrowing and reachability of class patterns with arguments (#19512)

## Summary

I noticed that our type narrowing and reachability analysis was
incorrect for class patterns that are not irrefutable. The test cases
below compare the old and the new behavior:

```py
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

class Other: ...

def _(target: Point):
    y = 1

    match target:
        case Point(0, 0):
            y = 2
        case Point(x=0, y=1):
            y = 3
        case Point(x=1, y=0):
            y = 4
    
    reveal_type(y)  # revealed: Literal[1, 2, 3, 4]    (previously: Literal[2])


def _(target: Point | Other):
    match target:
        case Point(0, 0):
            reveal_type(target)  # revealed: Point
        case Point(x=0, y=1):
            reveal_type(target)  # revealed: Point    (previously: Never)
        case Point(x=1, y=0):
            reveal_type(target)  # revealed: Point    (previously: Never)
        case Other():
            reveal_type(target)  # revealed: Other    (previously: Other & ~Point)
```

## Test Plan

New Markdown test
This commit is contained in:
David Peter 2025-07-23 18:45:03 +02:00 committed by GitHub
parent fa1df4cedc
commit 3d17897c02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 111 additions and 11 deletions

View file

@ -80,6 +80,8 @@ def _(subject: C):
A `case` branch with a class pattern is taken if the subject is an instance of the given class, and
all subpatterns in the class pattern match.
### Without arguments
```py
from typing import final
@ -136,6 +138,51 @@ def _(target: FooSub | str):
reveal_type(y) # revealed: Literal[1, 3, 4]
```
### With arguments
```py
from typing_extensions import assert_never
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
class Other: ...
def _(target: Point):
y = 1
match target:
case Point(0, 0):
y = 2
case Point(x=0, y=1):
y = 3
case Point(x=1, y=0):
y = 4
reveal_type(y) # revealed: Literal[1, 2, 3, 4]
def _(target: Point):
match target:
case Point(x, y): # irrefutable sub-patterns
pass
case _:
assert_never(target)
def _(target: Point | Other):
match target:
case Point(0, 0):
reveal_type(target) # revealed: Point
case Point(x=0, y=1):
reveal_type(target) # revealed: Point
case Point(x=1, y=0):
reveal_type(target) # revealed: Point
case Other():
reveal_type(target) # revealed: Other
```
## Singleton match
Singleton patterns are matched based on identity, not equality comparisons or `isinstance()` checks.