diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md index 11d1d36b353df..82b2b8f7eb5b2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -275,14 +275,16 @@ c: C[int] = C[int]() reveal_type(c.method("string")) # revealed: Literal["string"] ``` -## Cyclic class definition +## Cyclic class definitions + +### F-bounded quantification A class can use itself as the type parameter of one of its superclasses. (This is also known as the [curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].) -Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). +#### In a stub file -`stub.pyi`: +Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself). ```pyi class Base[T]: ... @@ -291,9 +293,9 @@ class Sub(Base[Sub]): ... reveal_type(Sub) # revealed: Literal[Sub] ``` -A similar case can work in a non-stub file, if forward references are stringified: +#### With string forward references -`string_annotation.py`: +A similar case can work in a non-stub file, if forward references are stringified: ```py class Base[T]: ... @@ -302,9 +304,9 @@ class Sub(Base["Sub"]): ... reveal_type(Sub) # revealed: Literal[Sub] ``` -In a non-stub file, without stringified forward references, this raises a `NameError`: +#### Without string forward references -`bare_annotation.py`: +In a non-stub file, without stringified forward references, this raises a `NameError`: ```py class Base[T]: ... @@ -313,11 +315,23 @@ class Base[T]: ... class Sub(Base[Sub]): ... ``` -## Another cyclic case +### Cyclic inheritance as a generic parameter ```pyi class Derived[T](list[Derived[T]]): ... ``` +### Direct cyclic inheritance + +Inheritance that would result in a cyclic MRO is detected as an error. + +```py +# error: [cyclic-class-definition] +class C[T](C): ... + +# error: [cyclic-class-definition] +class D[T](D[int]): ... +``` + [crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern [f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 9f7e2f37388d7..6f8465bbe8ee3 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -174,6 +174,10 @@ impl<'db> GenericAlias<'db> { pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.origin(db).class(db).definition(db) } + + pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteralType<'db> { + ClassLiteralType::Generic(self.origin(db)) + } } impl<'db> From> for Type<'db> { @@ -1690,8 +1694,12 @@ impl<'db> ClassLiteralType<'db> { visited_classes: &mut IndexSet>, ) -> bool { let mut result = false; - for explicit_base_class in class.fully_static_explicit_bases(db) { - let (explicit_base_class_literal, _) = explicit_base_class.class_literal(db); + for explicit_base in class.explicit_bases(db) { + let explicit_base_class_literal = match explicit_base { + Type::ClassLiteral(class_literal) => *class_literal, + Type::GenericAlias(generic_alias) => generic_alias.class_literal(db), + _ => continue, + }; if !classes_on_stack.insert(explicit_base_class_literal) { return true; } @@ -1705,7 +1713,6 @@ impl<'db> ClassLiteralType<'db> { visited_classes, ); } - classes_on_stack.pop(); } result