[ty] Do not carry the generic context of Protocol or Generic in the ClassBase enum (#17989)
Some checks failed
CI / Determine changes (push) Has been cancelled
CI / cargo fmt (push) Has been cancelled
CI / cargo build (release) (push) Has been cancelled
CI / mkdocs (push) Has been cancelled
CI / python package (push) Has been cancelled
CI / pre-commit (push) Has been cancelled
[ty Playground] Release / publish (push) Has been cancelled
CI / cargo clippy (push) Has been cancelled
CI / cargo test (linux) (push) Has been cancelled
CI / cargo test (linux, release) (push) Has been cancelled
CI / cargo test (windows) (push) Has been cancelled
CI / cargo test (wasm) (push) Has been cancelled
CI / cargo build (msrv) (push) Has been cancelled
CI / cargo fuzz build (push) Has been cancelled
CI / fuzz parser (push) Has been cancelled
CI / test scripts (push) Has been cancelled
CI / ecosystem (push) Has been cancelled
CI / Fuzz for new ty panics (push) Has been cancelled
CI / cargo shear (push) Has been cancelled
CI / formatter instabilities and black similarity (push) Has been cancelled
CI / test ruff-lsp (push) Has been cancelled
CI / check playground (push) Has been cancelled
CI / benchmarks (push) Has been cancelled

## Summary

It doesn't seem to be necessary for our generics implementation to carry
the `GenericContext` in the `ClassBase` variants. Removing it simplifies
the code, fixes many TODOs about `Generic` or `Protocol` appearing
multiple times in MROs when each should only appear at most once, and
allows us to more accurately detect runtime errors that occur due to
`Generic` or `Protocol` appearing multiple times in a class's bases.

In order to remove the `GenericContext` from the `ClassBase` variant, it
turns out to be necessary to emulate
`typing._GenericAlias.__mro_entries__`, or we end up with a large number
of false-positive `inconsistent-mro` errors. This PR therefore also does
that.

Lastly, this PR fixes the inferred MROs of PEP-695 generic classes,
which implicitly inherit from `Generic` even if they have no explicit
bases.

## Test Plan

mdtests
This commit is contained in:
Alex Waygood 2025-05-22 21:37:03 -04:00 committed by GitHub
parent 6c0a59ea78
commit d02c9ada5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 271 additions and 215 deletions

View file

@ -81,23 +81,22 @@ import typing
class ListSubclass(typing.List): ...
# revealed: tuple[<class 'ListSubclass'>, <class 'list[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'ListSubclass'>, <class 'list[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# TODO: should not have multiple `Generic[]` elements
# revealed: tuple[<class 'DictSubclass'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
# revealed: tuple[<class 'DictSubclass'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# revealed: tuple[<class 'SetSubclass'>, <class 'set[Unknown]'>, <class 'MutableSet[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'SetSubclass'>, <class 'set[Unknown]'>, <class 'MutableSet[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'FrozenSetSubclass'>, <class 'frozenset[Unknown]'>, <class 'AbstractSet[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(FrozenSetSubclass.__mro__)
####################
@ -106,30 +105,26 @@ reveal_type(FrozenSetSubclass.__mro__)
class ChainMapSubclass(typing.ChainMap): ...
# TODO: should not have multiple `Generic[]` elements
# revealed: tuple[<class 'ChainMapSubclass'>, <class 'ChainMap[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
# revealed: tuple[<class 'ChainMapSubclass'>, <class 'ChainMap[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
# TODO: Should have one `Generic[]` element, not three(!)
# revealed: tuple[<class 'CounterSubclass'>, <class 'Counter[Unknown]'>, <class 'dict[Unknown, int]'>, <class 'MutableMapping[Unknown, int]'>, <class 'Mapping[Unknown, int]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], typing.Generic[_T], <class 'object'>]
# revealed: tuple[<class 'CounterSubclass'>, <class 'Counter[Unknown]'>, <class 'dict[Unknown, int]'>, <class 'MutableMapping[Unknown, int]'>, <class 'Mapping[Unknown, int]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(CounterSubclass.__mro__)
class DefaultDictSubclass(typing.DefaultDict): ...
# TODO: Should not have multiple `Generic[]` elements
# revealed: tuple[<class 'DefaultDictSubclass'>, <class 'defaultdict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
# revealed: tuple[<class 'DefaultDictSubclass'>, <class 'defaultdict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# revealed: tuple[<class 'DequeSubclass'>, <class 'deque[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'DequeSubclass'>, <class 'deque[Unknown]'>, <class 'MutableSequence[Unknown]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# TODO: Should not have multiple `Generic[]` elements
# revealed: tuple[<class 'OrderedDictSubclass'>, <class 'OrderedDict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], typing.Generic[_KT, _VT_co], <class 'object'>]
# revealed: tuple[<class 'OrderedDictSubclass'>, <class 'OrderedDict[Unknown, Unknown]'>, <class 'dict[Unknown, Unknown]'>, <class 'MutableMapping[Unknown, Unknown]'>, <class 'Mapping[Unknown, Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(OrderedDictSubclass.__mro__)
```

View file

@ -24,9 +24,7 @@ class:
```py
class Bad(Generic[T], Generic[T]): ... # error: [duplicate-base]
# TODO: should emit an error (fails at runtime)
class AlsoBad(Generic[T], Generic[S]): ...
class AlsoBad(Generic[T], Generic[S]): ... # error: [duplicate-base]
```
You cannot use the same typevar more than once.

View file

@ -527,6 +527,45 @@ reveal_type(unknown_object) # revealed: Unknown
reveal_type(unknown_object.__mro__) # revealed: Unknown
```
## MROs of classes that use multiple inheritance with generic aliases and subscripted `Generic`
```py
from typing import Generic, TypeVar, Iterator
T = TypeVar("T")
class peekable(Generic[T], Iterator[T]): ...
# revealed: tuple[<class 'peekable[Unknown]'>, <class 'Iterator[T]'>, <class 'Iterable[T]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(peekable.__mro__)
class peekable2(Iterator[T], Generic[T]): ...
# revealed: tuple[<class 'peekable2[Unknown]'>, <class 'Iterator[T]'>, <class 'Iterable[T]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(peekable2.__mro__)
class Base: ...
class Intermediate(Base, Generic[T]): ...
class Sub(Intermediate[T], Base): ...
# revealed: tuple[<class 'Sub[Unknown]'>, <class 'Intermediate[T]'>, <class 'Base'>, typing.Generic, <class 'object'>]
reveal_type(Sub.__mro__)
```
## Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases
<!-- snapshot-diagnostics -->
```py
from typing_extensions import Protocol, TypeVar, Generic
T = TypeVar("T")
class Foo(Protocol): ...
class Bar(Protocol[T]): ...
class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
```
## Classes that inherit from themselves
These are invalid, but we need to be able to handle them gracefully without panicking.

View file

@ -67,12 +67,10 @@ It's an error to include both bare `Protocol` and subscripted `Protocol[]` in th
simultaneously:
```py
# TODO: should emit a `[duplicate-bases]` error here:
class DuplicateBases(Protocol, Protocol[T]):
class DuplicateBases(Protocol, Protocol[T]): # error: [duplicate-base]
x: T
# TODO: should not have `Protocol` or `Generic` multiple times
# revealed: tuple[<class 'DuplicateBases[Unknown]'>, typing.Protocol, typing.Generic, typing.Protocol[T], typing.Generic[T], <class 'object'>]
# revealed: tuple[<class 'DuplicateBases[Unknown]'>, Unknown, <class 'object'>]
reveal_type(DuplicateBases.__mro__)
```

View file

@ -0,0 +1,37 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: mro.md - Method Resolution Order tests - Unresolvable MROs involving generics have the original bases reported in the error message, not the resolved bases
mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol, TypeVar, Generic
2 |
3 | T = TypeVar("T")
4 |
5 | class Foo(Protocol): ...
6 | class Bar(Protocol[T]): ...
7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
```
# Diagnostics
```
error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO) for class `Baz` with bases list `[typing.Protocol[T], <class 'Foo'>, <class 'Bar[T]'>]`
--> src/mdtest_snippet.py:7:1
|
5 | class Foo(Protocol): ...
6 | class Bar(Protocol[T]): ...
7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info: rule `inconsistent-mro` is enabled by default
```

View file

@ -16,7 +16,7 @@ class Foo[T]: ...
class Bar(Foo[Bar]): ...
reveal_type(Bar) # revealed: <class 'Bar'>
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, <class 'Foo[Bar]'>, <class 'object'>]
reveal_type(Bar.__mro__) # revealed: tuple[<class 'Bar'>, <class 'Foo[Bar]'>, typing.Generic, <class 'object'>]
```
## Access to attributes declared in stubs

