From 20eb5b5b35b8a0925830a3e6111bb3d6506c8555 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 1 Oct 2025 11:05:54 +0100 Subject: [PATCH] [ty] Fix subtyping of invariant generics specialized with `Any` (#20650) --- .../mdtest/type_properties/is_subtype_of.md | 36 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 26 ++++++++------ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index c3057d6c82..4320b56e72 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -830,6 +830,42 @@ static_assert(not is_subtype_of(object, Any)) static_assert(is_subtype_of(int, Any | int)) static_assert(is_subtype_of(Intersection[Any, int], int)) static_assert(not is_subtype_of(tuple[int, int], tuple[int, Any])) + +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +static_assert(not is_subtype_of(Covariant[Any], Covariant[Any])) +static_assert(not is_subtype_of(Covariant[Any], Covariant[int])) +static_assert(not is_subtype_of(Covariant[int], Covariant[Any])) +static_assert(is_subtype_of(Covariant[Any], Covariant[object])) +static_assert(not is_subtype_of(Covariant[object], Covariant[Any])) + +class Contravariant[T]: + def receive(self, input: T): ... + +static_assert(not is_subtype_of(Contravariant[Any], Contravariant[Any])) +static_assert(not is_subtype_of(Contravariant[Any], Contravariant[int])) +static_assert(not is_subtype_of(Contravariant[int], Contravariant[Any])) +static_assert(not is_subtype_of(Contravariant[Any], Contravariant[object])) +static_assert(is_subtype_of(Contravariant[object], Contravariant[Any])) + +class Invariant[T]: + mutable_attribute: T + +static_assert(not is_subtype_of(Invariant[Any], Invariant[Any])) +static_assert(not is_subtype_of(Invariant[Any], Invariant[int])) +static_assert(not is_subtype_of(Invariant[int], Invariant[Any])) +static_assert(not is_subtype_of(Invariant[Any], Invariant[object])) +static_assert(not is_subtype_of(Invariant[object], Invariant[Any])) + +class Bivariant[T]: ... + +static_assert(is_subtype_of(Bivariant[Any], Bivariant[Any])) +static_assert(is_subtype_of(Bivariant[Any], Bivariant[int])) +static_assert(is_subtype_of(Bivariant[int], Bivariant[Any])) +static_assert(is_subtype_of(Bivariant[Any], Bivariant[object])) +static_assert(is_subtype_of(Bivariant[object], Bivariant[Any])) ``` The same for `Unknown`: diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index fe82ac5c14..fab802450a 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -602,18 +602,22 @@ fn has_relation_in_invariant_position<'db>( base_mat, visitor, ), - // Subtyping between invariant type parameters without a top/bottom materialization involved - // is equivalence - (None, None, TypeRelation::Subtyping) => derived_type.when_equivalent_to(db, *base_type), - (None, None, TypeRelation::Assignability) => derived_type - .has_relation_to_impl(db, *base_type, TypeRelation::Assignability, visitor) + // Subtyping between invariant type parameters without a top/bottom materialization necessitates + // checking the subtyping relation both ways: `A` must be a subtype of `B` *and* `B` must be a + // subtype of `A`. The same applies to assignability. + // + // For subtyping between fully static types, this is the same as equivalence. However, we cannot + // use `is_equivalent_to` (or `when_equivalent_to`) here, because we (correctly) understand + // `list[Any]` as being equivalent to `list[Any]`, but we don't want `list[Any]` to be + // considered a subtype of `list[Any]`. For assignability, we would have the opposite issue if + // we simply checked for equivalence here: `Foo[Any]` should be considered assignable to + // `Foo[list[Any]]` even if `Foo` is invariant, and even though `Any` is not equivalent to + // `list[Any]`, because `Any` is assignable to `list[Any]` and `list[Any]` is assignable to + // `Any`. + (None, None, relation) => derived_type + .has_relation_to_impl(db, *base_type, relation, visitor) .and(db, || { - base_type.has_relation_to_impl( - db, - *derived_type, - TypeRelation::Assignability, - visitor, - ) + base_type.has_relation_to_impl(db, *derived_type, relation, visitor) }), // For gradual types, A <: B (subtyping) is defined as Top[A] <: Bottom[B] (None, Some(base_mat), TypeRelation::Subtyping) => is_subtype_in_invariant_position(