Skip to content

Commit de947de

Browse files
authored
[red-knot] Consolidate detection of cyclically defined classes (#14207)
1 parent c0c4ae1 commit de947de

File tree

3 files changed

+145
-206
lines changed

3 files changed

+145
-206
lines changed

crates/red_knot_python_semantic/src/types.rs

Lines changed: 101 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,6 +2349,15 @@ impl<'db> Class<'db> {
23492349
self.explicit_bases_query(db)
23502350
}
23512351

2352+
/// Iterate over this class's explicit bases, filtering out any bases that are not class objects.
2353+
fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = Class<'db>> {
2354+
self.explicit_bases(db)
2355+
.iter()
2356+
.copied()
2357+
.filter_map(Type::into_class_literal)
2358+
.map(|ClassLiteralType { class }| class)
2359+
}
2360+
23522361
#[salsa::tracked(return_ref)]
23532362
fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
23542363
let class_stmt = self.node(db);
@@ -2448,102 +2457,80 @@ impl<'db> Class<'db> {
24482457
/// Return the metaclass of this class, or `Unknown` if the metaclass cannot be inferred.
24492458
pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
24502459
// TODO: `type[Unknown]` would be a more precise fallback
2451-
// (needs support for <https://docs.python.org/3/library/typing.html#the-type-of-class-objects>)
24522460
self.try_metaclass(db).unwrap_or(Type::Unknown)
24532461
}
24542462

24552463
/// Return the metaclass of this class, or an error if the metaclass cannot be inferred.
24562464
#[salsa::tracked]
24572465
pub(crate) fn try_metaclass(self, db: &'db dyn Db) -> Result<Type<'db>, MetaclassError<'db>> {
2458-
/// Infer the metaclass of a class, tracking the classes that have been visited to detect
2459-
/// cyclic definitions.
2460-
fn infer<'db>(
2461-
db: &'db dyn Db,
2462-
class: Class<'db>,
2463-
seen: &mut SeenSet<Class<'db>>,
2464-
) -> Result<Type<'db>, MetaclassError<'db>> {
2465-
// Recursively infer the metaclass of a class, ensuring that cyclic definitions are
2466-
// detected.
2467-
let mut safe_recurse = |class: Class<'db>| -> Result<Type<'db>, MetaclassError<'db>> {
2468-
// Each base must be considered in isolation.
2469-
let num_seen = seen.len();
2470-
if !seen.insert(class) {
2471-
return Err(MetaclassError {
2472-
kind: MetaclassErrorKind::CyclicDefinition,
2473-
});
2474-
}
2475-
let metaclass = infer(db, class, seen)?;
2476-
seen.truncate(num_seen);
2477-
Ok(metaclass)
2478-
};
2466+
// Identify the class's own metaclass (or take the first base class's metaclass).
2467+
let mut base_classes = self.fully_static_explicit_bases(db).peekable();
24792468

2480-
let mut base_classes = class
2481-
.explicit_bases(db)
2482-
.iter()
2483-
.copied()
2484-
.filter_map(Type::into_class_literal);
2469+
if base_classes.peek().is_some() && self.is_cyclically_defined(db) {
2470+
// We emit diagnostics for cyclic class definitions elsewhere.
2471+
// Avoid attempting to infer the metaclass if the class is cyclically defined:
2472+
// it would be easy to enter an infinite loop.
2473+
//
2474+
// TODO: `type[Unknown]` might be better here?
2475+
return Ok(Type::Unknown);
2476+
}
24852477

2486-
// Identify the class's own metaclass (or take the first base class's metaclass).
2487-
let explicit_metaclass = class.explicit_metaclass(db);
2488-
let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass
2489-
{
2490-
(metaclass, class)
2491-
} else if let Some(base_class) = base_classes.next() {
2492-
(safe_recurse(base_class.class)?, base_class.class)
2493-
} else {
2494-
(KnownClass::Type.to_class(db), class)
2495-
};
2478+
let explicit_metaclass = self.explicit_metaclass(db);
2479+
let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass {
2480+
(metaclass, self)
2481+
} else if let Some(base_class) = base_classes.next() {
2482+
(base_class.metaclass(db), base_class)
2483+
} else {
2484+
(KnownClass::Type.to_class(db), self)
2485+
};
24962486

2497-
let mut candidate = if let Type::ClassLiteral(metaclass_ty) = metaclass {
2498-
MetaclassCandidate {
2499-
metaclass: metaclass_ty.class,
2500-
explicit_metaclass_of: class_metaclass_was_from,
2501-
}
2502-
} else {
2503-
// TODO: If the metaclass is not a class, we should verify that it's a callable
2504-
// which accepts the same arguments as `type.__new__` (otherwise error), and return
2505-
// the meta-type of its return type. (And validate that is a class type?)
2506-
return Ok(Type::Todo);
2507-
};
2487+
let mut candidate = if let Type::ClassLiteral(metaclass_ty) = metaclass {
2488+
MetaclassCandidate {
2489+
metaclass: metaclass_ty.class,
2490+
explicit_metaclass_of: class_metaclass_was_from,
2491+
}
2492+
} else {
2493+
// TODO: If the metaclass is not a class, we should verify that it's a callable
2494+
// which accepts the same arguments as `type.__new__` (otherwise error), and return
2495+
// the meta-type of its return type. (And validate that is a class type?)
2496+
return Ok(Type::Todo);
2497+
};
25082498

