[ty] Avoid panic for comparison on synthesized variants (#22509)

## Summary

Like `ProtocolInstance`, we now use `left.cmp(right)` by deriving
`PartialOrd` and `Ord`. IIUC, this uses Salsa ID for Salsa-interned
types, but avoids `None.cmp(None)` for synthesized variants.

Closes https://github.com/astral-sh/ty/issues/2451.
This commit is contained in:
Charlie Marsh
2026-01-12 08:35:56 -05:00
committed by GitHub
parent 5a3deee353
commit f1db842821
3 changed files with 37 additions and 4 deletions

View File

@@ -556,3 +556,27 @@ def _(x: type[object], y: type[object], z: type[object]):
if issubclass(z, Invariant):
reveal_type(z) # revealed: type[Top[Invariant[Unknown]]]
```
## Narrowing with TypedDict unions
Narrowing unions of `int` and multiple TypedDicts using `isinstance(x, dict)` should not panic
during type ordering of normalized intersection types. Regression test for
<https://github.com/astral-sh/ty/issues/2451>.
```py
from typing import Any, TypedDict, cast
class A(TypedDict):
x: str
class B(TypedDict):
y: str
T = int | A | B
def test(a: Any, items: list[T]) -> None:
combined = a or items
v = combined[0]
if isinstance(v, dict):
cast(T, v) # no panic
```

View File

@@ -228,9 +228,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::TypeAlias(_), _) => Ordering::Less,
(_, Type::TypeAlias(_)) => Ordering::Greater,
(Type::TypedDict(left), Type::TypedDict(right)) => {
left.defining_class().cmp(&right.defining_class())
}
(Type::TypedDict(left), Type::TypedDict(right)) => left.cmp(right),
(Type::TypedDict(_), _) => Ordering::Less,
(_, Type::TypedDict(_)) => Ordering::Greater,

View File

@@ -48,7 +48,14 @@ impl Default for TypedDictParams {
/// Type that represents the set of all inhabitants (`dict` instances) that conform to
/// a given `TypedDict` schema.
#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash, get_size2::GetSize)]
///
/// # Ordering
/// Ordering is derived from the variant order (`Class` < `Synthesized`) and the inner types.
/// The Salsa IDs of inner types may change between runs or when the type was garbage collected
/// and recreated.
#[derive(
Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, salsa::Update, Hash, get_size2::GetSize,
)]
pub enum TypedDictType<'db> {
/// A reference to the class (inheriting from `typing.TypedDict`) that specifies the
/// schema of this `TypedDict`.
@@ -878,7 +885,11 @@ pub(super) fn validate_typed_dict_dict_literal<'db>(
}
}
/// # Ordering
/// Ordering is based on the type's salsa-assigned id and not on its values.
/// The id may change between runs, or when the type was garbage collected and recreated.
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct SynthesizedTypedDictType<'db> {
#[returns(ref)]
pub(crate) items: TypedDictSchema<'db>,