View file

@ -83,7 +83,7 @@ python-version = "3.9"
```py
class A(tuple[int, str]): ...
# revealed: tuple[<class 'A'>, <class 'tuple[@Todo(Generic tuple specializations), ...]'>, <class 'Sequence[@Todo(Generic tuple specializations)]'>, <class 'Reversible[@Todo(Generic tuple specializations)]'>, <class 'Collection[@Todo(Generic tuple specializations)]'>, <class 'Iterable[@Todo(Generic tuple specializations)]'>, <class 'Container[@Todo(Generic tuple specializations)]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'A'>, <class 'tuple[@Todo(Generic tuple specializations), ...]'>, <class 'Sequence[@Todo(Generic tuple specializations)]'>, <class 'Reversible[@Todo(Generic tuple specializations)]'>, <class 'Collection[@Todo(Generic tuple specializations)]'>, <class 'Iterable[@Todo(Generic tuple specializations)]'>, <class 'Container[@Todo(Generic tuple specializations)]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(A.__mro__)
```
@ -114,6 +114,6 @@ from typing import Tuple
class C(Tuple): ...
# revealed: tuple[<class 'C'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol[_T_co], typing.Generic[_T_co], <class 'object'>]
# revealed: tuple[<class 'C'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(C.__mro__)
```

View file

@ -598,6 +598,10 @@ impl<'db> Type<'db> {
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
}
pub const fn is_generic_alias(&self) -> bool {
matches!(self, Type::GenericAlias(_))
}
/// Replace references to the class `class` with a self-reference marker. This is currently
/// used for recursive protocols, but could probably be extended to self-referential type-
/// aliases and similar.

View file

@ -223,6 +223,10 @@ impl<'db> ClassType<'db> {
}
}
pub(super) const fn is_generic(self) -> bool {
matches!(self, Self::Generic(_))
}
/// Returns the class literal and specialization for this class. For a non-generic class, this
/// is the class itself. For a generic alias, this is the alias's origin.
pub(crate) fn class_literal(
@ -352,7 +356,7 @@ impl<'db> ClassType<'db> {
ClassBase::Dynamic(_) => false,
// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol(_) | ClassBase::Generic(_) => false,
ClassBase::Protocol | ClassBase::Generic => false,
ClassBase::Class(base) => match (base, other) {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
@ -390,7 +394,7 @@ impl<'db> ClassType<'db> {
ClassBase::Dynamic(_) => false,
// Protocol and Generic are not represented by a ClassType.
ClassBase::Protocol(_) | ClassBase::Generic(_) => false,
ClassBase::Protocol | ClassBase::Generic => false,
ClassBase::Class(base) => match (base, other) {
(ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => base == other,
@ -602,11 +606,6 @@ impl<'db> ClassLiteral<'db> {
)
}
/// Return `true` if this class represents the builtin class `object`
pub(crate) fn is_object(self, db: &'db dyn Db) -> bool {
self.is_known(db, KnownClass::Object)
}
fn file(self, db: &dyn Db) -> File {
self.body_scope(db).file(db)
}
@ -1068,7 +1067,7 @@ impl<'db> ClassLiteral<'db> {
for superclass in mro_iter {
match superclass {
ClassBase::Generic(_) | ClassBase::Protocol(_) => {
ClassBase::Generic | ClassBase::Protocol => {
// Skip over these very special class bases that aren't really classes.
}
ClassBase::Dynamic(_) => {
@ -1427,7 +1426,7 @@ impl<'db> ClassLiteral<'db> {
for superclass in self.iter_mro(db, specialization) {
match superclass {
ClassBase::Generic(_) | ClassBase::Protocol(_) => {
ClassBase::Generic | ClassBase::Protocol => {
// Skip over these very special class bases that aren't really classes.
}
ClassBase::Dynamic(_) => {

View file

@ -1,5 +1,5 @@
use crate::Db;
use crate::types::generics::{GenericContext, Specialization};
use crate::types::generics::Specialization;
use crate::types::{
ClassType, DynamicType, KnownClass, KnownInstanceType, MroError, MroIterator, Type,
TypeMapping, todo_type,
@ -19,11 +19,11 @@ pub enum ClassBase<'db> {
Class(ClassType<'db>),
/// Although `Protocol` is not a class in typeshed's stubs, it is at runtime,
/// and can appear in the MRO of a class.
Protocol(Option<GenericContext<'db>>),
Protocol,
/// Bare `Generic` cannot be subclassed directly in user code,
/// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`,
/// `Protocol[T]`, or bare `Protocol`.
Generic(Option<GenericContext<'db>>),
Generic,
}
impl<'db> ClassBase<'db> {
@ -35,60 +35,18 @@ impl<'db> ClassBase<'db> {
match self {
Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()),
Self::Class(class) => Self::Class(class.normalized(db)),
Self::Protocol(generic_context) => {
Self::Protocol(generic_context.map(|context| context.normalized(db)))
}
Self::Generic(generic_context) => {
Self::Generic(generic_context.map(|context| context.normalized(db)))
}
Self::Protocol | Self::Generic => self,
}
}
pub(crate) 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::Dynamic(dynamic) => dynamic.fmt(f),
ClassBase::Class(class @ ClassType::NonGeneric(_)) => {
write!(f, "<class '{}'>", class.name(self.db))
}
ClassBase::Class(ClassType::Generic(alias)) => {
write!(f, "<class '{}'>", alias.display(self.db))
}
ClassBase::Protocol(generic_context) => {
f.write_str("typing.Protocol")?;
if let Some(generic_context) = generic_context {
generic_context.display(self.db).fmt(f)?;
}
Ok(())
}
ClassBase::Generic(generic_context) => {
f.write_str("typing.Generic")?;
if let Some(generic_context) = generic_context {
generic_context.display(self.db).fmt(f)?;
}
Ok(())
}
}
}
}
Display { base: self, db }
}
pub(crate) fn name(self, db: &'db dyn Db) -> &'db str {
match self {
ClassBase::Class(class) => class.name(db),
ClassBase::Dynamic(DynamicType::Any) => "Any",
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec) => "@Todo",
ClassBase::Protocol(_) => "Protocol",
ClassBase::Generic(_) => "Generic",
ClassBase::Protocol => "Protocol",
ClassBase::Generic => "Generic",
}
}
@ -255,12 +213,8 @@ impl<'db> ClassBase<'db> {
KnownInstanceType::Callable => {
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
}
KnownInstanceType::Protocol(generic_context) => {
Some(ClassBase::Protocol(generic_context))
}
KnownInstanceType::Generic(generic_context) => {
Some(ClassBase::Generic(generic_context))
}
KnownInstanceType::Protocol(_) => Some(ClassBase::Protocol),
KnownInstanceType::Generic(_) => Some(ClassBase::Generic),
},
}
}
@ -268,14 +222,14 @@ impl<'db> ClassBase<'db> {
pub(super) fn into_class(self) -> Option<ClassType<'db>> {
match self {
Self::Class(class) => Some(class),
Self::Dynamic(_) | Self::Generic(_) | Self::Protocol(_) => None,
Self::Dynamic(_) | Self::Generic | Self::Protocol => None,
}
}
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
match self {
Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)),
Self::Dynamic(_) | Self::Generic(_) | Self::Protocol(_) => self,
Self::Dynamic(_) | Self::Generic | Self::Protocol => self,
}
}
@ -299,7 +253,7 @@ impl<'db> ClassBase<'db> {
.try_mro(db, specialization)
.is_err_and(MroError::is_cycle)
}
ClassBase::Dynamic(_) | ClassBase::Generic(_) | ClassBase::Protocol(_) => false,
ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::Protocol => false,
}
}
@ -310,12 +264,8 @@ impl<'db> ClassBase<'db> {
additional_specialization: Option<Specialization<'db>>,
) -> impl Iterator<Item = ClassBase<'db>> {
match self {
ClassBase::Protocol(context) => {
ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(context))
}
ClassBase::Dynamic(_) | ClassBase::Generic(_) => {
ClassBaseMroIterator::length_2(db, self)
}
ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic),
ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self),
ClassBase::Class(class) => {
ClassBaseMroIterator::from_class(db, class, additional_specialization)
}
@ -338,12 +288,8 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
match value {
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
ClassBase::Class(class) => class.into(),
ClassBase::Protocol(generic_context) => {
Type::KnownInstance(KnownInstanceType::Protocol(generic_context))
}
ClassBase::Generic(generic_context) => {
Type::KnownInstance(KnownInstanceType::Generic(generic_context))
}
ClassBase::Protocol => Type::KnownInstance(KnownInstanceType::Protocol(None)),
ClassBase::Generic => Type::KnownInstance(KnownInstanceType::Generic(None)),
}
}
}

