mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-17 00:50:16 +00:00
[ty] Improve disjointness inference for NominalInstanceType
s and SubclassOfType
s (#18864)
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
d89f75f9cc
commit
9d8cba4e8b
23 changed files with 1255 additions and 442 deletions
|
@ -39,6 +39,7 @@ mod util;
|
|||
pub mod pull_types;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
type FxIndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
/// Returns the default registry with all known semantic lints.
|
||||
pub fn default_lint_registry() -> &'static LintRegistry {
|
||||
|
|
|
@ -75,7 +75,6 @@ mod mro;
|
|||
mod narrow;
|
||||
mod protocol_class;
|
||||
mod signatures;
|
||||
mod slots;
|
||||
mod special_form;
|
||||
mod string_annotation;
|
||||
mod subclass_of;
|
||||
|
@ -1824,6 +1823,8 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
(Type::SubclassOf(left), Type::SubclassOf(right)) => left.is_disjoint_from(db, right),
|
||||
|
||||
(
|
||||
Type::SubclassOf(_),
|
||||
Type::BooleanLiteral(..)
|
||||
|
@ -2107,7 +2108,7 @@ impl<'db> Type<'db> {
|
|||
(Type::Tuple(tuple), Type::NominalInstance(instance))
|
||||
| (Type::NominalInstance(instance), Type::Tuple(tuple)) => {
|
||||
tuple.to_class_type(db).is_some_and(|tuple_class| {
|
||||
instance.is_disjoint_from_nominal_instance_of_class(db, tuple_class)
|
||||
!instance.class.could_coexist_in_mro_with(db, tuple_class)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -298,6 +298,11 @@ impl<'db> ClassType<'db> {
|
|||
class_literal.definition(db)
|
||||
}
|
||||
|
||||
/// Return `Some` if this class is known to be a [`SolidBase`], or `None` if it is not.
|
||||
pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
self.class_literal(db).0.as_solid_base(db)
|
||||
}
|
||||
|
||||
/// Return `true` if this class represents `known_class`
|
||||
pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
|
||||
self.known(db) == Some(known_class)
|
||||
|
@ -434,6 +439,69 @@ impl<'db> ClassType<'db> {
|
|||
.apply_optional_specialization(db, specialization)
|
||||
}
|
||||
|
||||
/// Return the [`SolidBase`] that appears first in the MRO of this class.
|
||||
///
|
||||
/// Returns `None` if this class does not have any solid bases in its MRO.
|
||||
pub(super) fn nearest_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
self.iter_mro(db)
|
||||
.filter_map(ClassBase::into_class)
|
||||
.find_map(|base| base.as_solid_base(db))
|
||||
}
|
||||
|
||||
/// Return `true` if this class could coexist in an MRO with `other`.
|
||||
///
|
||||
/// For two given classes `A` and `B`, it is often possible to say for sure
|
||||
/// that there could never exist any class `C` that inherits from both `A` and `B`.
|
||||
/// In these situations, this method returns `false`; in all others, it returns `true`.
|
||||
pub(super) fn could_coexist_in_mro_with(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
if self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Optimisation: if either class is `@final`, we only need to do one `is_subclass_of` call.
|
||||
if self.is_final(db) {
|
||||
return self.is_subclass_of(db, other);
|
||||
}
|
||||
if other.is_final(db) {
|
||||
return other.is_subclass_of(db, self);
|
||||
}
|
||||
|
||||
// Two solid bases can only coexist in an MRO if one is a subclass of the other.
|
||||
if self.nearest_solid_base(db).is_some_and(|solid_base_1| {
|
||||
other.nearest_solid_base(db).is_some_and(|solid_base_2| {
|
||||
!solid_base_1.could_coexist_in_mro_with(db, &solid_base_2)
|
||||
})
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check to see whether the metaclasses of `self` and `other` are disjoint.
|
||||
// Avoid this check if the metaclass of either `self` or `other` is `type`,
|
||||
// however, since we end up with infinite recursion in that case due to the fact
|
||||
// that `type` is its own metaclass (and we know that `type` can coexist in an MRO
|
||||
// with any other arbitrary class, anyway).
|
||||
let type_class = KnownClass::Type.to_class_literal(db);
|
||||
let self_metaclass = self.metaclass(db);
|
||||
if self_metaclass == type_class {
|
||||
return true;
|
||||
}
|
||||
let other_metaclass = other.metaclass(db);
|
||||
if other_metaclass == type_class {
|
||||
return true;
|
||||
}
|
||||
let Some(self_metaclass_instance) = self_metaclass.to_instance(db) else {
|
||||
return true;
|
||||
};
|
||||
let Some(other_metaclass_instance) = other_metaclass.to_instance(db) else {
|
||||
return true;
|
||||
};
|
||||
if self_metaclass_instance.is_disjoint_from(db, other_metaclass_instance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Return a type representing "the set of all instances of the metaclass of this class".
|
||||
pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
self
|
||||
|
@ -860,6 +928,19 @@ impl<'db> ClassLiteral<'db> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Return `Some()` if this class is known to be a [`SolidBase`], or `None` if it is not.
|
||||
pub(super) fn as_solid_base(self, db: &'db dyn Db) -> Option<SolidBase<'db>> {
|
||||
if let Some(known_class) = self.known(db) {
|
||||
known_class
|
||||
.is_solid_base()
|
||||
.then_some(SolidBase::hard_coded(self))
|
||||
} else if SlotsKind::from(db, self) == SlotsKind::NotEmpty {
|
||||
Some(SolidBase::due_to_dunder_slots(self))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over this class's explicit bases, filtering out any bases that are not class
|
||||
/// objects, and applying default specialization to any unspecialized generic class literals.
|
||||
fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = ClassType<'db>> {
|
||||
|
@ -2122,6 +2203,60 @@ impl InheritanceCycle {
|
|||
}
|
||||
}
|
||||
|
||||
/// CPython internally considers a class a "solid base" if it has an atypical instance memory layout,
|
||||
/// with additional memory "slots" for each instance, besides the default object metadata and an
|
||||
/// attribute dictionary. A "solid base" can be a class defined in a C extension which defines C-level
|
||||
/// instance slots, or a Python class that defines non-empty `__slots__`.
|
||||
///
|
||||
/// Two solid bases can only coexist in a class's MRO if one is a subclass of the other. Knowing if
|
||||
/// a class is "solid base" or not is therefore valuable for inferring whether two instance types or
|
||||
/// two subclass-of types are disjoint from each other. It also allows us to detect possible
|
||||
/// `TypeError`s resulting from class definitions.
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub(super) struct SolidBase<'db> {
|
||||
pub(super) class: ClassLiteral<'db>,
|
||||
pub(super) kind: SolidBaseKind,
|
||||
}
|
||||
|
||||
impl<'db> SolidBase<'db> {
|
||||
/// Creates a [`SolidBase`] instance where we know the class is a solid base
|
||||
/// because it is special-cased by ty.
|
||||
fn hard_coded(class: ClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class,
|
||||
kind: SolidBaseKind::HardCoded,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`SolidBase`] instance where we know the class is a solid base
|
||||
/// because of its `__slots__` definition.
|
||||
fn due_to_dunder_slots(class: ClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class,
|
||||
kind: SolidBaseKind::DefinesSlots,
|
||||
}
|
||||
}
|
||||
|
||||
/// Two solid bases can only coexist in a class's MRO if one is a subclass of the other
|
||||
fn could_coexist_in_mro_with(&self, db: &'db dyn Db, other: &Self) -> bool {
|
||||
self == other
|
||||
|| self
|
||||
.class
|
||||
.is_subclass_of(db, None, other.class.default_specialization(db))
|
||||
|| other
|
||||
.class
|
||||
.is_subclass_of(db, None, self.class.default_specialization(db))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub(super) enum SolidBaseKind {
|
||||
/// We know the class is a solid base because of some hardcoded knowledge in ty.
|
||||
HardCoded,
|
||||
/// We know the class is a solid base because it has a non-empty `__slots__` definition.
|
||||
DefinesSlots,
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow
|
||||
/// for easier syntax when interacting with very common classes.
|
||||
///
|
||||
|
@ -2294,6 +2429,83 @@ impl<'db> KnownClass {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class is a [`SolidBase`]
|
||||
const fn is_solid_base(self) -> bool {
|
||||
match self {
|
||||
Self::Object => false,
|
||||
|
||||
// Most non-`@final` builtins (other than `object`) are solid bases.
|
||||
Self::Set
|
||||
| Self::FrozenSet
|
||||
| Self::BaseException
|
||||
| Self::Bytearray
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Complex
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
| Self::Dict
|
||||
| Self::Slice
|
||||
| Self::Property
|
||||
| Self::Staticmethod
|
||||
| Self::Classmethod
|
||||
| Self::Type
|
||||
| Self::ModuleType
|
||||
| Self::Super
|
||||
| Self::GenericAlias
|
||||
| Self::Deque
|
||||
| Self::Bytes => true,
|
||||
|
||||
// It doesn't really make sense to ask the question for `@final` types,
|
||||
// since these are "more than solid bases". But we'll anyway infer a `@final`
|
||||
// class as being disjoint from a class that doesn't appear in its MRO,
|
||||
// and we'll anyway complain if we see a class definition that includes a
|
||||
// `@final` class in its bases. We therefore return `false` here to avoid
|
||||
// unnecessary duplicate diagnostics elsewhere.
|
||||
Self::TypeVarTuple
|
||||
| Self::TypeAliasType
|
||||
| Self::UnionType
|
||||
| Self::NoDefaultType
|
||||
| Self::MethodType
|
||||
| Self::MethodWrapperType
|
||||
| Self::FunctionType
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::StdlibAlias
|
||||
| Self::SpecialForm
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
| Self::ParamSpecArgs
|
||||
| Self::ParamSpecKwargs
|
||||
| Self::WrapperDescriptorType
|
||||
| Self::EllipsisType
|
||||
| Self::NotImplementedType
|
||||
| Self::KwOnly
|
||||
| Self::VersionInfo
|
||||
| Self::Bool
|
||||
| Self::NoneType => false,
|
||||
|
||||
// Anything with a *runtime* MRO (N.B. sometimes different from the MRO that typeshed gives!)
|
||||
// with length >2, or anything that is implemented in pure Python, is not a solid base.
|
||||
Self::ABCMeta
|
||||
| Self::Any
|
||||
| Self::Enum
|
||||
| Self::ChainMap
|
||||
| Self::Exception
|
||||
| Self::ExceptionGroup
|
||||
| Self::Field
|
||||
| Self::SupportsIndex
|
||||
| Self::NamedTuple
|
||||
| Self::NamedTupleFallback
|
||||
| Self::Counter
|
||||
| Self::DefaultDict
|
||||
| Self::OrderedDict
|
||||
| Self::NewType
|
||||
| Self::BaseExceptionGroup => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class is a protocol class.
|
||||
///
|
||||
/// In an ideal world, perhaps we wouldn't hardcode this knowledge here;
|
||||
|
@ -3114,6 +3326,52 @@ pub(super) enum MetaclassErrorKind<'db> {
|
|||
Cycle,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum SlotsKind {
|
||||
/// `__slots__` is not found in the class.
|
||||
NotSpecified,
|
||||
/// `__slots__` is defined but empty: `__slots__ = ()`.
|
||||
Empty,
|
||||
/// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`.
|
||||
NotEmpty,
|
||||
/// `__slots__` is defined but its value is dynamic:
|
||||
/// * `__slots__ = tuple(a for a in b)`
|
||||
/// * `__slots__ = ["a", "b"]`
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl SlotsKind {
|
||||
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
|
||||
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
|
||||
else {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
||||
if matches!(bound, Boundness::PossiblyUnbound) {
|
||||
return Self::Dynamic;
|
||||
}
|
||||
|
||||
match slots_ty {
|
||||
// __slots__ = ("a", "b")
|
||||
Type::Tuple(tuple) => {
|
||||
let tuple = tuple.tuple(db);
|
||||
if tuple.is_variadic() {
|
||||
Self::Dynamic
|
||||
} else if tuple.is_empty() {
|
||||
Self::Empty
|
||||
} else {
|
||||
Self::NotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// __slots__ = "abc" # Same as `("abc",)`
|
||||
Type::StringLiteral(_) => Self::NotEmpty,
|
||||
|
||||
_ => Self::Dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -8,6 +8,7 @@ use super::{
|
|||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::LintDiagnosticGuard;
|
||||
use crate::types::class::{SolidBase, SolidBaseKind};
|
||||
use crate::types::function::KnownFunction;
|
||||
use crate::types::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
|
@ -16,7 +17,7 @@ use crate::types::string_annotation::{
|
|||
};
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
|
||||
use crate::{Db, Module, ModuleName, Program, declare_lint};
|
||||
use crate::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint};
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
|
@ -35,7 +36,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
|||
registry.register_lint(&DIVISION_BY_ZERO);
|
||||
registry.register_lint(&DUPLICATE_BASE);
|
||||
registry.register_lint(&DUPLICATE_KW_ONLY);
|
||||
registry.register_lint(&INCOMPATIBLE_SLOTS);
|
||||
registry.register_lint(&INSTANCE_LAYOUT_CONFLICT);
|
||||
registry.register_lint(&INCONSISTENT_MRO);
|
||||
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
|
||||
registry.register_lint(&INVALID_ARGUMENT_TYPE);
|
||||
|
@ -313,27 +314,27 @@ declare_lint! {
|
|||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for classes whose bases define incompatible `__slots__`.
|
||||
/// Checks for classes definitions which will fail at runtime due to
|
||||
/// "instance memory layout conflicts".
|
||||
///
|
||||
/// This error is usually caused by attempting to combine multiple classes
|
||||
/// that define non-empty `__slots__` in a class's [Method Resolution Order]
|
||||
/// (MRO), or by attempting to combine multiple builtin classes in a class's
|
||||
/// MRO.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Inheriting from bases with incompatible `__slots__`s
|
||||
/// Inheriting from bases with conflicting instance memory layouts
|
||||
/// will lead to a `TypeError` at runtime.
|
||||
///
|
||||
/// Classes with no or empty `__slots__` are always compatible:
|
||||
/// An instance memory layout conflict occurs when CPython cannot determine
|
||||
/// the memory layout instances of a class should have, because the instance
|
||||
/// memory layout of one of its bases conflicts with the instance memory layout
|
||||
/// of one or more of its other bases.
|
||||
///
|
||||
/// ```python
|
||||
/// class A: ...
|
||||
/// class B:
|
||||
/// __slots__ = ()
|
||||
/// class C:
|
||||
/// __slots__ = ("a", "b")
|
||||
///
|
||||
/// # fine
|
||||
/// class D(A, B, C): ...
|
||||
/// ```
|
||||
///
|
||||
/// Multiple inheritance from more than one different class
|
||||
/// defining non-empty `__slots__` is not allowed:
|
||||
/// For example, if a Python class defines non-empty `__slots__`, this will
|
||||
/// impact the memory layout of instances of that class. Multiple inheritance
|
||||
/// from more than one different class defining non-empty `__slots__` is not
|
||||
/// allowed:
|
||||
///
|
||||
/// ```python
|
||||
/// class A:
|
||||
|
@ -346,24 +347,48 @@ declare_lint! {
|
|||
/// class C(A, B): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Dynamic (not tuple or string literal) `__slots__` are not checked.
|
||||
/// Additionally, classes inheriting from built-in classes with implicit layouts
|
||||
/// like `str` or `int` are also not checked.
|
||||
/// An instance layout conflict can also be caused by attempting to use
|
||||
/// multiple inheritance with two builtin classes, due to the way that these
|
||||
/// classes are implemented in a CPython C extension:
|
||||
///
|
||||
/// ```pycon
|
||||
/// >>> hasattr(int, "__slots__")
|
||||
/// False
|
||||
/// >>> hasattr(str, "__slots__")
|
||||
/// False
|
||||
/// >>> class A(int, str): ...
|
||||
/// Traceback (most recent call last):
|
||||
/// File "<python-input-0>", line 1, in <module>
|
||||
/// class A(int, str): ...
|
||||
/// TypeError: multiple bases have instance lay-out conflict
|
||||
/// ```python
|
||||
/// class A(int, float): ... # TypeError: multiple bases have instance lay-out conflict
|
||||
/// ```
|
||||
pub(crate) static INCOMPATIBLE_SLOTS = {
|
||||
summary: "detects class definitions whose MRO has conflicting `__slots__`",
|
||||
///
|
||||
/// Note that pure-Python classes with no `__slots__`, or pure-Python classes
|
||||
/// with empty `__slots__`, are always compatible:
|
||||
///
|
||||
/// ```python
|
||||
/// class A: ...
|
||||
/// class B:
|
||||
/// __slots__ = ()
|
||||
/// class C:
|
||||
/// __slots__ = ("a", "b")
|
||||
///
|
||||
/// # fine
|
||||
/// class D(A, B, C): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
/// Classes that have "dynamic" definitions of `__slots__` (definitions do not consist
|
||||
/// of string literals, or tuples of string literals) are not currently considered solid
|
||||
/// bases by ty.
|
||||
///
|
||||
/// Additionally, this check is not exhaustive: many C extensions (including several in
|
||||
/// the standard library) define classes that use extended memory layouts and thus cannot
|
||||
/// coexist in a single MRO. Since it is currently not possible to represent this fact in
|
||||
/// stub files, having a full knowledge of these classes is also impossible. When it comes
|
||||
/// to classes that do not define `__slots__` at the Python level, therefore, ty, currently
|
||||
/// only hard-codes a number of cases where it knows that a class will produce instances with
|
||||
/// an atypical memory layout.
|
||||
///
|
||||
/// ## Further reading
|
||||
/// - [CPython documentation: `__slots__`](https://docs.python.org/3/reference/datamodel.html#slots)
|
||||
/// - [CPython documentation: Method Resolution Order](https://docs.python.org/3/glossary.html#term-method-resolution-order)
|
||||
///
|
||||
/// [Method Resolution Order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
pub(crate) static INSTANCE_LAYOUT_CONFLICT = {
|
||||
summary: "detects class definitions that raise `TypeError` due to instance layout conflict",
|
||||
status: LintStatus::preview("1.0.0"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
|
@ -1901,11 +1926,193 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
|
|||
));
|
||||
}
|
||||
|
||||
pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) {
|
||||
let Some(builder) = context.report_lint(&INCOMPATIBLE_SLOTS, node) else {
|
||||
pub(crate) fn report_instance_layout_conflict(
|
||||
context: &InferContext,
|
||||
class: ClassLiteral,
|
||||
node: &ast::StmtClassDef,
|
||||
solid_bases: &IncompatibleBases,
|
||||
) {
|
||||
debug_assert!(solid_bases.len() > 1);
|
||||
|
||||
let db = context.db();
|
||||
|
||||
let Some(builder) = context.report_lint(&INSTANCE_LAYOUT_CONFLICT, class.header_range(db))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
builder.into_diagnostic("Class base has incompatible `__slots__`");
|
||||
|
||||
let mut diagnostic = builder
|
||||
.into_diagnostic("Class will raise `TypeError` at runtime due to incompatible bases");
|
||||
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Bases {} cannot be combined in multiple inheritance",
|
||||
solid_bases.describe_problematic_class_bases(db)
|
||||
));
|
||||
|
||||
let mut subdiagnostic = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"Two classes cannot coexist in a class's MRO if their instances \
|
||||
have incompatible memory layouts",
|
||||
);
|
||||
|
||||
for (solid_base, solid_base_info) in solid_bases {
|
||||
let IncompatibleBaseInfo {
|
||||
node_index,
|
||||
originating_base,
|
||||
} = solid_base_info;
|
||||
|
||||
let span = context.span(&node.bases()[*node_index]);
|
||||
let mut annotation = Annotation::secondary(span.clone());
|
||||
if solid_base.class == *originating_base {
|
||||
match solid_base.kind {
|
||||
SolidBaseKind::DefinesSlots => {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout because `{base}` defines non-empty `__slots__`",
|
||||
base = originating_base.name(db)
|
||||
));
|
||||
}
|
||||
SolidBaseKind::HardCoded => {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout because of the way `{base}` \
|
||||
is implemented in a C extension",
|
||||
base = originating_base.name(db)
|
||||
));
|
||||
}
|
||||
}
|
||||
subdiagnostic.annotate(annotation);
|
||||
} else {
|
||||
annotation = annotation.message(format_args!(
|
||||
"`{base}` instances have a distinct memory layout \
|
||||
because `{base}` inherits from `{solid_base}`",
|
||||
base = originating_base.name(db),
|
||||
solid_base = solid_base.class.name(db)
|
||||
));
|
||||
subdiagnostic.annotate(annotation);
|
||||
|
||||
let mut additional_annotation = Annotation::secondary(span);
|
||||
|
||||
additional_annotation = match solid_base.kind {
|
||||
SolidBaseKind::DefinesSlots => additional_annotation.message(format_args!(
|
||||
"`{solid_base}` instances have a distinct memory layout because `{solid_base}` \
|
||||
defines non-empty `__slots__`",
|
||||
solid_base = solid_base.class.name(db),
|
||||
)),
|
||||
|
||||
SolidBaseKind::HardCoded => additional_annotation.message(format_args!(
|
||||
"`{solid_base}` instances have a distinct memory layout \
|
||||
because of the way `{solid_base}` is implemented in a C extension",
|
||||
solid_base = solid_base.class.name(db),
|
||||
)),
|
||||
};
|
||||
|
||||
subdiagnostic.annotate(additional_annotation);
|
||||
}
|
||||
}
|
||||
|
||||
diagnostic.sub(subdiagnostic);
|
||||
}
|
||||
|
||||
/// Information regarding the conflicting solid bases a class is inferred to have in its MRO.
|
||||
///
|
||||
/// For each solid base, we record information about which element in the class's bases list
|
||||
/// caused the solid base to be included in the class's MRO.
|
||||
///
|
||||
/// The inner data is an `IndexMap` to ensure that diagnostics regarding conflicting solid bases
|
||||
/// are reported in a stable order.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct IncompatibleBases<'db>(FxIndexMap<SolidBase<'db>, IncompatibleBaseInfo<'db>>);
|
||||
|
||||
impl<'db> IncompatibleBases<'db> {
|
||||
pub(super) fn insert(
|
||||
&mut self,
|
||||
base: SolidBase<'db>,
|
||||
node_index: usize,
|
||||
class: ClassLiteral<'db>,
|
||||
) {
|
||||
let info = IncompatibleBaseInfo {
|
||||
node_index,
|
||||
originating_base: class,
|
||||
};
|
||||
self.0.insert(base, info);
|
||||
}
|
||||
|
||||
/// List the problematic class bases in a human-readable format.
|
||||
fn describe_problematic_class_bases(&self, db: &dyn Db) -> String {
|
||||
let num_bases = self.len();
|
||||
debug_assert!(num_bases >= 2);
|
||||
|
||||
let mut bad_base_names = self.0.values().map(|info| info.originating_base.name(db));
|
||||
|
||||
let final_base = bad_base_names.next_back().unwrap();
|
||||
let penultimate_base = bad_base_names.next_back().unwrap();
|
||||
|
||||
let mut buffer = String::new();
|
||||
|
||||
for base_name in bad_base_names {
|
||||
buffer.push('`');
|
||||
buffer.push_str(base_name);
|
||||
buffer.push_str("`, ");
|
||||
}
|
||||
|
||||
buffer.push('`');
|
||||
buffer.push_str(penultimate_base);
|
||||
buffer.push_str("` and `");
|
||||
buffer.push_str(final_base);
|
||||
buffer.push('`');
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Two solid bases are allowed to coexist in an MRO if one is a subclass of the other.
|
||||
/// This method therefore removes any entry in `self` that is a subclass of one or more
|
||||
/// other entries also contained in `self`.
|
||||
pub(super) fn remove_redundant_entries(&mut self, db: &'db dyn Db) {
|
||||
self.0 = self
|
||||
.0
|
||||
.iter()
|
||||
.filter(|(solid_base, _)| {
|
||||
self.0
|
||||
.keys()
|
||||
.filter(|other_base| other_base != solid_base)
|
||||
.all(|other_base| {
|
||||
!solid_base.class.is_subclass_of(
|
||||
db,
|
||||
None,
|
||||
other_base.class.default_specialization(db),
|
||||
)
|
||||
})
|
||||
})
|
||||
.map(|(base, info)| (*base, *info))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a IncompatibleBases<'db> {
|
||||
type Item = (&'a SolidBase<'db>, &'a IncompatibleBaseInfo<'db>);
|
||||
type IntoIter = indexmap::map::Iter<'a, SolidBase<'db>, IncompatibleBaseInfo<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about which class base the "solid base" stems from
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(super) struct IncompatibleBaseInfo<'db> {
|
||||
/// The index of the problematic base in the [`ast::StmtClassDef`]'s bases list.
|
||||
node_index: usize,
|
||||
|
||||
/// The base class in the [`ast::StmtClassDef`]'s bases list that caused
|
||||
/// the solid base to be included in the class's MRO.
|
||||
///
|
||||
/// This won't necessarily be the same class as the `SolidBase`'s class,
|
||||
/// as the `SolidBase` may have found its way into the class's MRO by dint of it being a
|
||||
/// superclass of one of the classes in the class definition's bases list.
|
||||
originating_base: ClassLiteral<'db>,
|
||||
}
|
||||
|
||||
pub(crate) fn report_invalid_arguments_to_annotated(
|
||||
|
|
|
@ -81,13 +81,14 @@ use crate::types::diagnostic::{
|
|||
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
|
||||
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
|
||||
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT,
|
||||
TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT,
|
||||
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
|
||||
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated,
|
||||
report_invalid_arguments_to_callable, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
|
||||
report_invalid_return_type, report_possibly_unbound_attribute,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL,
|
||||
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
|
||||
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_implicit_return_type,
|
||||
report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
|
||||
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
|
||||
report_invalid_assignment, report_invalid_attribute_assignment,
|
||||
report_invalid_generator_function_return_type, report_invalid_return_type,
|
||||
report_possibly_unbound_attribute,
|
||||
};
|
||||
use crate::types::function::{
|
||||
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
|
||||
|
@ -123,7 +124,6 @@ use super::diagnostic::{
|
|||
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
|
||||
};
|
||||
use super::generics::LegacyGenericBase;
|
||||
use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
|
||||
};
|
||||
|
@ -887,12 +887,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
|
||||
let is_protocol = class.is_protocol(self.db());
|
||||
let mut solid_bases = IncompatibleBases::default();
|
||||
|
||||
// (2) Iterate through the class's explicit bases to check for various possible errors:
|
||||
// - Check for inheritance from plain `Generic`,
|
||||
// - Check for inheritance from a `@final` classes
|
||||
// - If the class is a protocol class: check for inheritance from a non-protocol class
|
||||
for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() {
|
||||
if let Some((class, solid_base)) = base_class
|
||||
.to_class_type(self.db())
|
||||
.and_then(|class| Some((class, class.nearest_solid_base(self.db())?)))
|
||||
{
|
||||
solid_bases.insert(solid_base, i, class.class_literal(self.db()).0);
|
||||
}
|
||||
|
||||
let base_class = match base_class {
|
||||
Type::SpecialForm(SpecialFormType::Generic) => {
|
||||
if let Some(builder) = self
|
||||
|
@ -1016,7 +1024,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
},
|
||||
Ok(_) => check_class_slots(&self.context, class, class_node),
|
||||
Ok(_) => {
|
||||
solid_bases.remove_redundant_entries(self.db());
|
||||
|
||||
if solid_bases.len() > 1 {
|
||||
report_instance_layout_conflict(
|
||||
&self.context,
|
||||
class,
|
||||
class_node,
|
||||
&solid_bases,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (4) Check that the class's metaclass can be determined without error.
|
||||
|
|
|
@ -105,42 +105,7 @@ impl<'db> NominalInstanceType<'db> {
|
|||
}
|
||||
|
||||
pub(super) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
self.is_disjoint_from_nominal_instance_of_class(db, other.class)
|
||||
}
|
||||
|
||||
// Note that this method only exists so that we can check disjointness between nominal
|
||||
// instances of `tuple` and some other class. Tuples are currently represented by the
|
||||
// `Type::Tuple` variant, not `Type::NominalInstance`. We have a TODO to try to remove the
|
||||
// dedicated `Tuple` variant in favor of `NominalInstance`; if we can do that, then we won't
|
||||
// need this method, and its logic can be subsumed into `is_disjoint_from`.
|
||||
pub(super) fn is_disjoint_from_nominal_instance_of_class(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
other_class: ClassType,
|
||||
) -> bool {
|
||||
if self.class.is_final(db) && !self.class.is_subclass_of(db, other_class) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if other_class.is_final(db) && !other_class.is_subclass_of(db, self.class) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check to see whether the metaclasses of `self` and `other` are disjoint.
|
||||
// Avoid this check if the metaclass of either `self` or `other` is `type`,
|
||||
// however, since we end up with infinite recursion in that case due to the fact
|
||||
// that `type` is its own metaclass (and we know that `type` cannot be disjoint
|
||||
// from any metaclass, anyway).
|
||||
let type_type = KnownClass::Type.to_instance(db);
|
||||
let self_metaclass = self.class.metaclass_instance_type(db);
|
||||
if self_metaclass == type_type {
|
||||
return false;
|
||||
}
|
||||
let other_metaclass = other_class.metaclass_instance_type(db);
|
||||
if other_metaclass == type_type {
|
||||
return false;
|
||||
}
|
||||
self_metaclass.is_disjoint_from(db, other_metaclass)
|
||||
!self.class.could_coexist_in_mro_with(db, other.class)
|
||||
}
|
||||
|
||||
pub(super) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::place::{Boundness, Place};
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::diagnostic::report_base_with_incompatible_slots;
|
||||
use crate::types::{ClassLiteral, Type};
|
||||
|
||||
use super::InferContext;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum SlotsKind {
|
||||
/// `__slots__` is not found in the class.
|
||||
NotSpecified,
|
||||
/// `__slots__` is defined but empty: `__slots__ = ()`.
|
||||
Empty,
|
||||
/// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`.
|
||||
NotEmpty,
|
||||
/// `__slots__` is defined but its value is dynamic:
|
||||
/// * `__slots__ = tuple(a for a in b)`
|
||||
/// * `__slots__ = ["a", "b"]`
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl SlotsKind {
|
||||
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
|
||||
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
|
||||
else {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
||||
if matches!(bound, Boundness::PossiblyUnbound) {
|
||||
return Self::Dynamic;
|
||||
}
|
||||
|
||||
match slots_ty {
|
||||
// __slots__ = ("a", "b")
|
||||
Type::Tuple(tuple) => {
|
||||
if tuple.tuple(db).is_empty() {
|
||||
Self::Empty
|
||||
} else {
|
||||
Self::NotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// __slots__ = "abc" # Same as `("abc",)`
|
||||
Type::StringLiteral(_) => Self::NotEmpty,
|
||||
|
||||
_ => Self::Dynamic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check_class_slots(
|
||||
context: &InferContext,
|
||||
class: ClassLiteral,
|
||||
node: &ast::StmtClassDef,
|
||||
) {
|
||||
let db = context.db();
|
||||
|
||||
let mut first_with_solid_base = None;
|
||||
let mut common_solid_base = None;
|
||||
let mut found_second = false;
|
||||
|
||||
for (index, base) in class.explicit_bases(db).iter().enumerate() {
|
||||
let Type::ClassLiteral(base) = base else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let solid_base = base.iter_mro(db, None).find_map(|current| {
|
||||
let ClassBase::Class(current) = current else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (class_literal, _) = current.class_literal(db);
|
||||
match SlotsKind::from(db, class_literal) {
|
||||
SlotsKind::NotEmpty => Some(current),
|
||||
SlotsKind::NotSpecified | SlotsKind::Empty => None,
|
||||
SlotsKind::Dynamic => None,
|
||||
}
|
||||
});
|
||||
|
||||
if solid_base.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let base_node = &node.bases()[index];
|
||||
|
||||
if first_with_solid_base.is_none() {
|
||||
first_with_solid_base = Some(index);
|
||||
common_solid_base = solid_base;
|
||||
continue;
|
||||
}
|
||||
|
||||
if solid_base == common_solid_base {
|
||||
continue;
|
||||
}
|
||||
|
||||
found_second = true;
|
||||
report_base_with_incompatible_slots(context, base_node);
|
||||
}
|
||||
|
||||
if found_second {
|
||||
if let Some(index) = first_with_solid_base {
|
||||
let base_node = &node.bases()[index];
|
||||
report_base_with_incompatible_slots(context, base_node);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -159,6 +159,18 @@ impl<'db> SubclassOfType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return` true` if `self` is a disjoint type from `other`.
|
||||
///
|
||||
/// See [`Type::is_disjoint_from`] for more details.
|
||||
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
match (self.subclass_of, other.subclass_of) {
|
||||
(SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => false,
|
||||
(SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => {
|
||||
!self_class.could_coexist_in_mro_with(db, other_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(self, db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
subclass_of: self.subclass_of.normalized(db),
|
||||
|
|
|
@ -710,6 +710,10 @@ impl<'db> TupleSpec<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_variadic(&self) -> bool {
|
||||
matches!(self, TupleSpec::Variable(_))
|
||||
}
|
||||
|
||||
/// Returns the minimum and maximum length of this tuple. (The maximum length will be `None`
|
||||
/// for a tuple with a variable-length portion.)
|
||||
pub(crate) fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue