From 3932f7c849b40c0a018aeee588f54471e4f93f36 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 26 Sep 2025 15:05:03 +0200 Subject: [PATCH] [ty] Fix subtyping for dynamic specializations (#20592) ## Summary Fixes a bug observed by @AlexWaygood where `C[Any] <: C[object]` should hold for a class that is covariant in its type parameter (and similar subtyping relations involving dynamic types for other variance configurations). ## Test Plan New and updated Markdown tests --- .../mdtest/exhaustiveness_checking.md | 8 ++-- .../mdtest/generics/pep695/variance.md | 38 +++++++++++-------- .../ty_python_semantic/src/types/generics.rs | 12 ------ 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 53d272b834..800ae7c7bb 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -312,12 +312,10 @@ def match_exhaustive(x: A[D] | B[E] | C[F]): case C(): pass case _: - # TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456) - no_diagnostic_here # error: [unresolved-reference] - assert_never(x) # error: [type-assertion-failure] + no_diagnostic_here + assert_never(x) -# TODO: false-positive diagnostic (https://github.com/astral-sh/ty/issues/456) -def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type] +def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: match x: case A(): return 0 diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index 161c222c4c..4c96a3c4f4 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -28,7 +28,7 @@ get from the sequence is a valid `int`. ```py from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown -from typing import Any +from typing import Any, Never class A: ... class B(A): ... @@ -60,6 +60,8 @@ static_assert(not is_subtype_of(C[A], C[Any])) static_assert(not is_subtype_of(C[B], C[Any])) static_assert(not is_subtype_of(C[Any], C[A])) static_assert(not is_subtype_of(C[Any], C[B])) +static_assert(is_subtype_of(C[Any], C[object])) +static_assert(is_subtype_of(C[Never], C[Any])) static_assert(is_subtype_of(D[B], C[A])) static_assert(not is_subtype_of(D[A], C[B])) @@ -104,7 +106,7 @@ that you pass into the consumer is a valid `int`. ```py from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown -from typing import Any +from typing import Any, Never class A: ... class B(A): ... @@ -135,6 +137,8 @@ static_assert(not is_subtype_of(C[A], C[Any])) static_assert(not is_subtype_of(C[B], C[Any])) static_assert(not is_subtype_of(C[Any], C[A])) static_assert(not is_subtype_of(C[Any], C[B])) +static_assert(is_subtype_of(C[object], C[Any])) +static_assert(is_subtype_of(C[Any], C[Never])) static_assert(not is_subtype_of(D[B], C[A])) static_assert(is_subtype_of(D[A], C[B])) @@ -192,7 +196,7 @@ since we can't know in advance which of the allowed methods you'll want to use. ```py from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown -from typing import Any +from typing import Any, Never class A: ... class B(A): ... @@ -225,6 +229,8 @@ static_assert(not is_subtype_of(C[A], C[Any])) static_assert(not is_subtype_of(C[B], C[Any])) static_assert(not is_subtype_of(C[Any], C[A])) static_assert(not is_subtype_of(C[Any], C[B])) +static_assert(not is_subtype_of(C[object], C[Any])) +static_assert(not is_subtype_of(C[Any], C[Never])) static_assert(not is_subtype_of(D[B], C[A])) static_assert(not is_subtype_of(D[A], C[B])) @@ -261,8 +267,8 @@ static_assert(not is_equivalent_to(D[Any], C[Unknown])) ## Bivariance With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact, -gradually equivalent to) each other, and all fully static specializations are subtypes of (and -equivalent to) each other. +gradually equivalent to) each other, and all specializations are subtypes of (and equivalent to) +each other. This is a bit of pathological case, which really only happens when the class doesn't use the typevar at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_ @@ -270,7 +276,7 @@ the typevar was used.) ```py from ty_extensions import is_assignable_to, is_equivalent_to, is_subtype_of, static_assert, Unknown -from typing import Any +from typing import Any, Never class A: ... class B(A): ... @@ -298,18 +304,20 @@ static_assert(is_assignable_to(D[Any], C[B])) static_assert(is_subtype_of(C[B], C[A])) static_assert(is_subtype_of(C[A], C[B])) -static_assert(not is_subtype_of(C[A], C[Any])) -static_assert(not is_subtype_of(C[B], C[Any])) -static_assert(not is_subtype_of(C[Any], C[A])) -static_assert(not is_subtype_of(C[Any], C[B])) -static_assert(not is_subtype_of(C[Any], C[Any])) +static_assert(is_subtype_of(C[A], C[Any])) +static_assert(is_subtype_of(C[B], C[Any])) +static_assert(is_subtype_of(C[Any], C[A])) +static_assert(is_subtype_of(C[Any], C[B])) +static_assert(is_subtype_of(C[Any], C[Any])) +static_assert(is_subtype_of(C[object], C[Any])) +static_assert(is_subtype_of(C[Any], C[Never])) static_assert(is_subtype_of(D[B], C[A])) static_assert(is_subtype_of(D[A], C[B])) -static_assert(not is_subtype_of(D[A], C[Any])) -static_assert(not is_subtype_of(D[B], C[Any])) -static_assert(not is_subtype_of(D[Any], C[A])) -static_assert(not is_subtype_of(D[Any], C[B])) +static_assert(is_subtype_of(D[A], C[Any])) +static_assert(is_subtype_of(D[B], C[Any])) +static_assert(is_subtype_of(D[Any], C[A])) +static_assert(is_subtype_of(D[Any], C[B])) static_assert(is_equivalent_to(C[A], C[A])) static_assert(is_equivalent_to(C[B], C[B])) diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 0edb463366..3eb227b647 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -841,18 +841,6 @@ impl<'db> Specialization<'db> { .zip(self.types(db)) .zip(other.types(db)) { - // As an optimization, we can return early if either type is dynamic, unless - // we're dealing with a top or bottom materialization. - if other_materialization_kind.is_none() - && self_materialization_kind.is_none() - && (self_type.is_dynamic() || other_type.is_dynamic()) - { - match relation { - TypeRelation::Assignability => continue, - TypeRelation::Subtyping => return ConstraintSet::from(false), - } - } - // Subtyping/assignability of each type in the specialization depends on the variance // of the corresponding typevar: // - covariant: verify that self_type <: other_type