View file

@ -13,6 +13,7 @@ use crate::types::string_annotation::{
};
use crate::types::{KnownFunction, KnownInstanceType, Type, protocol_class::ProtocolClassLiteral};
use crate::{Program, PythonVersionWithSource, declare_lint};
use itertools::Itertools;
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic};
use ruff_db::files::system_path_to_file;
use ruff_python_ast::{self as ast, AnyNodeRef};
@ -1698,10 +1699,7 @@ pub(super) fn report_implicit_return_type(
let Some(class) = enclosing_class_of_method else {
return;
};
if class
.iter_mro(db, None)
.any(|base| matches!(base, ClassBase::Protocol(_)))
{
if class.iter_mro(db, None).contains(&ClassBase::Protocol) {
diagnostic.info(
"Only functions in stub files, methods on protocol classes, \
or methods with `@abstractmethod` are permitted to have empty bodies",

View file

@ -7529,7 +7529,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if !value_ty.into_class_literal().is_some_and(|class| {
class
.iter_mro(self.db(), None)
.any(|base| matches!(base, ClassBase::Generic(_)))
.contains(&ClassBase::Generic)
}) {
report_non_subscriptable(
&self.context,

View file

@ -7,7 +7,7 @@ use rustc_hash::FxBuildHasher;
use crate::Db;
use crate::types::class_base::ClassBase;
use crate::types::generics::Specialization;
use crate::types::{ClassLiteral, ClassType, Type};
use crate::types::{ClassLiteral, ClassType, KnownInstanceType, Type};
/// The inferred method resolution order of a given class.
///
@ -48,12 +48,12 @@ impl<'db> Mro<'db> {
/// [`super::infer::TypeInferenceBuilder::infer_region_scope`].)
pub(super) fn of_class(
db: &'db dyn Db,
class: ClassLiteral<'db>,
class_literal: ClassLiteral<'db>,
specialization: Option<Specialization<'db>>,
) -> Result<Self, MroError<'db>> {
Self::of_class_impl(db, class, specialization).map_err(|err| {
err.into_mro_error(db, class.apply_optional_specialization(db, specialization))
})
let class = class_literal.apply_optional_specialization(db, specialization);
Self::of_class_impl(db, class, class_literal.explicit_bases(db), specialization)
.map_err(|err| err.into_mro_error(db, class))
}
pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self {
@ -66,17 +66,16 @@ impl<'db> Mro<'db> {
fn of_class_impl(
db: &'db dyn Db,
class: ClassLiteral<'db>,
class: ClassType<'db>,
bases: &[Type<'db>],
specialization: Option<Specialization<'db>>,
) -> Result<Self, MroErrorKind<'db>> {
let class_type = class.apply_optional_specialization(db, specialization);
match class.explicit_bases(db) {
match bases {
// `builtins.object` is the special case:
// the only class in Python that has an MRO with length <2
[] if class.is_object(db) => Ok(Self::from([
// object is not generic, so the default specialization should be a no-op
ClassBase::Class(class_type),
ClassBase::Class(class),
])),
// All other classes in Python have an MRO with length >=2.
@ -92,44 +91,82 @@ impl<'db> Mro<'db> {
// >>> Foo.__mro__
// (<class '__main__.Foo'>, <class 'object'>)
// ```
[] => Ok(Self::from([
ClassBase::Class(class_type),
ClassBase::object(db),
])),
[] => {
// e.g. `class Foo[T]: ...` implicitly has `Generic` inserted into its bases
if class.is_generic() {
Ok(Self::from([
ClassBase::Class(class),
ClassBase::Generic,
ClassBase::object(db),
]))
} else {
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] => ClassBase::try_from_type(db, *single_base).map_or_else(
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|single_base| {
if single_base.has_cyclic_mro(db) {
Err(MroErrorKind::InheritanceCycle)
} else {
Ok(std::iter::once(ClassBase::Class(
class.apply_optional_specialization(db, specialization),
))
.chain(single_base.mro(db, specialization))
.collect())
}
},
),
[single_base]
if !matches!(
single_base,
Type::GenericAlias(_)
| Type::KnownInstance(
KnownInstanceType::Generic(_) | KnownInstanceType::Protocol(_)
)
) =>
{
ClassBase::try_from_type(db, *single_base).map_or_else(
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|single_base| {
if single_base.has_cyclic_mro(db) {
Err(MroErrorKind::InheritanceCycle)
} else {
Ok(std::iter::once(ClassBase::Class(class))
.chain(single_base.mro(db, specialization))
.collect())
}
},
)
}
// 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 => {
let mut valid_bases = vec![];
original_bases => {
let mut resolved_bases = vec![];
let mut invalid_bases = vec![];
for (i, base) in multiple_bases.iter().enumerate() {
match ClassBase::try_from_type(db, *base) {
Some(valid_base) => valid_bases.push(valid_base),
None => invalid_bases.push((i, *base)),
for (i, base) in original_bases.iter().enumerate() {
// This emulates the behavior of `typing._GenericAlias.__mro_entries__` at
// <https://github.com/python/cpython/blob/ad42dc1909bdf8ec775b63fb22ed48ff42797a17/Lib/typing.py#L1487-L1500>.
//
// Note that emit a diagnostic for inheriting from bare (unsubscripted) `Generic` elsewhere
// (see `infer::TypeInferenceBuilder::check_class_definitions`),
// which is why we only care about `KnownInstanceType::Generic(Some(_))`,
// not `KnownInstanceType::Generic(None)`.
if let Type::KnownInstance(KnownInstanceType::Generic(Some(_))) = base {
if original_bases
.contains(&Type::KnownInstance(KnownInstanceType::Protocol(None)))
{
continue;
}
if original_bases[i + 1..]
.iter()
.any(|b| b.is_generic_alias() && b != base)
{
continue;
}
resolved_bases.push(ClassBase::Generic);
} else {
match ClassBase::try_from_type(db, *base) {
Some(valid_base) => resolved_bases.push(valid_base),
None => invalid_bases.push((i, *base)),
}
}
}
@ -137,15 +174,15 @@ impl<'db> Mro<'db> {
return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()));
}
let mut seqs = vec![VecDeque::from([ClassBase::Class(class_type)])];
for base in &valid_bases {
let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])];
for base in &resolved_bases {
if base.has_cyclic_mro(db) {
return Err(MroErrorKind::InheritanceCycle);
}
seqs.push(base.mro(db, specialization).collect());
}
seqs.push(
valid_bases
resolved_bases
.iter()
.map(|base| base.apply_optional_specialization(db, specialization))
.collect(),
@ -161,8 +198,20 @@ impl<'db> Mro<'db> {
let mut base_to_indices: IndexMap<ClassBase<'db>, Vec<usize>, FxBuildHasher> =
IndexMap::default();
for (index, base) in valid_bases.iter().enumerate() {
base_to_indices.entry(*base).or_default().push(index);
// We need to iterate over `original_bases` here rather than `resolved_bases`
// so that we get the correct index of the duplicate bases if there were any
// (`resolved_bases` may be a longer list than `original_bases`!). However, we
// need to use a `ClassBase` rather than a `Type` as the key type for the
// `base_to_indices` map so that a class such as
// `class Foo(Protocol[T], Protocol): ...` correctly causes us to emit a
// `duplicate-base` diagnostic (matching the runtime behaviour) rather than an
// `inconsistent-mro` diagnostic (which would be accurate -- but not nearly as
// precise!).
for (index, base) in original_bases.iter().enumerate() {
let Some(base) = ClassBase::try_from_type(db, *base) else {
continue;
};
base_to_indices.entry(base).or_default().push(index);
}
let mut errors = vec![];
@ -175,9 +224,7 @@ impl<'db> Mro<'db> {
continue;
}
match base {
ClassBase::Class(_)
| ClassBase::Generic(_)
| ClassBase::Protocol(_) => {
ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => {
errors.push(DuplicateBaseError {
duplicate_base: base,
first_index: *first_index,
@ -193,13 +240,10 @@ impl<'db> Mro<'db> {
if duplicate_bases.is_empty() {
if duplicate_dynamic_bases {
Ok(Mro::from_error(
db,
class.apply_optional_specialization(db, specialization),
))
Ok(Mro::from_error(db, class))
} else {
Err(MroErrorKind::UnresolvableMro {
bases_list: valid_bases.into_boxed_slice(),
bases_list: original_bases.iter().copied().collect(),
})
}
} else {
@ -378,7 +422,7 @@ pub(super) enum MroErrorKind<'db> {
/// The MRO is otherwise unresolvable through the C3-merge algorithm.
///
/// See [`c3_merge`] for more details.
UnresolvableMro { bases_list: Box<[ClassBase<'db>]> },
UnresolvableMro { bases_list: Box<[Type<'db>]> },
}
impl<'db> MroErrorKind<'db> {

View file

@ -152,13 +152,11 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(ClassBase::Class(_), _) => Ordering::Less,
(_, ClassBase::Class(_)) => Ordering::Greater,
(ClassBase::Protocol(left), ClassBase::Protocol(right)) => left.cmp(&right),
(ClassBase::Protocol(_), _) => Ordering::Less,
(_, ClassBase::Protocol(_)) => Ordering::Greater,
(ClassBase::Protocol, _) => Ordering::Less,
(_, ClassBase::Protocol) => Ordering::Greater,
(ClassBase::Generic(left), ClassBase::Generic(right)) => left.cmp(&right),
(ClassBase::Generic(_), _) => Ordering::Less,
(_, ClassBase::Generic(_)) => Ordering::Greater,
(ClassBase::Generic, _) => Ordering::Less,
(_, ClassBase::Generic) => Ordering::Greater,
(ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => {
dynamic_elements_ordering(left, right)