[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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -688,20 +688,21 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::ClassLiteral(..))
|
||||
}
|
||||
|
||||
pub const fn into_class_type(self) -> Option<ClassType<'db>> {
|
||||
/// 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<ClassType<'db>> {
|
||||
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 {
|
||||
|
||||
@@ -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<Item = ClassType<'db>> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user