mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 13:33:50 +00:00
[pylint
] Implement redefined-slots-in-subclass
(W0244
) (#9640)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
## 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 #970 --------- Co-authored-by: Akira Noda <akira.noda@onecareer.com> Co-authored-by: dylwil3 <dylwil3@gmail.com>
This commit is contained in:
parent
4fdf8af747
commit
5cdac2533e
8 changed files with 281 additions and 0 deletions
23
crates/ruff_linter/resources/test/fixtures/pylint/redefined_slots_in_subclass.py
vendored
Normal file
23
crates/ruff_linter/resources/test/fixtures/pylint/redefined_slots_in_subclass.py
vendored
Normal file
|
@ -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"]
|
|
@ -424,6 +424,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
|||
if checker.enabled(Rule::ClassAsDataStructure) {
|
||||
flake8_bugbear::rules::class_as_data_structure(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::RedefinedSlotsInSubclass) {
|
||||
pylint::rules::redefined_slots_in_subclass(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::TooManyPublicMethods) {
|
||||
pylint::rules::too_many_public_methods(
|
||||
checker,
|
||||
|
|
|
@ -279,6 +279,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Pylint, "W0131") => (RuleGroup::Stable, rules::pylint::rules::NamedExprWithoutContext),
|
||||
(Pylint, "W0133") => (RuleGroup::Stable, rules::pylint::rules::UselessExceptionStatement),
|
||||
(Pylint, "W0211") => (RuleGroup::Stable, rules::pylint::rules::BadStaticmethodArgument),
|
||||
(Pylint, "W0244") => (RuleGroup::Preview, rules::pylint::rules::RedefinedSlotsInSubclass),
|
||||
(Pylint, "W0245") => (RuleGroup::Stable, rules::pylint::rules::SuperWithoutBrackets),
|
||||
(Pylint, "W0406") => (RuleGroup::Stable, rules::pylint::rules::ImportSelf),
|
||||
(Pylint, "W0602") => (RuleGroup::Stable, rules::pylint::rules::GlobalVariableNotAssigned),
|
||||
|
|
|
@ -118,6 +118,10 @@ mod tests {
|
|||
Path::new("named_expr_without_context.py")
|
||||
)]
|
||||
#[test_case(Rule::NonlocalAndGlobal, Path::new("nonlocal_and_global.py"))]
|
||||
#[test_case(
|
||||
Rule::RedefinedSlotsInSubclass,
|
||||
Path::new("redefined_slots_in_subclass.py")
|
||||
)]
|
||||
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"))]
|
||||
#[test_case(Rule::NonSlotAssignment, Path::new("non_slot_assignment.py"))]
|
||||
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"))]
|
||||
|
|
|
@ -63,6 +63,7 @@ pub(crate) use property_with_parameters::*;
|
|||
pub(crate) use redeclared_assigned_name::*;
|
||||
pub(crate) use redefined_argument_from_local::*;
|
||||
pub(crate) use redefined_loop_name::*;
|
||||
pub(crate) use redefined_slots_in_subclass::*;
|
||||
pub(crate) use repeated_equality_comparison::*;
|
||||
pub(crate) use repeated_isinstance_calls::*;
|
||||
pub(crate) use repeated_keyword_argument::*;
|
||||
|
@ -171,6 +172,7 @@ mod property_with_parameters;
|
|||
mod redeclared_assigned_name;
|
||||
mod redefined_argument_from_local;
|
||||
mod redefined_loop_name;
|
||||
mod redefined_slots_in_subclass;
|
||||
mod repeated_equality_comparison;
|
||||
mod repeated_isinstance_calls;
|
||||
mod repeated_keyword_argument;
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
|
|
1
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -3669,6 +3669,7 @@
|
|||
"PLW021",
|
||||
"PLW0211",
|
||||
"PLW024",
|
||||
"PLW0244",
|
||||
"PLW0245",
|
||||
"PLW04",
|
||||
"PLW040",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue