From 534de0039f2cd234bcf3a40c3468d50bfd547d36 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Thu, 20 Nov 2025 15:56:47 -0800 Subject: [PATCH 1/3] [ty] support generic aliases in `type[...]`, like `type[C[int]]` --- .../mdtest/diagnostics/same_names.md | 2 +- .../resources/mdtest/type_of/basic.md | 33 +++++++++++++ .../type_properties/is_assignable_to.md | 11 +++-- .../types/infer/builder/type_expression.rs | 46 +++++++++++++++++-- 4 files changed, 82 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md index 7711a7c3db936d..5414312f9fe735 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/same_names.md @@ -171,7 +171,7 @@ class Config: import generic_a import generic_b -# TODO should be error: [invalid-assignment] "Object of type `` is not assignable to `type[generic_a.Container[int]]`" +# error: [invalid-assignment] "Object of type `` is not assignable to `type[generic_a.Container[int]]`" container: type[generic_a.Container[int]] = generic_b.Container[int] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md index 3a167d528b3440..4e3d9e9f077780 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -174,6 +174,39 @@ def _(x: Foo[int], y: Bar[str], z: list[bytes]): reveal_type(type(z)) # revealed: type[list[bytes]] ``` +## Checking generic `type[]` types + +```toml +[environment] +python-version = "3.12" +``` + +```py +class C[T]: + pass + +class D[T]: + pass + +var: type[C[int]] = C[int] +var: type[C[int]] = D[int] # error: [invalid-assignment] "Object of type `` is not assignable to `type[C[int]]`" +``` + +However, generic `Protocol` classes are still TODO: + +```py +from typing import Protocol + +class Proto[U](Protocol): + def some_method(self): ... + +# TODO: should be error: [invalid-assignment] +var: type[Proto[int]] = C[int] + +def _(p: type[Proto[int]]): + reveal_type(p) # revealed: type[@Todo(type[T] for protocols)] +``` + ## `@final` classes `type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 3ac4f9b65216c8..83c373434481af 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -241,15 +241,18 @@ class Bar(Foo[T_co], Generic[T_co]): ... static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]])) -static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar[Unknown]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar], type[Foo])) static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[Any]])) static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) -# TODO: these should pass (all subscripts inside `type[]` type expressions are currently TODO types) -static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) # error: [static-assert-error] -static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # error: [static-assert-error] +static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) +static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) + +# `TypeOf` does not implicitly default-specialize. The unspecialized class literal object `Bar` does +# not inhabit the type `type[Foo[int]]`. +static_assert(not is_assignable_to(TypeOf[Bar], type[Foo[int]])) ``` ## `type[]` is not assignable to types disjoint from `builtins.type` diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index eff43e23e83a50..3a536249ccef0b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -679,11 +679,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - ast::Expr::Subscript(ast::ExprSubscript { - value, - slice: parameters, - .. - }) => { + ast::Expr::Subscript( + subscript @ ast::ExprSubscript { + value, + slice: parameters, + .. + }, + ) => { let parameters_ty = match self.infer_expression(value, TypeContext::default()) { Type::SpecialForm(SpecialFormType::Union) => match &**parameters { ast::Expr::Tuple(tuple) => { @@ -698,6 +700,40 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } _ => self.infer_subclass_of_type_expression(parameters), }, + value_ty @ Type::ClassLiteral(class_literal) => { + if class_literal.is_protocol(self.db()) { + SubclassOfType::from( + self.db(), + todo_type!("type[T] for protocols").expect_dynamic(), + ) + } else { + match class_literal.generic_context(self.db()) { + Some(generic_context) => { + let db = self.db(); + let specialize = |types: &[Option>]| { + SubclassOfType::from( + db, + class_literal.apply_specialization(db, |_| { + generic_context + .specialize_partial(db, types.iter().copied()) + }), + ) + }; + self.infer_explicit_callable_specialization( + subscript, + value_ty, + generic_context, + specialize, + ) + } + None => { + // TODO: emit a diagnostic if you try to specialize a non-generic class. + self.infer_type_expression(parameters); + todo_type!("specialized non-generic class") + } + } + } + } _ => { self.infer_type_expression(parameters); todo_type!("unsupported nested subscript in type[X]") From 83d2bc237cd167dc99d0b8e5235de4a21c82455f Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Mon, 24 Nov 2025 08:11:49 -0800 Subject: [PATCH 2/3] default specialize when constructing ClassType from ClassLiteral --- .../mdtest/type_properties/is_assignable_to.md | 2 +- crates/ty_python_semantic/src/types.rs | 8 +++++--- crates/ty_python_semantic/src/types/class.rs | 11 +++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 83c373434481af..4eb846c7f2d9ce 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -252,7 +252,7 @@ static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) # `TypeOf` does not implicitly default-specialize. The unspecialized class literal object `Bar` does # not inhabit the type `type[Foo[int]]`. -static_assert(not is_assignable_to(TypeOf[Bar], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]])) ``` ## `type[]` is not assignable to types disjoint from `builtins.type` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a5a2d1c6af6175..47f9f4b40e29ae 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1561,7 +1561,7 @@ impl<'db> Type<'db> { } } Type::ClassLiteral(class_literal) => { - Some(ClassType::NonGeneric(class_literal).into_callable(db)) + Some(class_literal.default_specialization(db).into_callable(db)) } Type::GenericAlias(alias) => Some(ClassType::Generic(alias).into_callable(db)), @@ -2386,7 +2386,7 @@ impl<'db> Type<'db> { .subclass_of() .into_class() .map(|subclass_of_class| { - ClassType::NonGeneric(class).has_relation_to_impl( + class.default_specialization(db).has_relation_to_impl( db, subclass_of_class, inferable, @@ -6687,7 +6687,9 @@ impl<'db> Type<'db> { KnownClass::Float.to_instance(db), ], ), - _ if class.is_typed_dict(db) => Type::typed_dict(*class), + _ if class.is_typed_dict(db) => { + Type::typed_dict(class.default_specialization(db)) + } _ => Type::instance(db, class.default_specialization(db)), }; Ok(ty) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index aaec186b9554a6..2433ee04312d37 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -362,6 +362,11 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> { get_size2::GetSize, )] pub enum ClassType<'db> { + // `NonGeneric` is intended to mean that the `ClassLiteral` has no type parameters. There are + // places where we currently violate this rule (e.g. so that we print `Foo` instead of + // `Foo[Unknown]`), but most callers who need to make a `ClassType` from a `ClassLiteral` + // should use `ClassLiteral::default_specialization` instead of assuming + // `ClassType::NonGeneric`. NonGeneric(ClassLiteral<'db>), Generic(GenericAlias<'db>), } @@ -3662,12 +3667,6 @@ impl<'db> From> for Type<'db> { } } -impl<'db> From> for ClassType<'db> { - fn from(class: ClassLiteral<'db>) -> ClassType<'db> { - ClassType::NonGeneric(class) - } -} - #[salsa::tracked] impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { #[salsa::tracked(cycle_initial=crate::types::variance_cycle_initial, heap_size=ruff_memory_usage::heap_size)] From eb763ae7696f218563a139191cd0292e1cee9da4 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Mon, 24 Nov 2025 12:04:38 -0800 Subject: [PATCH 3/3] put the is_assignable_to test case back where it started, now that it's passing --- .../resources/mdtest/type_properties/is_assignable_to.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 4eb846c7f2d9ce..885d3c2e4f1944 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -241,6 +241,7 @@ class Bar(Foo[T_co], Generic[T_co]): ... static_assert(is_assignable_to(TypeOf[Bar[int]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[bool]], type[Foo[int]])) +static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar[Unknown]], type[Foo[int]])) static_assert(is_assignable_to(TypeOf[Bar], type[Foo])) @@ -249,10 +250,6 @@ static_assert(is_assignable_to(TypeOf[Bar[Any]], type[Foo[int]])) static_assert(not is_assignable_to(TypeOf[Bar[int]], type[Foo[bool]])) static_assert(not is_assignable_to(TypeOf[Foo[bool]], type[Bar[int]])) - -# `TypeOf` does not implicitly default-specialize. The unspecialized class literal object `Bar` does -# not inhabit the type `type[Foo[int]]`. -static_assert(is_assignable_to(TypeOf[Bar], type[Foo[int]])) ``` ## `type[]` is not assignable to types disjoint from `builtins.type`