mirror of https://github.com/astral-sh/ruff
[`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:
parent
2a2ac8a483
commit
f82ef32e53
|
|
@ -396,11 +396,10 @@ class Foo: ...
|
||||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||||
class Bar(Foo): ...
|
class Bar(Foo): ...
|
||||||
|
|
||||||
# TODO: can we avoid emitting the errors for these?
|
# Avoid emitting the errors for these. The classes have cyclic superclasses,
|
||||||
# The classes have cyclic superclasses,
|
|
||||||
# but are not themselves cyclic...
|
# but are not themselves cyclic...
|
||||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
|
class Baz(Bar, BarCycle): ...
|
||||||
class Spam(Baz): ... # error: [cyclic-class-definition]
|
class Spam(Baz): ...
|
||||||
|
|
||||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||||
|
|
|
||||||
|
|
@ -3729,6 +3729,22 @@ pub struct Class<'db> {
|
||||||
known: Option<KnownClass>,
|
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]
|
#[salsa::tracked]
|
||||||
impl<'db> Class<'db> {
|
impl<'db> Class<'db> {
|
||||||
/// Return `true` if this class represents `known_class`
|
/// 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).
|
// 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();
|
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.
|
// We emit diagnostics for cyclic class definitions elsewhere.
|
||||||
// Avoid attempting to infer the metaclass if the class is cyclically defined:
|
// Avoid attempting to infer the metaclass if the class is cyclically defined:
|
||||||
// it would be easy to enter an infinite loop.
|
// 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,
|
/// Return this class' involvement in an inheritance cycle, if any.
|
||||||
/// i.e., it inherits either directly or indirectly from itself.
|
|
||||||
///
|
///
|
||||||
/// A class definition like this will fail at runtime,
|
/// A class definition like this will fail at runtime,
|
||||||
/// but we must be resilient to it or we could panic.
|
/// but we must be resilient to it or we could panic.
|
||||||
#[salsa::tracked]
|
#[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>(
|
fn is_cyclically_defined_recursive<'db>(
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
class: Class<'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 {
|
) -> bool {
|
||||||
if !classes_to_watch.insert(class) {
|
let mut result = false;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for explicit_base_class in class.fully_static_explicit_bases(db) {
|
for explicit_base_class in class.fully_static_explicit_bases(db) {
|
||||||
// Each base must be considered in isolation.
|
if !classes_on_stack.insert(explicit_base_class) {
|
||||||
// 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) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
classes_to_watch.truncate(classes_to_watch_len);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
self.fully_static_explicit_bases(db)
|
if visited_classes.insert(explicit_base_class) {
|
||||||
.any(|base_class| is_cyclically_defined_recursive(db, base_class, &mut IndexSet::new()))
|
// 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();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -576,16 +576,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
// Iterate through all class definitions in this scope.
|
// Iterate through all class definitions in this scope.
|
||||||
for (class, class_node) in class_definitions {
|
for (class, class_node) in class_definitions {
|
||||||
// (1) Check that the class does not have a cyclic definition
|
// (1) Check that the class does not have a cyclic definition
|
||||||
if class.is_cyclically_defined(self.db()) {
|
if let Some(inheritance_cycle) = class.inheritance_cycle(self.db()) {
|
||||||
|
if inheritance_cycle.is_participant() {
|
||||||
self.context.report_lint(
|
self.context.report_lint(
|
||||||
&CYCLIC_CLASS_DEFINITION,
|
&CYCLIC_CLASS_DEFINITION,
|
||||||
class_node.into(),
|
class_node.into(),
|
||||||
format_args!(
|
format_args!(
|
||||||
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
|
"Cyclic definition of `{}` (class cannot inherit from itself)",
|
||||||
class.name(self.db()),
|
|
||||||
class.name(self.db())
|
class.name(self.db())
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
// Attempting to determine the MRO of a class or if the class has a metaclass conflict
|
// 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.
|
// is impossible if the class is cyclically defined; there's nothing more to do here.
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ impl<'db> Mro<'db> {
|
||||||
fn of_class_impl(db: &'db dyn Db, class: Class<'db>) -> Result<Self, MroErrorKind<'db>> {
|
fn of_class_impl(db: &'db dyn Db, class: Class<'db>) -> Result<Self, MroErrorKind<'db>> {
|
||||||
let class_bases = class.explicit_bases(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.
|
// 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,
|
// 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.
|
// or we'll end up in an infinite loop.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue