[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

@ -35,8 +35,8 @@ use crate::semantic_index::place::{
PlaceExprWithFlags, PlaceTableBuilder, Scope, ScopeId, ScopeKind, ScopedPlaceId,
};
use crate::semantic_index::predicate::{
CallableAndCallExpr, PatternPredicate, PatternPredicateKind, Predicate, PredicateNode,
PredicateOrLiteral, ScopedPredicateId, StarImportPlaceholderPredicate,
CallableAndCallExpr, ClassPatternKind, PatternPredicate, PatternPredicateKind, Predicate,
PredicateNode, PredicateOrLiteral, ScopedPredicateId, StarImportPlaceholderPredicate,
};
use crate::semantic_index::re_exports::exported_names;
use crate::semantic_index::reachability_constraints::{
@ -697,7 +697,25 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
}
ast::Pattern::MatchClass(pattern) => {
let cls = self.add_standalone_expression(&pattern.cls);
PatternPredicateKind::Class(cls)
PatternPredicateKind::Class(
cls,
if pattern
.arguments
.patterns
.iter()
.all(ast::Pattern::is_irrefutable)
&& pattern
.arguments
.keywords
.iter()
.all(|kw| kw.pattern.is_irrefutable())
{
ClassPatternKind::Irrefutable
} else {
ClassPatternKind::Refutable
},
)
}
ast::Pattern::MatchOr(pattern) => {
let predicates = pattern

View file

@ -116,13 +116,25 @@ pub(crate) enum PredicateNode<'db> {
StarImportPlaceholder(StarImportPlaceholderPredicate<'db>),
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) enum ClassPatternKind {
Irrefutable,
Refutable,
}
impl ClassPatternKind {
pub(crate) fn is_irrefutable(self) -> bool {
matches!(self, ClassPatternKind::Irrefutable)
}
}
/// Pattern kinds for which we support type narrowing and/or static reachability analysis.
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
pub(crate) enum PatternPredicateKind<'db> {
Singleton(Singleton),
Value(Expression<'db>),
Or(Vec<PatternPredicateKind<'db>>),
Class(Expression<'db>),
Class(Expression<'db>, ClassPatternKind),
Unsupported,
}

View file

@ -689,13 +689,20 @@ impl ReachabilityConstraints {
});
truthiness
}
PatternPredicateKind::Class(class_expr) => {
PatternPredicateKind::Class(class_expr, kind) => {
let subject_ty = infer_expression_type(db, subject);
let class_ty = infer_expression_type(db, *class_expr).to_instance(db);
class_ty.map_or(Truthiness::Ambiguous, |class_ty| {
if subject_ty.is_subtype_of(db, class_ty) {
Truthiness::AlwaysTrue
if kind.is_irrefutable() {
Truthiness::AlwaysTrue
} else {
// A class pattern like `case Point(x=0, y=0)` is not irrefutable,
// i.e. it does not match all instances of `Point`. This means that
// we can't tell for sure if this pattern will match or not.
Truthiness::Ambiguous
}
} else if subject_ty.is_disjoint_from(db, class_ty) {
Truthiness::AlwaysFalse
} else {