[ty] Improve disjointness inference for NominalInstanceTypes and SubclassOfTypes (#18864)

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Alex Waygood 2025-06-24 21:27:37 +01:00 committed by GitHub
parent d89f75f9cc
commit 9d8cba4e8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1255 additions and 442 deletions

View file

@ -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 {

View file

@ -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)
})
}

View file

@ -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::*;

View file

@ -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(

View file

@ -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.

View file

@ -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 {

View file

@ -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);
}
}
}

View file

@ -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),

View file

@ -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>) {