[red-knot] Add MRO resolution for classes (#14027)

This commit is contained in:
Alex Waygood 2024-11-04 13:31:38 +00:00 committed by GitHub
parent 88d9bb191b
commit df45a0e3f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1175 additions and 124 deletions

7
Cargo.lock generated
View file

@ -1162,12 +1162,12 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.5", "hashbrown 0.15.0",
"serde", "serde",
] ]
@ -2112,6 +2112,7 @@ dependencies = [
"countme", "countme",
"dir-test", "dir-test",
"hashbrown 0.15.0", "hashbrown 0.15.0",
"indexmap",
"insta", "insta",
"itertools 0.13.0", "itertools 0.13.0",
"memchr", "memchr",

View file

@ -81,6 +81,7 @@ hashbrown = { version = "0.15.0", default-features = false, features = [
ignore = { version = "0.4.22" } ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" } imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" } imperative = { version = "1.0.4" }
indexmap = {version = "2.6.0" }
indicatif = { version = "0.17.8" } indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" } indoc = { version = "2.0.4" }
insta = { version = "1.35.1" } insta = { version = "1.35.1" }

View file

@ -24,7 +24,8 @@ bitflags = { workspace = true }
camino = { workspace = true } camino = { workspace = true }
compact_str = { workspace = true } compact_str = { workspace = true }
countme = { workspace = true } countme = { workspace = true }
itertools = { workspace = true} indexmap = { workspace = true }
itertools = { workspace = true }
ordermap = { workspace = true } ordermap = { workspace = true }
salsa = { workspace = true } salsa = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
@ -43,10 +44,9 @@ red_knot_test = { workspace = true }
red_knot_vendored = { workspace = true } red_knot_vendored = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
dir-test = {workspace = true} dir-test = { workspace = true }
insta = { workspace = true } insta = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
[lints] [lints]
workspace = true workspace = true

View file

@ -85,7 +85,7 @@ f = Foo()
# that `Foo.__iadd__` may be unbound as additional context. # that `Foo.__iadd__` may be unbound as additional context.
f += "Hello, world!" f += "Hello, world!"
reveal_type(f) # revealed: int | @Todo reveal_type(f) # revealed: int | Unknown
``` ```
## Partially bound with `__add__` ## Partially bound with `__add__`

View file

@ -18,3 +18,38 @@ else:
reveal_type(C.x) # revealed: Literal[1, 2] reveal_type(C.x) # revealed: Literal[1, 2]
``` ```
## Inherited attributes
```py
class A:
X = "foo"
class B(A): ...
class C(B): ...
reveal_type(C.X) # revealed: Literal["foo"]
```
## Inherited attributes (multiple inheritance)
```py
class O: ...
class F(O):
X = 56
class E(O):
X = 42
class D(O): ...
class C(D, F): ...
class B(E, D): ...
class A(B, C): ...
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(A.__mro__)
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
reveal_type(A.X) # revealed: Literal[42]
```

View file

@ -202,11 +202,7 @@ reveal_type(A() + B()) # revealed: MyString
# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__` # N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__`
class C(B): ... class C(B): ...
# TODO: we currently only understand direct subclasses as subtypes of the superclass. reveal_type(A() + C()) # revealed: MyString
# We need to iterate through the full MRO rather than just the class's bases;
# if we do, we'll understand `C` as a subtype of `A`, and correctly understand this as being
# `MyString` rather than `str`
reveal_type(A() + C()) # revealed: str
``` ```
## Reflected precedence 2 ## Reflected precedence 2

View file

@ -0,0 +1,409 @@
# Method Resolution Order tests
Tests that assert that we can infer the correct type for a class's `__mro__` attribute.
This attribute is rarely accessed directly at runtime. However, it's extremely important for *us* to
know the precise possible values of a class's Method Resolution Order, or we won't be able to infer
the correct type of attributes accessed from instances.
For documentation on method resolution orders, see:
- <https://docs.python.org/3/glossary.html#term-method-resolution-order>
- <https://docs.python.org/3/howto/mro.html#python-2-3-mro>
## No bases
```py
class C: ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
```
## The special case: `object` itself
```py
reveal_type(object.__mro__) # revealed: tuple[Literal[object]]
```
## Explicit inheritance from `object`
```py
class C(object): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
```
## Explicit inheritance from non-`object` single base
```py
class A: ...
class B(A): ...
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
```
## Linearization of multiple bases
```py
class A: ...
class B: ...
class C(A, B): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
```
## Complex diamond inheritance (1)
This is "ex_2" from <https://docs.python.org/3/howto/mro.html#the-end>
```py
class O: ...
class X(O): ...
class Y(O): ...
class A(X, Y): ...
class B(Y, X): ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
```
## Complex diamond inheritance (2)
This is "ex_5" from <https://docs.python.org/3/howto/mro.html#the-end>
```py
class O: ...
class F(O): ...
class E(O): ...
class D(O): ...
class C(D, F): ...
class B(D, E): ...
class A(B, C): ...
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(C.__mro__)
# revealed: tuple[Literal[B], Literal[D], Literal[E], Literal[O], Literal[object]]
reveal_type(B.__mro__)
# revealed: tuple[Literal[A], Literal[B], Literal[C], Literal[D], Literal[E], Literal[F], Literal[O], Literal[object]]
reveal_type(A.__mro__)
```
## Complex diamond inheritance (3)
This is "ex_6" from <https://docs.python.org/3/howto/mro.html#the-end>
```py
class O: ...
class F(O): ...
class E(O): ...
class D(O): ...
class C(D, F): ...
class B(E, D): ...
class A(B, C): ...
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(C.__mro__)
# revealed: tuple[Literal[B], Literal[E], Literal[D], Literal[O], Literal[object]]
reveal_type(B.__mro__)
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
reveal_type(A.__mro__)
```
## Complex diamond inheritance (4)
This is "ex_9" from <https://docs.python.org/3/howto/mro.html#the-end>
```py
class O: ...
class A(O): ...
class B(O): ...
class C(O): ...
class D(O): ...
class E(O): ...
class K1(A, B, C): ...
class K2(D, B, E): ...
class K3(D, A): ...
class Z(K1, K2, K3): ...
# revealed: tuple[Literal[K1], Literal[A], Literal[B], Literal[C], Literal[O], Literal[object]]
reveal_type(K1.__mro__)
# revealed: tuple[Literal[K2], Literal[D], Literal[B], Literal[E], Literal[O], Literal[object]]
reveal_type(K2.__mro__)
# revealed: tuple[Literal[K3], Literal[D], Literal[A], Literal[O], Literal[object]]
reveal_type(K3.__mro__)
# revealed: tuple[Literal[Z], Literal[K1], Literal[K2], Literal[K3], Literal[D], Literal[A], Literal[B], Literal[C], Literal[E], Literal[O], Literal[object]]
reveal_type(Z.__mro__)
```
## Inheritance from `Unknown`
```py
from does_not_exist import DoesNotExist # error: [unresolved-import]
class A(DoesNotExist): ...
class B: ...
class C: ...
class D(A, B, C): ...
class E(B, C): ...
class F(E, A): ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[A], Unknown, Literal[B], Literal[C], Literal[object]]
reveal_type(E.__mro__) # revealed: tuple[Literal[E], Literal[B], Literal[C], Literal[object]]
reveal_type(F.__mro__) # revealed: tuple[Literal[F], Literal[E], Literal[B], Literal[C], Literal[A], Unknown, Literal[object]]
```
## `__bases__` lists that cause errors at runtime
If the class's `__bases__` cause an exception to be raised at runtime and therefore the class
creation to fail, we infer the class's `__mro__` as being `[<class>, Unknown, object]`:
```py
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[<class 'object'>, <class 'int'>]`"
class Foo(object, int): ...
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Bar(Foo): ...
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Unknown, Literal[object]]
# This is the `TypeError` at the bottom of "ex_2"
# in the examples at <https://docs.python.org/3/howto/mro.html#the-end>
class O: ...
class X(O): ...
class Y(O): ...
class A(X, Y): ...
class B(Y, X): ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[<class 'A'>, <class 'B'>]`"
class Z(A, B): ...
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
class AA(Z): ...
reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Literal[object]]
```
## `__bases__` includes a `Union`
We don't support union types in a class's bases; a base must resolve to a single `ClassLiteralType`.
If we find a union type in a class's bases, we infer the class's `__mro__` as being
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
```py
def returns_bool() -> bool:
return True
class A: ...
class B: ...
if returns_bool():
x = A
else:
x = B
reveal_type(x) # revealed: Literal[A, B]
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Foo(x): ...
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```
## `__bases__` includes multiple `Union`s
```py
def returns_bool() -> bool:
return True
class A: ...
class B: ...
class C: ...
class D: ...
if returns_bool():
x = A
else:
x = B
if returns_bool():
y = C
else:
y = D
reveal_type(x) # revealed: Literal[A, B]
reveal_type(y) # revealed: Literal[C, D]
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
# error: 14 [invalid-base] "Invalid class base with type `Literal[C, D]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Foo(x, y): ...
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```
## `__bases__` lists that cause errors... now with `Union`s
```py
def returns_bool() -> bool:
return True
class O: ...
class X(O): ...
class Y(O): ...
if bool():
foo = Y
else:
foo = object
# error: 21 [invalid-base] "Invalid class base with type `Literal[Y, object]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class PossibleError(foo, X): ...
reveal_type(PossibleError.__mro__) # revealed: tuple[Literal[PossibleError], Unknown, Literal[object]]
class A(X, Y): ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
if returns_bool():
class B(X, Y): ...
else:
class B(Y, X): ...
# revealed: tuple[Literal[B], Literal[X], Literal[Y], Literal[O], Literal[object]] | tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
reveal_type(B.__mro__)
# error: 12 [invalid-base] "Invalid class base with type `Literal[B, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
class Z(A, B): ...
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
```
## `__bases__` lists with duplicate bases
```py
class Foo(str, str): ... # error: 16 [duplicate-base] "Duplicate base class `str`"
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Spam: ...
class Eggs: ...
class Ham(
Spam,
Eggs,
Spam, # error: [duplicate-base] "Duplicate base class `Spam`"
Eggs, # error: [duplicate-base] "Duplicate base class `Eggs`"
): ...
reveal_type(Ham.__mro__) # revealed: tuple[Literal[Ham], Unknown, Literal[object]]
class Mushrooms: ...
class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
reveal_type(Omelette.__mro__) # revealed: tuple[Literal[Omelette], Unknown, Literal[object]]
```
## `__bases__` lists with duplicate `Unknown` bases
```py
# error: [unresolved-import]
# error: [unresolved-import]
from does_not_exist import unknown_object_1, unknown_object_2
reveal_type(unknown_object_1) # revealed: Unknown
reveal_type(unknown_object_2) # revealed: Unknown
# We *should* emit an error here to warn the user that we have no idea
# what the MRO of this class should really be.
# However, we don't complain about "duplicate base classes" here,
# even though two classes are both inferred as being `Unknown`.
#
# (TODO: should we revisit this? Does it violate the gradual guarantee?
# Should we just silently infer `[Foo, Unknown, object]` as the MRO here
# without emitting any error at all? Not sure...)
#
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[Unknown, Unknown]`"
class Foo(unknown_object_1, unknown_object_2): ...
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```
## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes
```py
from does_not_exist import unknown_object # error: [unresolved-import]
reveal_type(unknown_object) # revealed: Unknown
reveal_type(unknown_object.__mro__) # revealed: Unknown
```
## Classes that inherit from themselves
These are invalid, but we need to be able to handle them gracefully without panicking.
```py path=a.pyi
class Foo(Foo): ... # error: [cyclic-class-def]
reveal_type(Foo) # revealed: Literal[Foo]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Bar: ...
class Baz: ...
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def]
reveal_type(Boz) # revealed: Literal[Boz]
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
```
## Classes with indirect cycles in their MROs
These are similarly unlikely, but we still shouldn't crash:
```py path=a.pyi
class Foo(Bar): ... # error: [cyclic-class-def]
class Bar(Baz): ... # error: [cyclic-class-def]
class Baz(Foo): ... # error: [cyclic-class-def]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
```
## Classes with cycles in their MROs, and multiple inheritance
```py path=a.pyi
class Spam: ...
class Foo(Bar): ... # error: [cyclic-class-def]
class Bar(Baz): ... # error: [cyclic-class-def]
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
```
## Classes with cycles in their MRO, and a sub-graph
```py path=a.pyi
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
class Bar(Foo): ...
# TODO: can we avoid emitting the errors for these?
# The classes have cyclic superclasses,
# but are not themselves cyclic...
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
class Spam(Baz): ... # error: [cyclic-class-def]
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
reveal_type(Spam.__mro__) # revealed: tuple[Literal[Spam], Unknown, Literal[object]]
```

View file

@ -58,11 +58,10 @@ reveal_type(typing.__name__) # revealed: str
reveal_type(typing.__init__) # revealed: Literal[__init__] reveal_type(typing.__init__) # revealed: Literal[__init__]
# These come from `builtins.object`, not `types.ModuleType`: # These come from `builtins.object`, not `types.ModuleType`:
# TODO: we don't currently understand `types.ModuleType` as inheriting from `object`; reveal_type(typing.__eq__) # revealed: Literal[__eq__]
# these should not reveal `Unknown`:
reveal_type(typing.__eq__) # revealed: Unknown # TODO: understand properties
reveal_type(typing.__class__) # revealed: Unknown reveal_type(typing.__class__) # revealed: Literal[__class__]
reveal_type(typing.__module__) # revealed: Unknown
# TODO: needs support for attribute access on instances, properties and generics; # TODO: needs support for attribute access on instances, properties and generics;
# should be `dict[str, Any]` # should be `dict[str, Any]`

View file

@ -6,7 +6,12 @@ In type stubs, classes can reference themselves in their base class definitions.
`typeshed`, we have `class str(Sequence[str]): ...`. `typeshed`, we have `class str(Sequence[str]): ...`.
```py path=a.pyi ```py path=a.pyi
class C(C): ... class Foo[T]: ...
reveal_type(C) # revealed: Literal[C] # TODO: actually is subscriptable
# error: [non-subscriptable]
class Bar(Foo[Bar]): ...
reveal_type(Bar) # revealed: Literal[Bar]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
``` ```

View file

@ -1,6 +1,9 @@
use mro::{ClassBase, Mro, MroError, MroIterator};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_python_ast as ast; use ruff_python_ast as ast;
use itertools::Itertools;
use crate::module_resolver::file_to_module; use crate::module_resolver::file_to_module;
use crate::semantic_index::ast_ids::HasScopedAstId; use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::definition::{Definition, DefinitionKind}; use crate::semantic_index::definition::{Definition, DefinitionKind};
@ -26,6 +29,7 @@ mod builder;
mod diagnostic; mod diagnostic;
mod display; mod display;
mod infer; mod infer;
mod mro;
mod narrow; mod narrow;
mod unpacker; mod unpacker;
@ -1808,12 +1812,11 @@ pub struct ClassType<'db> {
known: Option<KnownClass>, known: Option<KnownClass>,
} }
#[salsa::tracked]
impl<'db> ClassType<'db> { 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 { pub fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
match self.known(db) { self.known(db) == Some(known_class)
Some(known) => known == known_class,
None => false,
}
} }
/// Return true if this class is a standard library type with given module name and name. /// 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: /// Note that any class (except for `object`) that has no explicit
/// If `definition` is not a `DefinitionKind::Class`. /// bases will implicitly inherit from `object` at runtime. Nonetheless,
pub fn bases(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> { /// 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 definition = self.definition(db);
let DefinitionKind::Class(class_stmt_node) = definition.kind(db) else { let class_stmt = self.node(db);
panic!("Class type definition must have DefinitionKind::Class"); let has_type_params = class_stmt.type_params.is_some();
};
class_stmt_node class_stmt
.bases() .bases()
.iter() .iter()
.map(move |base_expr: &ast::Expr| { .map(move |base_node| infer_class_base_type(db, base_node, definition, has_type_params))
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)
}
})
} }
/// 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 { 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 // `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 // participate, so we should not return `True` if we find `Any/Unknown` in the MRO.
// bases. self.iter_mro(db).contains(&ClassBase::Class(other))
_ => false,
})
} }
/// Returns the class member of this class named `name`. /// 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> { pub(crate) fn class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
let member = self.own_class_member(db, name); 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,
);
}
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() { if !member.is_unbound() {
return member; return member;
} }
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;
} }
} }
Symbol::Unbound 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] #[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::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::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::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) { fn is_subtype_of(from: Ty, to: Ty) {
let db = setup_db(); let db = setup_db();
assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db))); assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));

View file

@ -62,6 +62,8 @@ use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice}; use crate::util::subscript::{PyIndex, PySlice};
use crate::Db; use crate::Db;
use super::mro::MroErrorKind;
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// 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 /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
/// scope. /// scope.
@ -431,19 +433,83 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
if self.types.has_deferred { 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 // invariant: only annotations and base classes are deferred, and both of these only
// occur within a declaration (annotated assignment, function or class definition) // occur within a declaration (annotated assignment, function or class definition)
for definition in self.types.declarations.keys() { for definition in self.types.declarations.keys() {
if infer_definition_types(self.db, *definition).has_deferred { if infer_definition_types(self.db, *definition).has_deferred {
let deferred = infer_deferred_types(self.db, *definition); 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::definition::Definition;
use crate::semantic_index::symbol::FileScopeId; use crate::semantic_index::symbol::FileScopeId;
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map}; use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
use crate::types::{ use crate::types::check_types;
check_types, global_symbol, infer_definition_types, symbol, TypeCheckDiagnostics,
};
use crate::{HasTy, ProgramSettings, SemanticModel}; use crate::{HasTy, ProgramSettings, SemanticModel};
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
@ -4164,7 +4228,7 @@ mod tests {
use ruff_db::testing::assert_function_query_was_not_run; use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use super::TypeInferenceBuilder; use super::*;
fn setup_db() -> TestDb { fn setup_db() -> TestDb {
let db = TestDb::new(); let db = TestDb::new();
@ -4276,36 +4340,6 @@ mod tests {
Ok(()) 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] #[test]
fn resolve_method() -> anyhow::Result<()> { fn resolve_method() -> anyhow::Result<()> {
let mut db = setup_db(); 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 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_ty = global_symbol(&db, a, "C").expect_type();
let c_class = c_ty.expect_class_literal(); let c_class = c_ty.expect_class_literal();
let mut c_bases = c_class.bases(&db); let mut c_mro = c_class.iter_mro(&db);
let b_ty = c_bases.next().unwrap(); let b_ty = c_mro.nth(1).unwrap();
let b_class = b_ty.expect_class_literal(); let b_class = b_ty.expect_class();
assert_eq!(b_class.name(&db), "B"); assert_eq!(b_class.name(&db), "B");
let mut b_bases = b_class.bases(&db); let mut b_mro = b_class.iter_mro(&db);
let a_ty = b_bases.next().unwrap(); let a_ty = b_mro.nth(1).unwrap();
let a_class = a_ty.expect_class_literal(); let a_class = a_ty.expect_class();
assert_eq!(a_class.name(&db), "A"); assert_eq!(a_class.name(&db), "A");
Ok(()) Ok(())
@ -4689,15 +4723,8 @@ mod tests {
db.write_file("/src/a.pyi", "class C(object): pass")?; db.write_file("/src/a.pyi", "class C(object): pass")?;
let file = system_path_to_file(&db, "/src/a.pyi").unwrap(); let file = system_path_to_file(&db, "/src/a.pyi").unwrap();
let ty = global_symbol(&db, file, "C").expect_type(); let ty = global_symbol(&db, file, "C").expect_type();
let base = ty.expect_class_literal().iter_mro(&db).nth(1).unwrap();
let base = ty assert_eq!(base.display(&db).to_string(), "<class 'object'>");
.expect_class_literal()
.bases(&db)
.next()
.expect("there should be at least one base");
assert_eq!(base.display(&db).to_string(), "Literal[object]");
Ok(()) Ok(())
} }

View 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()))
}

View file

@ -27,6 +27,7 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
"/src/tomllib/_parser.py:7:29: Module `collections.abc` has no member `Iterable`", "/src/tomllib/_parser.py:7:29: Module `collections.abc` has no member `Iterable`",
// We don't support terminal statements in control flow yet: // We don't support terminal statements in control flow yet:
"/src/tomllib/_parser.py:246:15: Method `__class_getitem__` of type `Literal[frozenset]` is possibly unbound", "/src/tomllib/_parser.py:246:15: Method `__class_getitem__` of type `Literal[frozenset]` is possibly unbound",
"/src/tomllib/_parser.py:692:8354: Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
"/src/tomllib/_parser.py:66:18: Name `s` used when possibly not defined", "/src/tomllib/_parser.py:66:18: Name `s` used when possibly not defined",
"/src/tomllib/_parser.py:98:12: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:98:12: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:101:12: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:101:12: Name `char` used when possibly not defined",