From fa88989ef0085610dd8ddecbd289a5fcc1d38892 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 25 Apr 2025 06:55:54 -0700 Subject: [PATCH] [red-knot] fix detecting a metaclass on a not-explicitly-specialized generic base (#17621) ## Summary After https://github.com/astral-sh/ruff/pull/17620 (which this PR is based on), I was looking at other call sites of `Type::into_class_type`, and I began to feel that _all_ of them were currently buggy due to silently skipping unspecialized generic class literal types (though in some cases the bug hadn't shown up yet because we don't understand legacy generic classes from typeshed), and in every case they would be better off if an unspecialized generic class literal were implicitly specialized with the default specialization (which is the usual Python typing semantics for an unspecialized reference to a generic class), instead of silently skipped. So I changed the method to implicitly apply the default specialization, and added a test that previously failed for detecting metaclasses on an unspecialized generic base. I also renamed the method to `to_class_type`, because I feel we have a strong naming convention where `Type::into_foo` is always a trivial `const fn` that simply returns `Some()` if the type is of variant `Foo` and `None` otherwise. Even the existing method (with it handling both `GenericAlias` and `ClassLiteral`, and distinguishing kinds of `ClassLiteral`) was stretching this convention, and the new version definitely breaks that envelope. ## Test Plan Added a test that failed before this PR. --- .../resources/mdtest/metaclass.md | 19 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 15 ++++++++------- .../src/types/class.rs | 13 +++++++------ .../src/types/class_base.rs | 2 +- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md index fc66cc1220..84499c766a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md @@ -53,6 +53,25 @@ class B(A): ... reveal_type(B.__class__) # revealed: Literal[M] ``` +## Linear inheritance with PEP 695 generic class + +The same is true if the base with the metaclass is a generic class. + +```toml +[environment] +python-version = "3.13" +``` + +```py +class M(type): ... +class A[T](metaclass=M): ... +class B(A): ... +class C(A[int]): ... + +reveal_type(B.__class__) # revealed: Literal[M] +reveal_type(C.__class__) # revealed: Literal[M] +``` + ## Conflict (1) The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b230c6bab8..ab190dd448 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -688,20 +688,21 @@ impl<'db> Type<'db> { matches!(self, Type::ClassLiteral(..)) } - pub const fn into_class_type(self) -> Option> { + /// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`. + /// Since a `ClassType` must be specialized, apply the default specialization to any + /// unspecialized generic class literal. + pub fn to_class_type(self, db: &'db dyn Db) -> Option> { match self { - Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => { - Some(ClassType::NonGeneric(non_generic)) - } + Type::ClassLiteral(class_literal) => Some(class_literal.default_specialization(db)), Type::GenericAlias(alias) => Some(ClassType::Generic(alias)), _ => None, } } #[track_caller] - pub fn expect_class_type(self) -> ClassType<'db> { - self.into_class_type() - .expect("Expected a Type::GenericAlias or non-generic Type::ClassLiteral variant") + pub fn expect_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + self.to_class_type(db) + .expect("Expected a Type::GenericAlias or Type::ClassLiteral variant") } pub const fn is_class_type(&self) -> bool { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 6f8465bbe8..f74e7d276f 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -577,12 +577,13 @@ impl<'db> ClassLiteralType<'db> { self.explicit_bases_query(db) } - /// Iterate over this class's explicit bases, filtering out any bases that are not class objects. + /// Iterate over this class's explicit bases, filtering out any bases that are not class + /// objects, and applying default specialization to any unspecialized generic class literals. fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { self.explicit_bases(db) .iter() .copied() - .filter_map(Type::into_class_type) + .filter_map(|ty| ty.to_class_type(db)) } #[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)] @@ -767,7 +768,7 @@ impl<'db> ClassLiteralType<'db> { (KnownClass::Type.to_class_literal(db), self) }; - let mut candidate = if let Some(metaclass_ty) = metaclass.into_class_type() { + let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { MetaclassCandidate { metaclass: metaclass_ty, explicit_metaclass_of: class_metaclass_was_from, @@ -809,7 +810,7 @@ impl<'db> ClassLiteralType<'db> { // - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663 for base_class in base_classes { let metaclass = base_class.metaclass(db); - let Some(metaclass) = metaclass.into_class_type() else { + let Some(metaclass) = metaclass.to_class_type(db) else { continue; }; if metaclass.is_subclass_of(db, candidate.metaclass) { @@ -2164,7 +2165,7 @@ impl<'db> KnownClass { /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) - .into_class_type() + .to_class_type(db) .map(Type::instance) .unwrap_or_else(Type::unknown) } @@ -2231,7 +2232,7 @@ impl<'db> KnownClass { /// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this. pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) - .into_class_type() + .to_class_type(db) .map(|class| SubclassOfType::from(db, class)) .unwrap_or_else(SubclassOfType::subclass_of_unknown) } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index fa5175a0ab..395ff5268d 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -62,7 +62,7 @@ impl<'db> ClassBase<'db> { pub(super) fn object(db: &'db dyn Db) -> Self { KnownClass::Object .to_class_literal(db) - .into_class_type() + .to_class_type(db) .map_or(Self::unknown(), Self::Class) }