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

[pylint] Include name of base class in message for redefined-slots-in-subclass (W0244) #15559

Merged
merged 7 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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 ruff_diagnostics::{Diagnostic, Violation};
Expand Down Expand Up @@ -39,14 +39,15 @@ use crate::checkers::ast::Checker;
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct RedefinedSlotsInSubclass {
name: String,
base: String,
slot_name: String,
}

impl Violation for RedefinedSlotsInSubclass {
#[derive_message_formats]
fn message(&self) -> String {
let RedefinedSlotsInSubclass { name } = self;
format!("Redefined slot '{name}' in subclass")
let RedefinedSlotsInSubclass { base, slot_name } = self;
format!("Slot `{slot_name}` redefined from base `{base}`")
}
}

Expand All @@ -68,15 +69,7 @@ pub(crate) fn redefined_slots_in_subclass(checker: &mut Checker, class_def: &ast
let semantic = checker.semantic();
let mut diagnostics: Vec<_> = class_slots
.iter()
.filter(|&slot| contained_in_super_slots(class_def, semantic, slot))
.map(|slot| {
Diagnostic::new(
RedefinedSlotsInSubclass {
name: slot.name.to_string(),
},
slot.range(),
)
})
.filter_map(|slot| contained_in_super_slots(class_def, semantic, slot))
dylwil3 marked this conversation as resolved.
Show resolved Hide resolved
.collect();
checker.diagnostics.append(&mut diagnostics);
}
Expand Down Expand Up @@ -115,15 +108,21 @@ fn contained_in_super_slots(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
slot: &Slot,
) -> bool {
any_super_class(class_def, semantic, &|super_class| {
// This function checks every super class
// but we want every _strict_ super class, hence:
if class_def.name == super_class.name {
return false;
}
slots_members(&super_class.body).contains(slot)
})
) -> Option<Diagnostic> {
iter_super_class(class_def, semantic)
.skip(1)
.find_map(&|super_class: &ast::StmtClassDef| {
if slots_members(&super_class.body).contains(slot) {
return Some(Diagnostic::new(
RedefinedSlotsInSubclass {
base: super_class.name.to_string(),
slot_name: slot.name.to_string(),
},
slot.range(),
));
}
None
})
}

fn slots_members(body: &[Stmt]) -> FxHashSet<Slot> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
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 `Base`
dylwil3 marked this conversation as resolved.
Show resolved Hide resolved
|
5 | class Subclass(Base):
6 | __slots__ = ("a", "d") # [redefined-slots-in-subclass]
Expand All @@ -10,7 +10,7 @@ redefined_slots_in_subclass.py:6:18: PLW0244 Redefined slot 'a' in subclass
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 `Grandparent`
|
16 | class Child(Parent):
17 | __slots__ = ("c", "a")
Expand All @@ -19,14 +19,14 @@ redefined_slots_in_subclass.py:17:23: PLW0244 Redefined slot 'a' in subclass
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 `AnotherBase`
|
22 | class AnotherChild(AnotherBase):
23 | __slots__ = ["a","b","e","f"]
| ^^^ 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 `AnotherBase`
|
22 | class AnotherChild(AnotherBase):
23 | __slots__ = ["a","b","e","f"]
Expand Down
83 changes: 50 additions & 33 deletions crates/ruff_python_semantic/src/analyze/class.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::VecDeque;

use rustc_hash::FxHashSet;

use crate::analyze::typing;
Expand Down Expand Up @@ -70,46 +72,61 @@ pub fn any_base_class(
inner(class_def, semantic, func, &mut FxHashSet::default())
}

/// Return `true` if any base class matches an [`ast::StmtClassDef`] predicate.
pub fn any_super_class(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
func: &dyn Fn(&ast::StmtClassDef) -> bool,
) -> bool {
fn inner(
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;
}
/// Returns an iterator over all base classes, beginning with the
/// given class.
///
/// The traversal of the class hierarchy is breadth-first.
pub fn iter_super_class<'stmt>(
class_def: &'stmt ast::StmtClassDef,
semantic: &'stmt SemanticModel,
) -> impl Iterator<Item = &'stmt ast::StmtClassDef> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for doing breadth-first? I think the original implementation did depth-first traversal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my intuition is that the superclass graph will usually be deeper than it is wide (but maybe that assumption is off), and that the width is often pretty small in any event. Does that seem right to you?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To save you the re-review, I'm gonna merge this in. But if you wake up in the middle of the night shouting "depth first!" I will be happy to change it back 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, no worries.

I guess my intuition is that the superclass graph will usually be deeper than it is wide (but maybe that assumption is off), and that the width is often pretty small in any event. Does that seem right to you?

I'm not sure about this but I don't think this should necessarily affect the performance if that's your concern. One minor nit would be the usage of VecDeque in which the elements in memory are not guaranteed to be contiguous compared to Vec. This also makes the implementation inconsistent with the similar function in the same module any_base_class.

That said, we don't need to change anything here right now.

struct SuperClassIterator<'stmt> {
semantic: &'stmt SemanticModel<'stmt>,
to_visit: VecDeque<&'stmt ast::StmtClassDef>,
seen: FxHashSet<BindingId>,
}

// 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;
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);
}
}
}
}
false
})
Some(current)
}
}

inner(class_def, semantic, func, &mut FxHashSet::default())
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(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
func: &dyn Fn(&ast::StmtClassDef) -> bool,
) -> bool {
iter_super_class(class_def, semantic).any(func)
}

#[derive(Clone, Debug)]
Expand Down
Loading