forked from astral-sh/ruff
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
…sh#9640) ## Summary - Implementation of [redefined-slots-in-subclass / W0244](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/redefined-slots-in-subclass.html). - Related to astral-sh#970 --------- Co-authored-by: Akira Noda <[email protected]> Co-authored-by: dylwil3 <[email protected]>
- Loading branch information
1 parent
4fdf8af
commit 5cdac25
Showing
8 changed files
with
281 additions
and
0 deletions.
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
crates/ruff_linter/resources/test/fixtures/pylint/redefined_slots_in_subclass.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
class Base: | ||
__slots__ = ("a", "b") | ||
|
||
|
||
class Subclass(Base): | ||
__slots__ = ("a", "d") # [redefined-slots-in-subclass] | ||
|
||
class Grandparent: | ||
__slots__ = ("a", "b") | ||
|
||
|
||
class Parent(Grandparent): | ||
pass | ||
|
||
|
||
class Child(Parent): | ||
__slots__ = ("c", "a") | ||
|
||
class AnotherBase: | ||
__slots__ = ["a","b","c","d"] | ||
|
||
class AnotherChild(AnotherBase): | ||
__slots__ = ["a","b","e","f"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
crates/ruff_linter/src/rules/pylint/rules/redefined_slots_in_subclass.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
use std::hash::Hash; | ||
|
||
use ruff_python_semantic::{analyze::class::any_super_class, SemanticModel}; | ||
use rustc_hash::FxHashSet; | ||
|
||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ruff_macros::{derive_message_formats, ViolationMetadata}; | ||
use ruff_python_ast::{self as ast, Expr, Stmt}; | ||
use ruff_text_size::{Ranged, TextRange}; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for a re-defined slot in a subclass. | ||
/// | ||
/// ## Why is this bad? | ||
/// If a class defines a slot also defined in a base class, the | ||
/// instance variable defined by the base class slot is inaccessible | ||
/// (except by retrieving its descriptor directly from the base class). | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// class Base: | ||
/// __slots__ = ("a", "b") | ||
/// | ||
/// | ||
/// class Subclass(Base): | ||
/// __slots__ = ("a", "d") # slot "a" redefined | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// class Base: | ||
/// __slots__ = ("a", "b") | ||
/// | ||
/// | ||
/// class Subclass(Base): | ||
/// __slots__ = "d" | ||
/// ``` | ||
#[derive(ViolationMetadata)] | ||
pub(crate) struct RedefinedSlotsInSubclass { | ||
name: String, | ||
} | ||
|
||
impl Violation for RedefinedSlotsInSubclass { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
let RedefinedSlotsInSubclass { name } = self; | ||
format!("Redefined slot '{name}' in subclass") | ||
} | ||
} | ||
|
||
// PLW0244 | ||
pub(crate) fn redefined_slots_in_subclass(checker: &mut Checker, class_def: &ast::StmtClassDef) { | ||
// Early return if this is not a subclass | ||
if class_def.bases().is_empty() { | ||
return; | ||
} | ||
|
||
let ast::StmtClassDef { body, .. } = class_def; | ||
let class_slots = slots_members(body); | ||
|
||
// If there are no slots, we're safe | ||
if class_slots.is_empty() { | ||
return; | ||
} | ||
|
||
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(), | ||
) | ||
}) | ||
.collect(); | ||
checker.diagnostics.append(&mut diagnostics); | ||
} | ||
|
||
#[derive(Clone, Debug)] | ||
struct Slot<'a> { | ||
name: &'a str, | ||
range: TextRange, | ||
} | ||
|
||
impl std::cmp::PartialEq for Slot<'_> { | ||
// We will only ever be comparing slots | ||
// within a class and with the slots of | ||
// a super class. In that context, we | ||
// want to compare names and not ranges. | ||
fn eq(&self, other: &Self) -> bool { | ||
self.name == other.name | ||
} | ||
} | ||
|
||
impl std::cmp::Eq for Slot<'_> {} | ||
|
||
impl Hash for Slot<'_> { | ||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { | ||
self.name.hash(state); | ||
} | ||
} | ||
|
||
impl Ranged for Slot<'_> { | ||
fn range(&self) -> TextRange { | ||
self.range | ||
} | ||
} | ||
|
||
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) | ||
}) | ||
} | ||
|
||
fn slots_members(body: &[Stmt]) -> FxHashSet<Slot> { | ||
let mut members = FxHashSet::default(); | ||
for stmt in body { | ||
match stmt { | ||
// Ex) `__slots__ = ("name",)` | ||
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { | ||
let [Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else { | ||
continue; | ||
}; | ||
|
||
if id == "__slots__" { | ||
members.extend(slots_attributes(value)); | ||
} | ||
} | ||
|
||
// Ex) `__slots__: Tuple[str, ...] = ("name",)` | ||
Stmt::AnnAssign(ast::StmtAnnAssign { | ||
target, | ||
value: Some(value), | ||
.. | ||
}) => { | ||
let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else { | ||
continue; | ||
}; | ||
|
||
if id == "__slots__" { | ||
members.extend(slots_attributes(value)); | ||
} | ||
} | ||
|
||
// Ex) `__slots__ += ("name",)` | ||
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => { | ||
let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else { | ||
continue; | ||
}; | ||
|
||
if id == "__slots__" { | ||
members.extend(slots_attributes(value)); | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
members | ||
} | ||
|
||
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = Slot> { | ||
// Ex) `__slots__ = ("name",)` | ||
let elts_iter = match expr { | ||
Expr::Tuple(ast::ExprTuple { elts, .. }) | ||
| Expr::List(ast::ExprList { elts, .. }) | ||
| Expr::Set(ast::ExprSet { elts, .. }) => Some(elts.iter().filter_map(|elt| match elt { | ||
Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => Some(Slot { | ||
name: value.to_str(), | ||
range: *range, | ||
}), | ||
_ => None, | ||
})), | ||
_ => None, | ||
}; | ||
|
||
// Ex) `__slots__ = {"name": ...}` | ||
let keys_iter = match expr { | ||
Expr::Dict(ast::ExprDict { .. }) => Some( | ||
expr.as_dict_expr() | ||
.unwrap() | ||
.iter_keys() | ||
.filter_map(|key| match key { | ||
Some(Expr::StringLiteral(ast::ExprStringLiteral { value, range })) => { | ||
Some(Slot { | ||
name: value.to_str(), | ||
range: *range, | ||
}) | ||
} | ||
_ => None, | ||
}), | ||
), | ||
_ => None, | ||
}; | ||
|
||
elts_iter | ||
.into_iter() | ||
.flatten() | ||
.chain(keys_iter.into_iter().flatten()) | ||
} |
34 changes: 34 additions & 0 deletions
34
.../snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/pylint/mod.rs | ||
--- | ||
redefined_slots_in_subclass.py:6:18: PLW0244 Redefined slot 'a' in subclass | ||
| | ||
5 | class Subclass(Base): | ||
6 | __slots__ = ("a", "d") # [redefined-slots-in-subclass] | ||
| ^^^ PLW0244 | ||
7 | | ||
8 | class Grandparent: | ||
| | ||
|
||
redefined_slots_in_subclass.py:17:23: PLW0244 Redefined slot 'a' in subclass | ||
| | ||
16 | class Child(Parent): | ||
17 | __slots__ = ("c", "a") | ||
| ^^^ PLW0244 | ||
18 | | ||
19 | class AnotherBase: | ||
| | ||
|
||
redefined_slots_in_subclass.py:23:18: PLW0244 Redefined slot 'a' in subclass | ||
| | ||
22 | class AnotherChild(AnotherBase): | ||
23 | __slots__ = ["a","b","e","f"] | ||
| ^^^ PLW0244 | ||
| | ||
|
||
redefined_slots_in_subclass.py:23:22: PLW0244 Redefined slot 'b' in subclass | ||
| | ||
22 | class AnotherChild(AnotherBase): | ||
23 | __slots__ = ["a","b","e","f"] | ||
| ^^^ PLW0244 | ||
| |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.