[`red-knot`] No `cyclic-class-def` diagnostics for subclasses of cyclic classes (#15561)

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
wooly18 2025-01-20 21:35:29 +08:00 committed by GitHub
parent 2a2ac8a483
commit f82ef32e53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 62 additions and 33 deletions

View File

@ -396,11 +396,10 @@ class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
class Bar(Foo): ...
# TODO: can we avoid emitting the errors for these?
# The classes have cyclic superclasses,
# Avoid emitting the errors for these. The classes have cyclic superclasses,
# but are not themselves cyclic...
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
class Spam(Baz): ... # error: [cyclic-class-definition]
class Baz(Bar, BarCycle): ...
class Spam(Baz): ...
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]

View File

@ -3729,6 +3729,22 @@ pub struct Class<'db> {
known: Option<KnownClass>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
enum InheritanceCycle {
/// The class is cyclically defined and is a participant in the cycle.
/// i.e., it inherits either directly or indirectly from itself.
Participant,
/// The class inherits from a class that is a `Participant` in an inheritance cycle,
/// but is not itself a participant.
Inherited,
}
impl InheritanceCycle {
const fn is_participant(self) -> bool {
matches!(self, InheritanceCycle::Participant)
}
}
#[salsa::tracked]
impl<'db> Class<'db> {
/// Return `true` if this class represents `known_class`
@ -3886,7 +3902,7 @@ impl<'db> Class<'db> {
// Identify the class's own metaclass (or take the first base class's metaclass).
let mut base_classes = self.fully_static_explicit_bases(db).peekable();
if base_classes.peek().is_some() && self.is_cyclically_defined(db) {
if base_classes.peek().is_some() && self.inheritance_cycle(db).is_some() {
// We emit diagnostics for cyclic class definitions elsewhere.
// Avoid attempting to infer the metaclass if the class is cyclically defined:
// it would be easy to enter an infinite loop.
@ -4122,37 +4138,50 @@ impl<'db> Class<'db> {
}
}
/// Return `true` if this class appears to be a cyclic definition,
/// i.e., it inherits either directly or indirectly from itself.
/// Return this class' involvement in an inheritance cycle, if any.
///
/// A class definition like this will fail at runtime,
/// but we must be resilient to it or we could panic.
#[salsa::tracked]
fn is_cyclically_defined(self, db: &'db dyn Db) -> bool {
fn inheritance_cycle(self, db: &'db dyn Db) -> Option<InheritanceCycle> {
/// Return `true` if the class is cyclically defined.
///
/// Also, populates `visited_classes` with all base classes of `self`.
fn is_cyclically_defined_recursive<'db>(
db: &'db dyn Db,
class: Class<'db>,
classes_to_watch: &mut IndexSet<Class<'db>>,
classes_on_stack: &mut IndexSet<Class<'db>>,
visited_classes: &mut IndexSet<Class<'db>>,
) -> bool {
if !classes_to_watch.insert(class) {
return true;
}
let mut result = false;
for explicit_base_class in class.fully_static_explicit_bases(db) {
// Each base must be considered in isolation.
// This is due to the fact that if a class uses multiple inheritance,
// there could easily be a situation where two bases have the same class in their MROs;
// that isn't enough to constitute the class being cyclically defined.
let classes_to_watch_len = classes_to_watch.len();
if is_cyclically_defined_recursive(db, explicit_base_class, classes_to_watch) {
if !classes_on_stack.insert(explicit_base_class) {
return true;
}
classes_to_watch.truncate(classes_to_watch_len);
if visited_classes.insert(explicit_base_class) {
// If we find a cycle, keep searching to check if we can reach the starting class.
result |= is_cyclically_defined_recursive(
db,
explicit_base_class,
classes_on_stack,
visited_classes,
);
}
classes_on_stack.pop();
}
false
result
}
self.fully_static_explicit_bases(db)
.any(|base_class| is_cyclically_defined_recursive(db, base_class, &mut IndexSet::new()))
let visited_classes = &mut IndexSet::new();
if !is_cyclically_defined_recursive(db, self, &mut IndexSet::new(), visited_classes) {
None
} else if visited_classes.contains(&self) {
Some(InheritanceCycle::Participant)
} else {
Some(InheritanceCycle::Inherited)
}
}
}

View File

@ -576,16 +576,17 @@ impl<'db> TypeInferenceBuilder<'db> {
// Iterate through all class definitions in this scope.
for (class, class_node) in class_definitions {
// (1) Check that the class does not have a cyclic definition
if class.is_cyclically_defined(self.db()) {
self.context.report_lint(
&CYCLIC_CLASS_DEFINITION,
class_node.into(),
format_args!(
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
class.name(self.db()),
class.name(self.db())
),
);
if let Some(inheritance_cycle) = class.inheritance_cycle(self.db()) {
if inheritance_cycle.is_participant() {
self.context.report_lint(
&CYCLIC_CLASS_DEFINITION,
class_node.into(),
format_args!(
"Cyclic definition of `{}` (class cannot inherit from itself)",
class.name(self.db())
),
);
}
// Attempting to determine the MRO of a class or if the class has a metaclass conflict
// is impossible if the class is cyclically defined; there's nothing more to do here.
continue;

View File

@ -42,7 +42,7 @@ impl<'db> Mro<'db> {
fn of_class_impl(db: &'db dyn Db, class: Class<'db>) -> Result<Self, MroErrorKind<'db>> {
let class_bases = class.explicit_bases(db);
if !class_bases.is_empty() && class.is_cyclically_defined(db) {
if !class_bases.is_empty() && class.inheritance_cycle(db).is_some() {
// We emit errors for cyclically defined classes elsewhere.
// It's important that we don't even try to infer the MRO for a cyclically defined class,
// or we'll end up in an infinite loop.