2509-
// Reconcile all base classes' metaclasses with the candidate metaclass.
2510-
//
2511-
// See:
2512-
// - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass
2513-
// - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663
2514-
for base_class in base_classes {
2515-
let metaclass = safe_recurse(base_class.class)?;
2516-
let Type::ClassLiteral(metaclass) = metaclass else {
2517-
continue;
2499+
// Reconcile all base classes' metaclasses with the candidate metaclass.
2500+
//
2501+
// See:
2502+
// - https://docs.python.org/3/reference/datamodel.html#determining-the-appropriate-metaclass
2503+
// - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663
2504+
for base_class in base_classes {
2505+
let metaclass = base_class.metaclass(db);
2506+
let Type::ClassLiteral(metaclass) = metaclass else {
2507+
continue;
2508+
};
2509+
if metaclass.class.is_subclass_of(db, candidate.metaclass) {
2510+
candidate = MetaclassCandidate {
2511+
metaclass: metaclass.class,
2512+
explicit_metaclass_of: base_class,
25182513
};
2519-
if metaclass.class.is_subclass_of(db, candidate.metaclass) {
2520-
candidate = MetaclassCandidate {
2514+
continue;
2515+
}
2516+
if candidate.metaclass.is_subclass_of(db, metaclass.class) {
2517+
continue;
2518+
}
2519+
return Err(MetaclassError {
2520+
kind: MetaclassErrorKind::Conflict {
2521+
candidate1: candidate,
2522+
candidate2: MetaclassCandidate {
25212523
metaclass: metaclass.class,
2522-
explicit_metaclass_of: base_class.class,
2523-
};
2524-
continue;
2525-
}
2526-
if candidate.metaclass.is_subclass_of(db, metaclass.class) {
2527-
continue;
2528-
}
2529-
return Err(MetaclassError {
2530-
kind: MetaclassErrorKind::Conflict {
2531-
candidate1: candidate,
2532-
candidate2: MetaclassCandidate {
2533-
metaclass: metaclass.class,
2534-
explicit_metaclass_of: base_class.class,
2535-
},
2536-
candidate1_is_base_class: explicit_metaclass.is_none(),
2524+
explicit_metaclass_of: base_class,
25372525
},
2538-
});
2539-
}
2540-
2541-
Ok(Type::ClassLiteral(ClassLiteralType {
2542-
class: candidate.metaclass,
2543-
}))
2526+
candidate1_is_base_class: explicit_metaclass.is_none(),
2527+
},
2528+
});
25442529
}
25452530

2546-
infer(db, self, &mut SeenSet::new(self))
2531+
Ok(Type::ClassLiteral(ClassLiteralType {
2532+
class: candidate.metaclass,
2533+
}))
25472534
}
25482535

25492536
/// Returns the class member of this class named `name`.
@@ -2590,6 +2577,39 @@ impl<'db> Class<'db> {
25902577
let scope = self.body_scope(db);
25912578
symbol(db, scope, name)
25922579
}
2580+
2581+
/// Return `true` if this class appears to be a cyclic definition,
2582+
/// i.e., it inherits either directly or indirectly from itself.
2583+
///
2584+
/// A class definition like this will fail at runtime,
2585+
/// but we must be resilient to it or we could panic.
2586+
#[salsa::tracked]
2587+
fn is_cyclically_defined(self, db: &'db dyn Db) -> bool {
2588+
fn is_cyclically_defined_recursive<'db>(
2589+
db: &'db dyn Db,
2590+
class: Class<'db>,
2591+
classes_to_watch: &mut IndexSet<Class<'db>>,
2592+
) -> bool {
2593+
if !classes_to_watch.insert(class) {
2594+
return true;
2595+
}
2596+
for explicit_base_class in class.fully_static_explicit_bases(db) {
2597+
// Each base must be considered in isolation.
2598+
// This is due to the fact that if a class uses multiple inheritance,
2599+
// there could easily be a situation where two bases have the same class in their MROs;
2600+
// that isn't enough to constitute the class being cyclically defined.
2601+
let classes_to_watch_len = classes_to_watch.len();
2602+
if is_cyclically_defined_recursive(db, explicit_base_class, classes_to_watch) {
2603+
return true;
2604+
}
2605+
classes_to_watch.truncate(classes_to_watch_len);
2606+
}
2607+
false
2608+
}
2609+
2610+
self.fully_static_explicit_bases(db)
2611+
.any(|base_class| is_cyclically_defined_recursive(db, base_class, &mut IndexSet::new()))
2612+
}
25932613
}
25942614

25952615
/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.
@@ -2599,38 +2619,6 @@ pub(super) struct MetaclassCandidate<'db> {
25992619
explicit_metaclass_of: Class<'db>,
26002620
}
26012621

