diff --git a/pyrefly/lib/alt/class/class_metadata.rs b/pyrefly/lib/alt/class/class_metadata.rs index 9b0f032cbd..607cf066f7 100644 --- a/pyrefly/lib/alt/class/class_metadata.rs +++ b/pyrefly/lib/alt/class/class_metadata.rs @@ -402,6 +402,35 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } let extends_abc = self.extends_abc(&bases_with_metadata, metaclass); + // Check for __slots__ layout conflict: if two or more base classes + // define non-empty __slots__, CPython raises TypeError at runtime + // ("multiple bases have instance lay-out conflict"). + { + let bases_with_nonempty_slots: Vec<&Class> = bases_with_metadata + .iter() + .filter(|(_, metadata)| { + metadata.slots_info().is_some_and(|si| !si.names.is_empty()) + }) + .map(|(base, _)| base) + .collect(); + if bases_with_nonempty_slots.len() > 1 { + let names: Vec = bases_with_nonempty_slots + .iter() + .map(|b| format!("`{}`", b.name())) + .collect(); + self.error( + errors, + cls.range(), + ErrorInfo::Kind(ErrorKind::InvalidInheritance), + format!( + "Class `{}` has multiple base classes with non-empty `__slots__` ({}), which causes a TypeError at runtime", + cls.name(), + names.join(", "), + ), + ); + } + } + // Compute final base class list. let bases = if is_typed_dict && bases_with_metadata.is_empty() { // This is a "fallback" class that contains attributes that are available on all TypedDict subclasses. diff --git a/pyrefly/lib/test/slots.rs b/pyrefly/lib/test/slots.rs index 60dbc689bb..7c2504bcb2 100644 --- a/pyrefly/lib/test/slots.rs +++ b/pyrefly/lib/test/slots.rs @@ -281,7 +281,6 @@ c.x = 2 # OK: descriptor __set__ handles this, not instance storage // https://github.com/facebook/pyrefly/issues/2917 testcase!( - bug = "Should detect instance layout conflict when multiple bases have __slots__", test_slots_multiple_inheritance_layout_conflict, r#" class Left: @@ -292,13 +291,12 @@ class Right: # Inheriting from two classes that both define non-empty __slots__ # causes a TypeError at runtime. -class Combined(Left, Right): ... +class Combined(Left, Right): ... # E: multiple base classes with non-empty `__slots__` "#, ); // https://github.com/facebook/pyrefly/issues/2916 testcase!( - bug = "Should detect instance layout conflict even with identical slot names", test_slots_layout_conflict_same_names, r#" class First: @@ -308,7 +306,51 @@ class Second: __slots__ = ("x",) # Even though the slot names match, these are different C-level layouts. -class Both(First, Second): ... +class Both(First, Second): ... # E: multiple base classes with non-empty `__slots__` +"#, +); + +testcase!( + test_slots_layout_conflict_empty_slots_ok, + r#" +class A: + __slots__ = ("x",) + +class B: + __slots__ = () + +# One base has non-empty slots, the other has empty slots - this is fine. +class C(A, B): ... +"#, +); + +testcase!( + test_slots_layout_conflict_both_empty_ok, + r#" +class A: + __slots__ = () + +class B: + __slots__ = () + +# Both bases have empty slots - no conflict. +class C(A, B): ... +"#, +); + +testcase!( + test_slots_layout_conflict_three_bases, + r#" +class A: + __slots__ = ("x",) + +class B: + __slots__ = ("y",) + +class C: + __slots__ = ("z",) + +class D(A, B, C): ... # E: multiple base classes with non-empty `__slots__` "#, );