mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:25:17 +00:00
[red-knot] Add MRO resolution for classes (#14027)
This commit is contained in:
parent
88d9bb191b
commit
df45a0e3f9
13 changed files with 1175 additions and 124 deletions
|
@ -1,6 +1,9 @@
|
|||
use mro::{ClassBase, Mro, MroError, MroIterator};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::module_resolver::file_to_module;
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
||||
|
@ -26,6 +29,7 @@ mod builder;
|
|||
mod diagnostic;
|
||||
mod display;
|
||||
mod infer;
|
||||
mod mro;
|
||||
mod narrow;
|
||||
mod unpacker;
|
||||
|
||||
|
@ -1808,12 +1812,11 @@ pub struct ClassType<'db> {
|
|||
known: Option<KnownClass>,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> ClassType<'db> {
|
||||
/// Return `true` if this class represents `known_class`
|
||||
pub fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
|
||||
match self.known(db) {
|
||||
Some(known) => known == known_class,
|
||||
None => false,
|
||||
}
|
||||
self.known(db) == Some(known_class)
|
||||
}
|
||||
|
||||
/// Return true if this class is a standard library type with given module name and name.
|
||||
|
@ -1824,71 +1827,123 @@ impl<'db> ClassType<'db> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Return an iterator over the types of this class's bases.
|
||||
/// Return an iterator over the inferred types of this class's *explicit* bases.
|
||||
///
|
||||
/// # Panics:
|
||||
/// If `definition` is not a `DefinitionKind::Class`.
|
||||
pub fn bases(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
|
||||
/// Note that any class (except for `object`) that has no explicit
|
||||
/// bases will implicitly inherit from `object` at runtime. Nonetheless,
|
||||
/// this method does *not* include `object` in the bases it iterates over.
|
||||
fn explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
|
||||
let definition = self.definition(db);
|
||||
let DefinitionKind::Class(class_stmt_node) = definition.kind(db) else {
|
||||
panic!("Class type definition must have DefinitionKind::Class");
|
||||
};
|
||||
class_stmt_node
|
||||
let class_stmt = self.node(db);
|
||||
let has_type_params = class_stmt.type_params.is_some();
|
||||
|
||||
class_stmt
|
||||
.bases()
|
||||
.iter()
|
||||
.map(move |base_expr: &ast::Expr| {
|
||||
if class_stmt_node.type_params.is_some() {
|
||||
// when we have a specialized scope, we'll look up the inference
|
||||
// within that scope
|
||||
let model: SemanticModel<'db> = SemanticModel::new(db, definition.file(db));
|
||||
base_expr.ty(&model)
|
||||
} else {
|
||||
// Otherwise, we can do the lookup based on the definition scope
|
||||
definition_expression_ty(db, definition, base_expr)
|
||||
}
|
||||
})
|
||||
.map(move |base_node| infer_class_base_type(db, base_node, definition, has_type_params))
|
||||
}
|
||||
|
||||
/// Return the original [`ast::StmtClassDef`] node associated with this class
|
||||
fn node(self, db: &'db dyn Db) -> &'db ast::StmtClassDef {
|
||||
match self.definition(db).kind(db) {
|
||||
DefinitionKind::Class(class_stmt_node) => class_stmt_node,
|
||||
_ => unreachable!("Class type definition should always have DefinitionKind::Class"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to resolve the [method resolution order] ("MRO") for this class.
|
||||
/// If the MRO is unresolvable, return an error indicating why the class's MRO
|
||||
/// cannot be accurately determined. The error returned contains a fallback MRO
|
||||
/// that will be used instead for the purposes of type inference.
|
||||
///
|
||||
/// The MRO is the tuple of classes that can be retrieved as the `__mro__`
|
||||
/// attribute on a class at runtime.
|
||||
///
|
||||
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn try_mro(self, db: &'db dyn Db) -> Result<Mro<'db>, MroError<'db>> {
|
||||
Mro::of_class(db, self)
|
||||
}
|
||||
|
||||
/// Iterate over the [method resolution order] ("MRO") of the class.
|
||||
///
|
||||
/// If the MRO could not be accurately resolved, this method falls back to iterating
|
||||
/// over an MRO that has the class directly inheriting from `Unknown`. Use
|
||||
/// [`ClassType::try_mro`] if you need to distinguish between the success and failure
|
||||
/// cases rather than simply iterating over the inferred resolution order for the class.
|
||||
///
|
||||
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
fn iter_mro(self, db: &'db dyn Db) -> impl Iterator<Item = ClassBase<'db>> {
|
||||
MroIterator::new(db, self)
|
||||
}
|
||||
|
||||
/// Return `true` if `other` is present in this class's MRO.
|
||||
pub fn is_subclass_of(self, db: &'db dyn Db, other: ClassType) -> bool {
|
||||
// TODO: we need to iterate over the *MRO* here, not the bases
|
||||
(other == self)
|
||||
|| self.bases(db).any(|base| match base {
|
||||
Type::ClassLiteral(base_class) => base_class == other,
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate, so we should not return `True` if we find `Any/Unknown` in the
|
||||
// bases.
|
||||
_ => false,
|
||||
})
|
||||
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
|
||||
// participate, so we should not return `True` if we find `Any/Unknown` in the MRO.
|
||||
self.iter_mro(db).contains(&ClassBase::Class(other))
|
||||
}
|
||||
|
||||
/// Returns the class member of this class named `name`.
|
||||
///
|
||||
/// The member resolves to a member of the class itself or any of its bases.
|
||||
/// The member resolves to a member on the class itself or any of its proper superclasses.
|
||||
pub(crate) fn class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
let member = self.own_class_member(db, name);
|
||||
if !member.is_unbound() {
|
||||
return member;
|
||||
if name == "__mro__" {
|
||||
let tuple_elements: Box<_> = self.iter_mro(db).map(Type::from).collect();
|
||||
return Symbol::Type(
|
||||
Type::Tuple(TupleType::new(db, tuple_elements)),
|
||||
Boundness::Bound,
|
||||
);
|
||||
}
|
||||
|
||||
self.inherited_class_member(db, name)
|
||||
}
|
||||
|
||||
/// Returns the inferred type of the class member named `name`.
|
||||
pub(crate) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
let scope = self.body_scope(db);
|
||||
symbol(db, scope, name)
|
||||
}
|
||||
|
||||
pub(crate) fn inherited_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
for base in self.bases(db) {
|
||||
let member = base.member(db, name);
|
||||
if !member.is_unbound() {
|
||||
return member;
|
||||
for superclass in self.iter_mro(db) {
|
||||
match superclass {
|
||||
// TODO we may instead want to record the fact that we encountered dynamic, and intersect it with
|
||||
// the type found on the next "real" class.
|
||||
ClassBase::Any | ClassBase::Unknown | ClassBase::Todo => {
|
||||
return Type::from(superclass).member(db, name)
|
||||
}
|
||||
ClassBase::Class(class) => {
|
||||
let member = class.own_class_member(db, name);
|
||||
if !member.is_unbound() {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Symbol::Unbound
|
||||
}
|
||||
|
||||
/// Returns the inferred type of the class member named `name`.
|
||||
///
|
||||
/// Returns [`Symbol::Unbound`] if `name` cannot be found in this class's scope
|
||||
/// directly. Use [`ClassType::class_member`] if you require a method that will
|
||||
/// traverse through the MRO until it finds the member.
|
||||
pub(crate) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
let scope = self.body_scope(db);
|
||||
symbol(db, scope, name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the type of a node representing an explicit class base.
|
||||
///
|
||||
/// For example, infer the type of `Foo` in the statement `class Bar(Foo, Baz): ...`.
|
||||
fn infer_class_base_type<'db>(
|
||||
db: &'db dyn Db,
|
||||
base_node: &'db ast::Expr,
|
||||
class_definition: Definition<'db>,
|
||||
class_has_type_params: bool,
|
||||
) -> Type<'db> {
|
||||
if class_has_type_params {
|
||||
// when we have a specialized scope, we'll look up the inference
|
||||
// within that scope
|
||||
let model = SemanticModel::new(db, class_definition.file(db));
|
||||
base_node.ty(&model)
|
||||
} else {
|
||||
// Otherwise, we can do the lookup based on the definition scope
|
||||
definition_expression_ty(db, class_definition, base_node)
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
|
@ -2131,6 +2186,10 @@ mod tests {
|
|||
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(42), Ty::StringLiteral("foo")]), Ty::Tuple(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::BuiltinInstance("int"), Ty::StringLiteral("foo")]), Ty::Tuple(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(42), Ty::BuiltinInstance("str")]), Ty::Tuple(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
|
||||
#[test_case(
|
||||
Ty::BuiltinInstance("FloatingPointError"),
|
||||
Ty::BuiltinInstance("Exception")
|
||||
)]
|
||||
fn is_subtype_of(from: Ty, to: Ty) {
|
||||
let db = setup_db();
|
||||
assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
|
||||
|
|
|
@ -62,6 +62,8 @@ use crate::unpack::Unpack;
|
|||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
|
||||
use super::mro::MroErrorKind;
|
||||
|
||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
||||
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||
/// scope.
|
||||
|
@ -431,19 +433,83 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
|
||||
if self.types.has_deferred {
|
||||
let mut deferred_expression_types: FxHashMap<ScopedExpressionId, Type<'db>> =
|
||||
FxHashMap::default();
|
||||
// invariant: only annotations and base classes are deferred, and both of these only
|
||||
// occur within a declaration (annotated assignment, function or class definition)
|
||||
for definition in self.types.declarations.keys() {
|
||||
if infer_definition_types(self.db, *definition).has_deferred {
|
||||
let deferred = infer_deferred_types(self.db, *definition);
|
||||
deferred_expression_types.extend(deferred.expressions.iter());
|
||||
self.types.expressions.extend(&deferred.expressions);
|
||||
self.diagnostics.extend(&deferred.diagnostics);
|
||||
}
|
||||
}
|
||||
self.types
|
||||
.expressions
|
||||
.extend(deferred_expression_types.iter());
|
||||
}
|
||||
|
||||
self.check_class_definitions();
|
||||
}
|
||||
|
||||
/// Iterate over all class definitions to check that Python will be able to create a
|
||||
/// consistent "[method resolution order]" for each class at runtime. If not, issue a diagnostic.
|
||||
///
|
||||
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
fn check_class_definitions(&mut self) {
|
||||
let class_definitions = self
|
||||
.types
|
||||
.declarations
|
||||
.values()
|
||||
.filter_map(|ty| ty.into_class_literal_type());
|
||||
|
||||
let invalid_mros = class_definitions.filter_map(|class| {
|
||||
class
|
||||
.try_mro(self.db)
|
||||
.as_ref()
|
||||
.err()
|
||||
.map(|mro_error| (class, mro_error))
|
||||
});
|
||||
|
||||
for (class, mro_error) in invalid_mros {
|
||||
match mro_error.reason() {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
let base_nodes = class.node(self.db).bases();
|
||||
for (index, duplicate) in duplicates {
|
||||
self.diagnostics.add(
|
||||
(&base_nodes[*index]).into(),
|
||||
"duplicate-base",
|
||||
format_args!("Duplicate base class `{}`", duplicate.name(self.db))
|
||||
);
|
||||
}
|
||||
}
|
||||
MroErrorKind::CyclicClassDefinition => self.diagnostics.add(
|
||||
class.node(self.db).into(),
|
||||
"cyclic-class-def",
|
||||
format_args!(
|
||||
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
|
||||
class.name(self.db),
|
||||
class.name(self.db)
|
||||
)
|
||||
),
|
||||
MroErrorKind::InvalidBases(bases) => {
|
||||
let base_nodes = class.node(self.db).bases();
|
||||
for (index, base_ty) in bases {
|
||||
self.diagnostics.add(
|
||||
(&base_nodes[*index]).into(),
|
||||
"invalid-base",
|
||||
format_args!(
|
||||
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
|
||||
base_ty.display(self.db)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
MroErrorKind::UnresolvableMro{bases_list} => self.diagnostics.add(
|
||||
class.node(self.db).into(),
|
||||
"inconsistent-mro",
|
||||
format_args!(
|
||||
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
|
||||
class.name(self.db),
|
||||
bases_list.iter().map(|base| base.display(self.db)).join(", ")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4154,9 +4220,7 @@ mod tests {
|
|||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::FileScopeId;
|
||||
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
|
||||
use crate::types::{
|
||||
check_types, global_symbol, infer_definition_types, symbol, TypeCheckDiagnostics,
|
||||
};
|
||||
use crate::types::check_types;
|
||||
use crate::{HasTy, ProgramSettings, SemanticModel};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
|
@ -4164,7 +4228,7 @@ mod tests {
|
|||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
use super::TypeInferenceBuilder;
|
||||
use super::*;
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
@ -4276,36 +4340,6 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_base_class_by_name() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/mod.py",
|
||||
"
|
||||
class Base:
|
||||
pass
|
||||
|
||||
class Sub(Base):
|
||||
pass
|
||||
",
|
||||
)?;
|
||||
|
||||
let mod_file = system_path_to_file(&db, "src/mod.py").expect("file to exist");
|
||||
let ty = global_symbol(&db, mod_file, "Sub").expect_type();
|
||||
|
||||
let class = ty.expect_class_literal();
|
||||
|
||||
let base_names: Vec<_> = class
|
||||
.bases(&db)
|
||||
.map(|base_ty| format!("{}", base_ty.display(&db)))
|
||||
.collect();
|
||||
|
||||
assert_eq!(base_names, vec!["Literal[Base]"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_method() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
@ -4534,13 +4568,13 @@ mod tests {
|
|||
let a = system_path_to_file(&db, "src/a.py").expect("file to exist");
|
||||
let c_ty = global_symbol(&db, a, "C").expect_type();
|
||||
let c_class = c_ty.expect_class_literal();
|
||||
let mut c_bases = c_class.bases(&db);
|
||||
let b_ty = c_bases.next().unwrap();
|
||||
let b_class = b_ty.expect_class_literal();
|
||||
let mut c_mro = c_class.iter_mro(&db);
|
||||
let b_ty = c_mro.nth(1).unwrap();
|
||||
let b_class = b_ty.expect_class();
|
||||
assert_eq!(b_class.name(&db), "B");
|
||||
let mut b_bases = b_class.bases(&db);
|
||||
let a_ty = b_bases.next().unwrap();
|
||||
let a_class = a_ty.expect_class_literal();
|
||||
let mut b_mro = b_class.iter_mro(&db);
|
||||
let a_ty = b_mro.nth(1).unwrap();
|
||||
let a_class = a_ty.expect_class();
|
||||
assert_eq!(a_class.name(&db), "A");
|
||||
|
||||
Ok(())
|
||||
|
@ -4689,15 +4723,8 @@ mod tests {
|
|||
db.write_file("/src/a.pyi", "class C(object): pass")?;
|
||||
let file = system_path_to_file(&db, "/src/a.pyi").unwrap();
|
||||
let ty = global_symbol(&db, file, "C").expect_type();
|
||||
|
||||
let base = ty
|
||||
.expect_class_literal()
|
||||
.bases(&db)
|
||||
.next()
|
||||
.expect("there should be at least one base");
|
||||
|
||||
assert_eq!(base.display(&db).to_string(), "Literal[object]");
|
||||
|
||||
let base = ty.expect_class_literal().iter_mro(&db).nth(1).unwrap();
|
||||
assert_eq!(base.display(&db).to_string(), "<class 'object'>");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
518
crates/red_knot_python_semantic/src/types/mro.rs
Normal file
518
crates/red_knot_python_semantic/src/types/mro.rs
Normal file
|
@ -0,0 +1,518 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::ops::Deref;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Either;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use super::{infer_class_base_type, ClassType, KnownClass, Type};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::Db;
|
||||
|
||||
/// The inferred method resolution order of a given class.
|
||||
///
|
||||
/// See [`ClassType::iter_mro`] for more details.
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>);
|
||||
|
||||
impl<'db> Mro<'db> {
|
||||
/// Attempt to resolve the MRO of a given class
|
||||
///
|
||||
/// In the event that a possible list of bases would (or could) lead to a
|
||||
/// `TypeError` being raised at runtime due to an unresolvable MRO, we infer
|
||||
/// the MRO of the class as being `[<the class in question>, Unknown, object]`.
|
||||
/// This seems most likely to reduce the possibility of cascading errors
|
||||
/// elsewhere.
|
||||
///
|
||||
/// (We emit a diagnostic warning about the runtime `TypeError` in
|
||||
/// [`super::infer::TypeInferenceBuilder::infer_region_scope`].)
|
||||
pub(super) fn of_class(db: &'db dyn Db, class: ClassType<'db>) -> Result<Self, MroError<'db>> {
|
||||
Self::of_class_impl(db, class).map_err(|error_kind| {
|
||||
let fallback_mro = Self::from([
|
||||
ClassBase::Class(class),
|
||||
ClassBase::Unknown,
|
||||
ClassBase::object(db),
|
||||
]);
|
||||
MroError {
|
||||
kind: error_kind,
|
||||
fallback_mro,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn of_class_impl(db: &'db dyn Db, class: ClassType<'db>) -> Result<Self, MroErrorKind<'db>> {
|
||||
let class_stmt_node = class.node(db);
|
||||
let class_bases = class_stmt_node.bases();
|
||||
|
||||
match class_bases {
|
||||
// `builtins.object` is the special case:
|
||||
// the only class in Python that has an MRO with length <2
|
||||
[] if class.is_known(db, KnownClass::Object) => {
|
||||
Ok(Self::from([ClassBase::Class(class)]))
|
||||
}
|
||||
|
||||
// All other classes in Python have an MRO with length >=2.
|
||||
// Even if a class has no explicit base classes,
|
||||
// it will implicitly inherit from `object` at runtime;
|
||||
// `object` will appear in the class's `__bases__` list and `__mro__`:
|
||||
//
|
||||
// ```pycon
|
||||
// >>> class Foo: ...
|
||||
// ...
|
||||
// >>> Foo.__bases__
|
||||
// (<class 'object'>,)
|
||||
// >>> Foo.__mro__
|
||||
// (<class '__main__.Foo'>, <class 'object'>)
|
||||
// ```
|
||||
[] => Ok(Self::from([ClassBase::Class(class), ClassBase::object(db)])),
|
||||
|
||||
// Fast path for a class that has only a single explicit base.
|
||||
//
|
||||
// This *could* theoretically be handled by the final branch below,
|
||||
// but it's a common case (i.e., worth optimizing for),
|
||||
// and the `c3_merge` function requires lots of allocations.
|
||||
[single_base_node] => {
|
||||
let single_base = ClassBase::try_from_node(
|
||||
db,
|
||||
single_base_node,
|
||||
class.definition(db),
|
||||
class_stmt_node.type_params.is_some(),
|
||||
);
|
||||
single_base.map_or_else(
|
||||
|invalid_base_ty| {
|
||||
let bases_info = Box::from([(0, invalid_base_ty)]);
|
||||
Err(MroErrorKind::InvalidBases(bases_info))
|
||||
},
|
||||
|single_base| {
|
||||
if let ClassBase::Class(class_base) = single_base {
|
||||
if class_is_cyclically_defined(db, class_base) {
|
||||
return Err(MroErrorKind::CyclicClassDefinition);
|
||||
}
|
||||
}
|
||||
let mro = std::iter::once(ClassBase::Class(class))
|
||||
.chain(single_base.mro(db))
|
||||
.collect();
|
||||
Ok(mro)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// The class has multiple explicit bases.
|
||||
//
|
||||
// We'll fallback to a full implementation of the C3-merge algorithm to determine
|
||||
// what MRO Python will give this class at runtime
|
||||
// (if an MRO is indeed resolvable at all!)
|
||||
multiple_bases => {
|
||||
if class_is_cyclically_defined(db, class) {
|
||||
return Err(MroErrorKind::CyclicClassDefinition);
|
||||
}
|
||||
|
||||
let definition = class.definition(db);
|
||||
let has_type_params = class_stmt_node.type_params.is_some();
|
||||
let mut valid_bases = vec![];
|
||||
let mut invalid_bases = vec![];
|
||||
|
||||
for (i, base_node) in multiple_bases.iter().enumerate() {
|
||||
match ClassBase::try_from_node(db, base_node, definition, has_type_params) {
|
||||
Ok(valid_base) => valid_bases.push(valid_base),
|
||||
Err(invalid_base) => invalid_bases.push((i, invalid_base)),
|
||||
}
|
||||
}
|
||||
|
||||
if !invalid_bases.is_empty() {
|
||||
return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()));
|
||||
}
|
||||
|
||||
let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])];
|
||||
for base in &valid_bases {
|
||||
seqs.push(base.mro(db).collect());
|
||||
}
|
||||
seqs.push(valid_bases.iter().copied().collect());
|
||||
|
||||
c3_merge(seqs).ok_or_else(|| {
|
||||
let mut seen_bases = FxHashSet::default();
|
||||
let mut duplicate_bases = vec![];
|
||||
for (index, base) in valid_bases
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, base)| Some((index, base.into_class_literal_type()?)))
|
||||
{
|
||||
if !seen_bases.insert(base) {
|
||||
duplicate_bases.push((index, base));
|
||||
}
|
||||
}
|
||||
|
||||
if duplicate_bases.is_empty() {
|
||||
MroErrorKind::UnresolvableMro {
|
||||
bases_list: valid_bases.into_boxed_slice(),
|
||||
}
|
||||
} else {
|
||||
MroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, const N: usize> From<[ClassBase<'db>; N]> for Mro<'db> {
|
||||
fn from(value: [ClassBase<'db>; N]) -> Self {
|
||||
Self(Box::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Vec<ClassBase<'db>>> for Mro<'db> {
|
||||
fn from(value: Vec<ClassBase<'db>>) -> Self {
|
||||
Self(value.into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Deref for Mro<'db> {
|
||||
type Target = [ClassBase<'db>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> FromIterator<ClassBase<'db>> for Mro<'db> {
|
||||
fn from_iter<T: IntoIterator<Item = ClassBase<'db>>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that yields elements of a class's MRO.
|
||||
///
|
||||
/// We avoid materialising the *full* MRO unless it is actually necessary:
|
||||
/// - Materialising the full MRO is expensive
|
||||
/// - We need to do it for every class in the code that we're checking, as we need to make sure
|
||||
/// that there are no class definitions in the code we're checking that would cause an
|
||||
/// exception to be raised at runtime. But the same does *not* necessarily apply for every class
|
||||
/// in third-party and stdlib dependencies: we never emit diagnostics about non-first-party code.
|
||||
/// - However, we *do* need to resolve attribute accesses on classes/instances from
|
||||
/// third-party and stdlib dependencies. That requires iterating over the MRO of third-party/stdlib
|
||||
/// classes, but not necessarily the *whole* MRO: often just the first element is enough.
|
||||
/// Luckily we know that for any class `X`, the first element of `X`'s MRO will always be `X` itself.
|
||||
/// We can therefore avoid resolving the full MRO for many third-party/stdlib classes while still
|
||||
/// being faithful to the runtime semantics.
|
||||
///
|
||||
/// Even for first-party code, where we will have to resolve the MRO for every class we encounter,
|
||||
/// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the
|
||||
/// Salsa-tracked [`ClassType::try_mro`] method unless it's absolutely necessary.
|
||||
pub(super) struct MroIterator<'db> {
|
||||
db: &'db dyn Db,
|
||||
|
||||
/// The class whose MRO we're iterating over
|
||||
class: ClassType<'db>,
|
||||
|
||||
/// Whether or not we've already yielded the first element of the MRO
|
||||
first_element_yielded: bool,
|
||||
|
||||
/// Iterator over all elements of the MRO except the first.
|
||||
///
|
||||
/// The full MRO is expensive to materialize, so this field is `None`
|
||||
/// unless we actually *need* to iterate past the first element of the MRO,
|
||||
/// at which point it is lazily materialized.
|
||||
subsequent_elements: Option<std::slice::Iter<'db, ClassBase<'db>>>,
|
||||
}
|
||||
|
||||
impl<'db> MroIterator<'db> {
|
||||
pub(super) fn new(db: &'db dyn Db, class: ClassType<'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
class,
|
||||
first_element_yielded: false,
|
||||
subsequent_elements: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize the full MRO of the class.
|
||||
/// Return an iterator over that MRO which skips the first element of the MRO.
|
||||
fn full_mro_except_first_element(&mut self) -> impl Iterator<Item = ClassBase<'db>> + '_ {
|
||||
self.subsequent_elements
|
||||
.get_or_insert_with(|| {
|
||||
let mut full_mro_iter = match self.class.try_mro(self.db) {
|
||||
Ok(mro) => mro.iter(),
|
||||
Err(error) => error.fallback_mro().iter(),
|
||||
};
|
||||
full_mro_iter.next();
|
||||
full_mro_iter
|
||||
})
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Iterator for MroIterator<'db> {
|
||||
type Item = ClassBase<'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.first_element_yielded {
|
||||
self.first_element_yielded = true;
|
||||
return Some(ClassBase::Class(self.class));
|
||||
}
|
||||
self.full_mro_except_first_element().next()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for MroIterator<'_> {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct MroError<'db> {
|
||||
kind: MroErrorKind<'db>,
|
||||
fallback_mro: Mro<'db>,
|
||||
}
|
||||
|
||||
impl<'db> MroError<'db> {
|
||||
/// Return an [`MroErrorKind`] variant describing why we could not resolve the MRO for this class.
|
||||
pub(super) fn reason(&self) -> &MroErrorKind<'db> {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
/// Return the fallback MRO we should infer for this class during type inference
|
||||
/// (since accurate resolution of its "true" MRO was impossible)
|
||||
pub(super) fn fallback_mro(&self) -> &Mro<'db> {
|
||||
&self.fallback_mro
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible ways in which attempting to resolve the MRO of a class might fail.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) enum MroErrorKind<'db> {
|
||||
/// The class inherits from one or more invalid bases.
|
||||
///
|
||||
/// To avoid excessive complexity in our implementation,
|
||||
/// we only permit classes to inherit from class-literal types,
|
||||
/// `Todo`, `Unknown` or `Any`. Anything else results in us
|
||||
/// emitting a diagnostic.
|
||||
///
|
||||
/// This variant records the indices and types of class bases
|
||||
/// that we deem to be invalid. The indices are the indices of nodes
|
||||
/// in the bases list of the class's [`ast::StmtClassDef`] node.
|
||||
/// Each index is the index of a node representing an invalid base.
|
||||
InvalidBases(Box<[(usize, Type<'db>)]>),
|
||||
|
||||
/// The class inherits from itself!
|
||||
///
|
||||
/// This is very unlikely to happen in working real-world code,
|
||||
/// but it's important to explicitly account for it.
|
||||
/// If we don't, there's a possibility of an infinite loop and a panic.
|
||||
CyclicClassDefinition,
|
||||
|
||||
/// The class has one or more duplicate bases.
|
||||
///
|
||||
/// This variant records the indices and [`ClassType`]s
|
||||
/// of the duplicate bases. The indices are the indices of nodes
|
||||
/// in the bases list of the class's [`ast::StmtClassDef`] node.
|
||||
/// Each index is the index of a node representing a duplicate base.
|
||||
DuplicateBases(Box<[(usize, ClassType<'db>)]>),
|
||||
|
||||
/// The MRO is otherwise unresolvable through the C3-merge algorithm.
|
||||
///
|
||||
/// See [`c3_merge`] for more details.
|
||||
UnresolvableMro { bases_list: Box<[ClassBase<'db>]> },
|
||||
}
|
||||
|
||||
/// Enumeration of the possible kinds of types we allow in class bases.
|
||||
///
|
||||
/// This is much more limited than the [`Type`] enum:
|
||||
/// all types that would be invalid to have as a class base are
|
||||
/// transformed into [`ClassBase::Unknown`]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(super) enum ClassBase<'db> {
|
||||
Any,
|
||||
Unknown,
|
||||
Todo,
|
||||
Class(ClassType<'db>),
|
||||
}
|
||||
|
||||
impl<'db> ClassBase<'db> {
|
||||
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
|
||||
struct Display<'db> {
|
||||
base: ClassBase<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Display<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.base {
|
||||
ClassBase::Any => f.write_str("Any"),
|
||||
ClassBase::Todo => f.write_str("Todo"),
|
||||
ClassBase::Unknown => f.write_str("Unknown"),
|
||||
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Display { base: self, db }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
pub(super) fn expect_class(self) -> ClassType<'db> {
|
||||
match self {
|
||||
ClassBase::Class(class) => class,
|
||||
_ => panic!("Expected a `ClassBase::Class()` variant"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a `ClassBase` representing the class `builtins.object`
|
||||
fn object(db: &'db dyn Db) -> Self {
|
||||
KnownClass::Object
|
||||
.to_class(db)
|
||||
.into_class_literal_type()
|
||||
.map_or(Self::Unknown, Self::Class)
|
||||
}
|
||||
|
||||
/// Attempt to resolve the node `base_node` into a `ClassBase`.
|
||||
///
|
||||
/// If the inferred type of `base_node` is not an acceptable class-base type,
|
||||
/// return an error indicating what the inferred type was.
|
||||
fn try_from_node(
|
||||
db: &'db dyn Db,
|
||||
base_node: &'db ast::Expr,
|
||||
class_definition: Definition<'db>,
|
||||
class_has_type_params: bool,
|
||||
) -> Result<Self, Type<'db>> {
|
||||
let base_ty = infer_class_base_type(db, base_node, class_definition, class_has_type_params);
|
||||
Self::try_from_ty(base_ty).ok_or(base_ty)
|
||||
}
|
||||
|
||||
/// Attempt to resolve `ty` into a `ClassBase`.
|
||||
///
|
||||
/// Return `None` if `ty` is not an acceptable type for a class base.
|
||||
fn try_from_ty(ty: Type<'db>) -> Option<Self> {
|
||||
match ty {
|
||||
Type::Any => Some(Self::Any),
|
||||
Type::Unknown => Some(Self::Unknown),
|
||||
Type::Todo => Some(Self::Todo),
|
||||
Type::ClassLiteral(class) => Some(Self::Class(class)),
|
||||
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
|
||||
Type::Intersection(_) => None, // TODO -- probably incorrect?
|
||||
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
|
||||
Type::Never
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::Tuple(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::ModuleLiteral(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_class_literal_type(self) -> Option<ClassType<'db>> {
|
||||
match self {
|
||||
Self::Class(class) => Some(class),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the MRO of this base
|
||||
fn mro(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> Either<impl Iterator<Item = ClassBase<'db>>, impl Iterator<Item = ClassBase<'db>>> {
|
||||
match self {
|
||||
ClassBase::Any => Either::Left([ClassBase::Any, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Unknown => {
|
||||
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Todo => Either::Left([ClassBase::Todo, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: ClassBase<'db>) -> Self {
|
||||
match value {
|
||||
ClassBase::Any => Type::Any,
|
||||
ClassBase::Todo => Type::Todo,
|
||||
ClassBase::Unknown => Type::Unknown,
|
||||
ClassBase::Class(class) => Type::ClassLiteral(class),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of the [C3-merge algorithm] for calculating a Python class's
|
||||
/// [method resolution order].
|
||||
///
|
||||
/// [C3-merge algorithm]: https://docs.python.org/3/howto/mro.html#python-2-3-mro
|
||||
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
fn c3_merge(mut sequences: Vec<VecDeque<ClassBase>>) -> Option<Mro> {
|
||||
// Most MROs aren't that long...
|
||||
let mut mro = Vec::with_capacity(8);
|
||||
|
||||
loop {
|
||||
sequences.retain(|sequence| !sequence.is_empty());
|
||||
|
||||
if sequences.is_empty() {
|
||||
return Some(Mro::from(mro));
|
||||
}
|
||||
|
||||
// If the candidate exists "deeper down" in the inheritance hierarchy,
|
||||
// we should refrain from adding it to the MRO for now. Add the first candidate
|
||||
// for which this does not hold true. If this holds true for all candidates,
|
||||
// return `None`; it will be impossible to find a consistent MRO for the class
|
||||
// with the given bases.
|
||||
let mro_entry = sequences.iter().find_map(|outer_sequence| {
|
||||
let candidate = outer_sequence[0];
|
||||
|
||||
let not_head = sequences
|
||||
.iter()
|
||||
.all(|sequence| sequence.iter().skip(1).all(|base| base != &candidate));
|
||||
|
||||
not_head.then_some(candidate)
|
||||
})?;
|
||||
|
||||
mro.push(mro_entry);
|
||||
|
||||
// Make sure we don't try to add the candidate to the MRO twice:
|
||||
for sequence in &mut sequences {
|
||||
if sequence[0] == mro_entry {
|
||||
sequence.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class appears to be a cyclic definition,
|
||||
/// i.e., it inherits either directly or indirectly from itself.
|
||||
///
|
||||
/// A class definition like this will fail at runtime,
|
||||
/// but we must be resilient to it or we could panic.
|
||||
fn class_is_cyclically_defined(db: &dyn Db, class: ClassType) -> bool {
|
||||
fn is_cyclically_defined_recursive<'db>(
|
||||
db: &'db dyn Db,
|
||||
class: ClassType<'db>,
|
||||
classes_to_watch: &mut IndexSet<ClassType<'db>>,
|
||||
) -> bool {
|
||||
if !classes_to_watch.insert(class) {
|
||||
return true;
|
||||
}
|
||||
for explicit_base_class in class
|
||||
.explicit_bases(db)
|
||||
.filter_map(Type::into_class_literal_type)
|
||||
{
|
||||
// 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) {
|
||||
return true;
|
||||
}
|
||||
classes_to_watch.truncate(classes_to_watch_len);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
class
|
||||
.explicit_bases(db)
|
||||
.filter_map(Type::into_class_literal_type)
|
||||
.any(|base_class| is_cyclically_defined_recursive(db, base_class, &mut IndexSet::default()))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue