[ty] Simplify unions of enum literals and subtypes thereof (#20324)

## Summary

When adding an enum literal `E = Literal[Color.RED]` to a union which
already contained a subtype of that enum literal(!), we were previously
not simplifying the union correctly. My assumption is that our property
tests didn't catch that earlier, because the only possible non-trivial
subytpe of an enum literal that I can think of is `Any & E`. And in
order for that to be detected by the property tests, it would have to
randomly generate `Any & E | E` and then also compare that with `E` on
the other side (in an equivalence test, or the subtyping-antisymmetry
test).

closes https://github.com/astral-sh/ty/issues/1155

## Test Plan

* Added a regression test.
* I also ran the property tests for a while, but probably not for two
months worth of daily CI runs.
This commit is contained in:
David Peter 2025-09-10 15:54:06 +02:00 committed by GitHub
parent 7a75702237
commit 57d1f7132d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 83 additions and 72 deletions

View file

@ -118,7 +118,8 @@ def _(
```py ```py
from enum import Enum from enum import Enum
from typing import Literal from typing import Literal, Any
from ty_extensions import Intersection
class Color(Enum): class Color(Enum):
RED = "red" RED = "red"
@ -139,6 +140,13 @@ def _(
reveal_type(u4) # revealed: Literal[Color.RED, Color.GREEN] reveal_type(u4) # revealed: Literal[Color.RED, Color.GREEN]
reveal_type(u5) # revealed: Color reveal_type(u5) # revealed: Color
reveal_type(u6) # revealed: Color reveal_type(u6) # revealed: Color
def _(
u1: Intersection[Literal[Color.RED], Any] | Literal[Color.RED],
u2: Literal[Color.RED] | Intersection[Literal[Color.RED], Any],
):
reveal_type(u1) # revealed: Literal[Color.RED]
reveal_type(u2) # revealed: Literal[Color.RED]
``` ```
## Do not erase `Unknown` ## Do not erase `Unknown`

View file

@ -444,8 +444,7 @@ impl<'db> UnionBuilder<'db> {
.filter_map(UnionElement::to_type_element) .filter_map(UnionElement::to_type_element)
.any(|ty| Type::EnumLiteral(enum_member_to_add).is_subtype_of(self.db, ty)) .any(|ty| Type::EnumLiteral(enum_member_to_add).is_subtype_of(self.db, ty))
{ {
self.elements self.push_type(Type::EnumLiteral(enum_member_to_add), seen_aliases);
.push(UnionElement::Type(Type::EnumLiteral(enum_member_to_add)));
} }
} }
// Adding `object` to a union results in `object`. // Adding `object` to a union results in `object`.
@ -453,6 +452,12 @@ impl<'db> UnionBuilder<'db> {
self.collapse_to_object(); self.collapse_to_object();
} }
_ => { _ => {
self.push_type(ty, seen_aliases);
}
}
}
fn push_type(&mut self, ty: Type<'db>, seen_aliases: &mut Vec<Type<'db>>) {
let bool_pair = if let Type::BooleanLiteral(b) = ty { let bool_pair = if let Type::BooleanLiteral(b) = ty {
Some(Type::BooleanLiteral(!b)) Some(Type::BooleanLiteral(!b))
} else { } else {
@ -530,8 +535,6 @@ impl<'db> UnionBuilder<'db> {
self.elements.push(UnionElement::Type(ty)); self.elements.push(UnionElement::Type(ty));
} }
} }
}
}
pub(crate) fn build(self) -> Type<'db> { pub(crate) fn build(self) -> Type<'db> {
self.try_build().unwrap_or(Type::Never) self.try_build().unwrap_or(Type::Never)