2602-
/// A utility struct for detecting duplicates in class hierarchies while storing the initial
2603-
/// entry on the stack.
2604-
#[derive(Debug, Clone, PartialEq, Eq)]
2605-
struct SeenSet<T: Hash + Eq> {
2606-
initial: T,
2607-
visited: IndexSet<T>,
2608-
}
2609-
2610-
impl<T: Hash + Eq> SeenSet<T> {
2611-
fn new(initial: T) -> SeenSet<T> {
2612-
Self {
2613-
initial,
2614-
visited: IndexSet::new(),
2615-
}
2616-
}
2617-
2618-
fn len(&self) -> usize {
2619-
self.visited.len()
2620-
}
2621-
2622-
fn truncate(&mut self, len: usize) {
2623-
self.visited.truncate(len);
2624-
}
2625-
2626-
fn insert(&mut self, value: T) -> bool {
2627-
if value == self.initial {
2628-
return false;
2629-
}
2630-
self.visited.insert(value)
2631-
}
2632-
}
2633-
26342622
/// A singleton type representing a single class object at runtime.
26352623
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
26362624
pub struct ClassLiteralType<'db> {
@@ -2714,13 +2702,6 @@ pub(super) enum MetaclassErrorKind<'db> {
27142702
/// inferred metaclass of a base class. This helps us give better error messages in diagnostics.
27152703
candidate1_is_base_class: bool,
27162704
},
2717-
2718-
/// The class inherits from itself!
2719-
///
2720-
/// This is very unlikely to happen in working real-world code,
2721-
/// but it's important to explicitly account for it.
2722-
/// If we don't, there's a possibility of an infinite loop and a panic.
2723-
CyclicDefinition,
27242705
}
27252706

27262707
#[salsa::interned]

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,13 @@ impl<'db> TypeInferenceBuilder<'db> {
457457
self.check_class_definitions();
458458
}
459459

460-
/// Iterate over all class definitions to check that Python will be able to create a
461-
/// consistent "[method resolution order]" and [metaclass] for each class at runtime. If not,
462-
/// issue a diagnostic.
460+
/// Iterate over all class definitions to check that the definition will not cause an exception
461+
/// to be raised at runtime. This needs to be done after most other types in the scope have been
462+
/// inferred, due to the fact that base classes can be deferred. If it looks like a class
463+
/// definition is invalid in some way, issue a diagnostic.
464+
///
465+
/// Among the things we check for in this method are whether Python will be able to determine a
466+
/// consistent "[method resolution order]" and [metaclass] for each class.
463467
///
464468
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
465469
/// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses
@@ -471,7 +475,25 @@ impl<'db> TypeInferenceBuilder<'db> {
471475
.filter_map(|ty| ty.into_class_literal())
472476
.map(|class_ty| class_ty.class);
473477

478+
// Iterate through all class definitions in this scope.
474479
for class in class_definitions {
480+
// (1) Check that the class does not have a cyclic definition
481+
if class.is_cyclically_defined(self.db) {
482+
self.diagnostics.add(
483+
class.node(self.db).into(),
484+
"cyclic-class-def",
485+
format_args!(
486+
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
487+
class.name(self.db),
488+
class.name(self.db)
489+
),
490+
);
491+
// Attempting to determine the MRO of a class or if the class has a metaclass conflict
492+
// is impossible if the class is cyclically defined; there's nothing more to do here.
493+
continue;
494+
}
495+
496+
// (2) Check that the class's MRO is resolvable
475497
if let Err(mro_error) = class.try_mro(self.db).as_ref() {
476498
match mro_error.reason() {
477499
MroErrorKind::DuplicateBases(duplicates) => {
@@ -484,15 +506,6 @@ impl<'db> TypeInferenceBuilder<'db> {
484506
);
485507
}
486508
}
487-
MroErrorKind::CyclicClassDefinition => self.diagnostics.add(
488-
class.node(self.db).into(),
489-
"cyclic-class-def",
490-
format_args!(
491-
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
492-
class.name(self.db),
493-
class.name(self.db)
494-
),
495-
),
496509
MroErrorKind::InvalidBases(bases) => {
497510
let base_nodes = class.node(self.db).bases();
498511
for (index, base_ty) in bases {
@@ -518,6 +531,7 @@ impl<'db> TypeInferenceBuilder<'db> {
518531
}
519532
}
520533

534+
// (3) Check that the class's metaclass can be determined without error.
521535
if let Err(metaclass_error) = class.try_metaclass(self.db) {
522536
match metaclass_error.reason() {
523537
MetaclassErrorKind::Conflict {
@@ -565,10 +579,6 @@ impl<'db> TypeInferenceBuilder<'db> {
565579
);
566580
}
567581
}
568-
MetaclassErrorKind::CyclicDefinition => {
569-
// Cyclic class definition diagnostic will already have been emitted above
570-
// in MRO calculation.
571-
}
572582
}
573583
}
574584
}

0 commit comments

Comments
 (0)