mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 20:10:09 +00:00
[pylint
] Include name of base class in message for redefined-slots-in-subclass
(W0244
) (#15559)
In the following situation: ```python class Grandparent: __slots__ = "a" class Parent(Grandparent): ... class Child(Parent): __slots__ = "a" ``` the message for `W0244` now specifies that `a` is overwriting a slot from `Grandparent`. To implement this, we introduce a helper function `iter_super_classes` which does a breadth-first traversal of the superclasses of a given class (as long as they are defined in the same file, due to the usual limitations of the semantic model). Note: Python does not allow conflicting slots definitions under multiple inheritance. Unless I'm misunderstanding something, I believe It follows that the subposet of superclasses of a given class that redefine a given slot is in fact totally ordered. There is therefore a unique _nearest_ superclass whose slot is being overwritten. So, you know, in case anyone was super worried about that... you can just chill. This is a followup to #9640 .
This commit is contained in:
parent
fb15da5694
commit
4caeeb8d98
3 changed files with 79 additions and 62 deletions
|
@ -1,6 +1,6 @@
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
use ruff_python_semantic::{analyze::class::any_super_class, SemanticModel};
|
use ruff_python_semantic::{analyze::class::iter_super_class, SemanticModel};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, Violation};
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
|
@ -39,14 +39,15 @@ use crate::checkers::ast::Checker;
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(ViolationMetadata)]
|
#[derive(ViolationMetadata)]
|
||||||
pub(crate) struct RedefinedSlotsInSubclass {
|
pub(crate) struct RedefinedSlotsInSubclass {
|
||||||
name: String,
|
base: String,
|
||||||
|
slot_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Violation for RedefinedSlotsInSubclass {
|
impl Violation for RedefinedSlotsInSubclass {
|
||||||
#[derive_message_formats]
|
#[derive_message_formats]
|
||||||
fn message(&self) -> String {
|
fn message(&self) -> String {
|
||||||
let RedefinedSlotsInSubclass { name } = self;
|
let RedefinedSlotsInSubclass { base, slot_name } = self;
|
||||||
format!("Redefined slot '{name}' in subclass")
|
format!("Slot `{slot_name}` redefined from base class `{base}`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,15 +69,7 @@ pub(crate) fn redefined_slots_in_subclass(checker: &mut Checker, class_def: &ast
|
||||||
let semantic = checker.semantic();
|
let semantic = checker.semantic();
|
||||||
let mut diagnostics: Vec<_> = class_slots
|
let mut diagnostics: Vec<_> = class_slots
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&slot| contained_in_super_slots(class_def, semantic, slot))
|
.filter_map(|slot| check_super_slots(class_def, semantic, slot))
|
||||||
.map(|slot| {
|
|
||||||
Diagnostic::new(
|
|
||||||
RedefinedSlotsInSubclass {
|
|
||||||
name: slot.name.to_string(),
|
|
||||||
},
|
|
||||||
slot.range(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
checker.diagnostics.append(&mut diagnostics);
|
checker.diagnostics.append(&mut diagnostics);
|
||||||
}
|
}
|
||||||
|
@ -111,18 +104,24 @@ impl Ranged for Slot<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contained_in_super_slots(
|
fn check_super_slots(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
slot: &Slot,
|
slot: &Slot,
|
||||||
) -> bool {
|
) -> Option<Diagnostic> {
|
||||||
any_super_class(class_def, semantic, &|super_class| {
|
iter_super_class(class_def, semantic)
|
||||||
// This function checks every super class
|
.skip(1)
|
||||||
// but we want every _strict_ super class, hence:
|
.find_map(&|super_class: &ast::StmtClassDef| {
|
||||||
if class_def.name == super_class.name {
|
if slots_members(&super_class.body).contains(slot) {
|
||||||
return false;
|
return Some(Diagnostic::new(
|
||||||
|
RedefinedSlotsInSubclass {
|
||||||
|
base: super_class.name.to_string(),
|
||||||
|
slot_name: slot.name.to_string(),
|
||||||
|
},
|
||||||
|
slot.range(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
slots_members(&super_class.body).contains(slot)
|
None
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||||
---
|
---
|
||||||
redefined_slots_in_subclass.py:6:18: PLW0244 Redefined slot 'a' in subclass
|
redefined_slots_in_subclass.py:6:18: PLW0244 Slot `a` redefined from base class `Base`
|
||||||
|
|
|
|
||||||
5 | class Subclass(Base):
|
5 | class Subclass(Base):
|
||||||
6 | __slots__ = ("a", "d") # [redefined-slots-in-subclass]
|
6 | __slots__ = ("a", "d") # [redefined-slots-in-subclass]
|
||||||
|
@ -10,7 +10,7 @@ redefined_slots_in_subclass.py:6:18: PLW0244 Redefined slot 'a' in subclass
|
||||||
8 | class Grandparent:
|
8 | class Grandparent:
|
||||||
|
|
|
|
||||||
|
|
||||||
redefined_slots_in_subclass.py:17:23: PLW0244 Redefined slot 'a' in subclass
|
redefined_slots_in_subclass.py:17:23: PLW0244 Slot `a` redefined from base class `Grandparent`
|
||||||
|
|
|
|
||||||
16 | class Child(Parent):
|
16 | class Child(Parent):
|
||||||
17 | __slots__ = ("c", "a")
|
17 | __slots__ = ("c", "a")
|
||||||
|
@ -19,14 +19,14 @@ redefined_slots_in_subclass.py:17:23: PLW0244 Redefined slot 'a' in subclass
|
||||||
19 | class AnotherBase:
|
19 | class AnotherBase:
|
||||||
|
|
|
|
||||||
|
|
||||||
redefined_slots_in_subclass.py:23:18: PLW0244 Redefined slot 'a' in subclass
|
redefined_slots_in_subclass.py:23:18: PLW0244 Slot `a` redefined from base class `AnotherBase`
|
||||||
|
|
|
|
||||||
22 | class AnotherChild(AnotherBase):
|
22 | class AnotherChild(AnotherBase):
|
||||||
23 | __slots__ = ["a","b","e","f"]
|
23 | __slots__ = ["a","b","e","f"]
|
||||||
| ^^^ PLW0244
|
| ^^^ PLW0244
|
||||||
|
|
|
|
||||||
|
|
||||||
redefined_slots_in_subclass.py:23:22: PLW0244 Redefined slot 'b' in subclass
|
redefined_slots_in_subclass.py:23:22: PLW0244 Slot `b` redefined from base class `AnotherBase`
|
||||||
|
|
|
|
||||||
22 | class AnotherChild(AnotherBase):
|
22 | class AnotherChild(AnotherBase):
|
||||||
23 | __slots__ = ["a","b","e","f"]
|
23 | __slots__ = ["a","b","e","f"]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
use crate::analyze::typing;
|
use crate::analyze::typing;
|
||||||
|
@ -70,46 +72,62 @@ pub fn any_base_class(
|
||||||
inner(class_def, semantic, func, &mut FxHashSet::default())
|
inner(class_def, semantic, func, &mut FxHashSet::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if any base class matches an [`ast::StmtClassDef`] predicate.
|
/// Returns an iterator over all base classes, beginning with the
|
||||||
|
/// given class.
|
||||||
|
///
|
||||||
|
/// The traversal of the class hierarchy is breadth-first, since
|
||||||
|
/// this graph tends to have small width but could be rather deep.
|
||||||
|
pub fn iter_super_class<'stmt>(
|
||||||
|
class_def: &'stmt ast::StmtClassDef,
|
||||||
|
semantic: &'stmt SemanticModel,
|
||||||
|
) -> impl Iterator<Item = &'stmt ast::StmtClassDef> {
|
||||||
|
struct SuperClassIterator<'stmt> {
|
||||||
|
semantic: &'stmt SemanticModel<'stmt>,
|
||||||
|
to_visit: VecDeque<&'stmt ast::StmtClassDef>,
|
||||||
|
seen: FxHashSet<BindingId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for SuperClassIterator<'a> {
|
||||||
|
type Item = &'a ast::StmtClassDef;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let current = self.to_visit.pop_front()?;
|
||||||
|
|
||||||
|
// Add all base classes to the to_visit list
|
||||||
|
for expr in current.bases() {
|
||||||
|
if let Some(id) = self.semantic.lookup_attribute(map_subscript(expr)) {
|
||||||
|
if self.seen.insert(id) {
|
||||||
|
let binding = self.semantic.binding(id);
|
||||||
|
if let Some(base_class) = binding
|
||||||
|
.kind
|
||||||
|
.as_class_definition()
|
||||||
|
.map(|id| &self.semantic.scopes[*id])
|
||||||
|
.and_then(|scope| scope.kind.as_class())
|
||||||
|
{
|
||||||
|
self.to_visit.push_back(base_class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SuperClassIterator {
|
||||||
|
semantic,
|
||||||
|
to_visit: VecDeque::from([class_def]),
|
||||||
|
seen: FxHashSet::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if any base class, including the given class,
|
||||||
|
/// matches an [`ast::StmtClassDef`] predicate.
|
||||||
pub fn any_super_class(
|
pub fn any_super_class(
|
||||||
class_def: &ast::StmtClassDef,
|
class_def: &ast::StmtClassDef,
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
func: &dyn Fn(&ast::StmtClassDef) -> bool,
|
func: &dyn Fn(&ast::StmtClassDef) -> bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
fn inner(
|
iter_super_class(class_def, semantic).any(func)
|
||||||
class_def: &ast::StmtClassDef,
|
|
||||||
semantic: &SemanticModel,
|
|
||||||
func: &dyn Fn(&ast::StmtClassDef) -> bool,
|
|
||||||
seen: &mut FxHashSet<BindingId>,
|
|
||||||
) -> bool {
|
|
||||||
// If the function itself matches the pattern, then this does too.
|
|
||||||
if func(class_def) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, check every base class.
|
|
||||||
class_def.bases().iter().any(|expr| {
|
|
||||||
// If the base class extends a class that matches the pattern, then this does too.
|
|
||||||
if let Some(id) = semantic.lookup_attribute(map_subscript(expr)) {
|
|
||||||
if seen.insert(id) {
|
|
||||||
let binding = semantic.binding(id);
|
|
||||||
if let Some(base_class) = binding
|
|
||||||
.kind
|
|
||||||
.as_class_definition()
|
|
||||||
.map(|id| &semantic.scopes[*id])
|
|
||||||
.and_then(|scope| scope.kind.as_class())
|
|
||||||
{
|
|
||||||
if inner(base_class, semantic, func, seen) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
inner(class_def, semantic, func, &mut FxHashSet::default())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue