Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] Report classes inheriting from bases with incompatible __slots__ #15129

Merged
merged 6 commits into from
Dec 27, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Per review
InSyncWithFoo committed Dec 25, 2024
commit eb388e0612fc30951ad90c2856a75a3a3034e8ba
13 changes: 13 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/slots.md
Original file line number Diff line number Diff line change
@@ -101,6 +101,19 @@ class E(
): ...
```

## Single solid base

```py
class A:
__slots__ = ("a", "b")

class B(A): ...
class C(A): ...

class D(B, A): ... # fine
class E(B, C, A): ... # fine
```

## False negatives

### Possibly unbound
5 changes: 3 additions & 2 deletions crates/red_knot_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
@@ -157,7 +157,7 @@ declare_lint! {
/// Inheriting from bases with incompatible `__slots__`s
/// will lead to a `TypeError` at runtime.
///
/// Classes with no or empty `__slots__` is always compatible:
/// Classes with no or empty `__slots__` are always compatible:
///
/// ```python
/// class A: ...
@@ -170,7 +170,8 @@ declare_lint! {
/// class D(A, B, C): ...
/// ```
///
/// Class with non-empty `__slots__` cannot participate in multiple inheritance:
/// Multiple inheritance from more than one different class
/// defining non-empty `__slots__` is not allowed:
///
/// ```python
/// class A:
35 changes: 26 additions & 9 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
@@ -624,33 +624,50 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
Ok(_) => {
let mut first_non_empty = None;
let mut has_incompatible = false;
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(self.db()).iter().enumerate() {
let Some(ClassLiteralType { class: base }) = base.into_class_literal()
else {
continue;
};

let slots_kind = SlotsKind::from(self.db(), base);
let solid_base = base.iter_mro(self.db()).find_map(|current| {
let ClassBase::Class(current) = current else {
return None;
};

match SlotsKind::from(self.db(), current) {
SlotsKind::NotEmpty => Some(current),
SlotsKind::NotSpecified | SlotsKind::Empty => None,
SlotsKind::Dynamic => None,
}
});

let base_node = &class_node.bases()[index];

if !matches!(slots_kind, SlotsKind::NotEmpty) {
if solid_base.is_none() {
continue;
}

if first_with_solid_base.is_none() {
first_with_solid_base = Some(index);
common_solid_base = solid_base;
continue;
}

if first_non_empty.is_none() {
first_non_empty = Some(index);
if solid_base == common_solid_base {
continue;
}

has_incompatible = true;
found_second = true;
report_base_with_incompatible_slots(&self.context, base_node);
}

if has_incompatible {
if let Some(index) = first_non_empty {
if found_second {
if let Some(index) = first_with_solid_base {
let base_node = &class_node.bases()[index];
report_base_with_incompatible_slots(&self.context, base_node);
};
2 changes: 1 addition & 1 deletion crates/red_knot_python_semantic/src/types/mro.rs
Original file line number Diff line number Diff line change
@@ -346,7 +346,7 @@ pub(super) enum SlotsKind {

impl SlotsKind {
pub(super) fn from(db: &dyn Db, base: Class) -> Self {
let Symbol::Type(slots_ty, bound) = base.class_member(db, "__slots__") else {
let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__") else {
return Self::NotSpecified;
};