mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 13:30:49 -05:00
[ty] Fix match exhaustiveness for enum | None unions (#22290)
## Summary If we match on an `TestEnum | None`, then when adding a case like `~Literal[TestEnum.FOO]` (i.e., after `if value == TestEnum.FOO: return`), we'd distribute `Literal[TestEnum.BAR]` on the entire builder, creating `None & Literal[TestEnum.BAR]` which simplified to `Never`. Instead, we should only expand to the remaining members for pieces of the intersection that contain the enum. Now, `(TestEnum | None) & ~Literal[TestEnum.FOO] & ~Literal[TestEnum.BAR]` correctly simplifies to `None` instead of `Never`. Closes https://github.com/astral-sh/ty/issues/2260.
This commit is contained in:
@@ -386,3 +386,52 @@ def _(target: int, flag: NotBoolable):
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## Matching on enum | None without covering None
|
||||
|
||||
When matching on a union of an enum and None, code after the match should still be reachable if None
|
||||
is not covered by any case, even when all enum members are covered.
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Answer(Enum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
def _(answer: Answer | None):
|
||||
y = 0
|
||||
match answer:
|
||||
case Answer.YES:
|
||||
y = 1
|
||||
case Answer.NO:
|
||||
y = 2
|
||||
|
||||
# The match is not exhaustive because None is not covered,
|
||||
# so y could still be 0
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
|
||||
def _(answer: Answer | None):
|
||||
match answer:
|
||||
case Answer.YES:
|
||||
return 1
|
||||
case Answer.NO:
|
||||
return 2
|
||||
|
||||
# Code here is reachable because None is not covered
|
||||
reveal_type(answer) # revealed: None
|
||||
return 3
|
||||
|
||||
class Foo: ...
|
||||
|
||||
def _(answer: Answer | None):
|
||||
match answer:
|
||||
case Answer.YES:
|
||||
return
|
||||
case Answer.NO:
|
||||
return
|
||||
|
||||
# New assignments after the match should not be `Never`
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
@@ -810,13 +810,6 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
ty: Type<'db>,
|
||||
seen_aliases: &mut Vec<Type<'db>>,
|
||||
) -> Self {
|
||||
let contains_enum = |enum_instance| {
|
||||
self.intersections
|
||||
.iter()
|
||||
.flat_map(|intersection| &intersection.positive)
|
||||
.any(|ty| *ty == enum_instance)
|
||||
};
|
||||
|
||||
// See comments above in `add_positive`; this is just the negated version.
|
||||
match ty {
|
||||
Type::TypeAlias(alias) => {
|
||||
@@ -871,12 +864,27 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
},
|
||||
)
|
||||
}
|
||||
Type::EnumLiteral(enum_literal)
|
||||
if contains_enum(enum_literal.enum_class_instance(self.db)) =>
|
||||
{
|
||||
let db = self.db;
|
||||
self.add_positive_impl(
|
||||
UnionType::from_elements(
|
||||
Type::EnumLiteral(enum_literal) => {
|
||||
let enum_instance = enum_literal.enum_class_instance(self.db);
|
||||
|
||||
// Partition intersections into those that contain the enum instance and those that don't.
|
||||
// For intersections containing the enum, we need to expand to remaining members.
|
||||
// For others, we just add the negative normally.
|
||||
let (enum_intersections, other_intersections): (Vec<_>, Vec<_>) = self
|
||||
.intersections
|
||||
.into_iter()
|
||||
.partition(|inner| inner.positive.contains(&enum_instance));
|
||||
|
||||
if enum_intersections.is_empty() {
|
||||
// No inner intersection contains the enum, just add negative normally
|
||||
self.intersections = other_intersections;
|
||||
for inner in &mut self.intersections {
|
||||
inner.add_negative(self.db, ty);
|
||||
}
|
||||
self
|
||||
} else {
|
||||
let db = self.db;
|
||||
let remaining_members = UnionType::from_elements(
|
||||
db,
|
||||
enum_member_literals(
|
||||
db,
|
||||
@@ -884,9 +892,32 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
Some(enum_literal.name(db)),
|
||||
)
|
||||
.expect("Calling `enum_member_literals` on an enum class"),
|
||||
),
|
||||
seen_aliases,
|
||||
)
|
||||
);
|
||||
|
||||
// For enum-containing intersections, add the remaining members as positive
|
||||
let mut enum_builder = IntersectionBuilder {
|
||||
db,
|
||||
order_elements: self.order_elements,
|
||||
intersections: enum_intersections,
|
||||
}
|
||||
.add_positive_impl(remaining_members, seen_aliases);
|
||||
|
||||
// For non-enum intersections, just add the negative normally
|
||||
let mut other_builder = IntersectionBuilder {
|
||||
db,
|
||||
order_elements: self.order_elements,
|
||||
intersections: other_intersections,
|
||||
};
|
||||
for inner in &mut other_builder.intersections {
|
||||
inner.add_negative(db, ty);
|
||||
}
|
||||
|
||||
// Combine the results
|
||||
enum_builder
|
||||
.intersections
|
||||
.extend(other_builder.intersections);
|
||||
enum_builder
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for inner in &mut self.intersections {
|
||||
|
||||
Reference in New Issue
Block a user