diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index b6c712f94b..a0f1c3fd8b 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -360,3 +360,14 @@ type X = tuple[X, int] def _(x: X): reveal_type(x is x) # revealed: bool ``` + +### Recursive invariant + +```py +type X = dict[str, X] +type Y = X | str | dict[str, Y] + +def _(y: Y): + if isinstance(y, dict): + reveal_type(y) # revealed: dict[str, X] | dict[str, Y] +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7f0edaa7b6..97e86737f8 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1543,6 +1543,7 @@ impl<'db> Type<'db> { /// Return `true` if it would be redundant to add `self` to a union that already contains `other`. /// /// See [`TypeRelation::Redundancy`] for more details. + #[salsa::tracked(cycle_fn=is_redundant_with_cycle_recover, cycle_initial=is_redundant_with_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool { self.has_relation_to(db, other, InferableTypeVars::None, TypeRelation::Redundancy) .is_always_satisfied() @@ -7326,6 +7327,25 @@ impl<'db> VarianceInferable<'db> for Type<'db> { } } +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_redundant_with_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &bool, + _count: u32, + _subtype: Type<'db>, + _supertype: Type<'db>, +) -> salsa::CycleRecoveryAction { + salsa::CycleRecoveryAction::Iterate +} + +fn is_redundant_with_cycle_initial<'db>( + _db: &'db dyn Db, + _subtype: Type<'db>, + _supertype: Type<'db>, +) -> bool { + true +} + fn apply_specialization_cycle_recover<'db>( _db: &'db dyn Db, _value: &Type<'db>,