mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 10:22:24 +00:00
[flake8-slots
] Add plugin, add SLOT000
, SLOT001
and SLOT002
(#4909)
This commit is contained in:
parent
ee1f094834
commit
2bb32ee943
20 changed files with 441 additions and 11 deletions
6
crates/ruff/resources/test/fixtures/flake8_slots/SLOT000.py
vendored
Normal file
6
crates/ruff/resources/test/fixtures/flake8_slots/SLOT000.py
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
class Bad(str): # SLOT000
|
||||
pass
|
||||
|
||||
|
||||
class Good(str): # Ok
|
||||
__slots__ = ["foo"]
|
21
crates/ruff/resources/test/fixtures/flake8_slots/SLOT001.py
vendored
Normal file
21
crates/ruff/resources/test/fixtures/flake8_slots/SLOT001.py
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
class Bad(tuple): # SLOT001
|
||||
pass
|
||||
|
||||
|
||||
class Good(tuple): # Ok
|
||||
__slots__ = ("foo",)
|
||||
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class Bad(Tuple): # SLOT001
|
||||
pass
|
||||
|
||||
|
||||
class Bad(Tuple[str, int, float]): # SLOT001
|
||||
pass
|
||||
|
||||
|
||||
class Good(Tuple[str, int, float]): # OK
|
||||
__slots__ = ("foo",)
|
14
crates/ruff/resources/test/fixtures/flake8_slots/SLOT002.py
vendored
Normal file
14
crates/ruff/resources/test/fixtures/flake8_slots/SLOT002.py
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
from collections import namedtuple
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Bad(namedtuple("foo", ["str", "int"])): # SLOT002
|
||||
pass
|
||||
|
||||
|
||||
class Good(namedtuple("foo", ["str", "int"])): # OK
|
||||
__slots__ = ("foo",)
|
||||
|
||||
|
||||
class Good(NamedTuple): # Ok
|
||||
pass
|
|
@ -48,9 +48,9 @@ use crate::rules::{
|
|||
flake8_debugger, flake8_django, flake8_errmsg, flake8_future_annotations, flake8_gettext,
|
||||
flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format, flake8_pie,
|
||||
flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_self,
|
||||
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
|
||||
flake8_use_pathlib, flynt, mccabe, numpy, pandas_vet, pep8_naming, pycodestyle, pydocstyle,
|
||||
pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
|
||||
flake8_simplify, flake8_slots, flake8_tidy_imports, flake8_type_checking,
|
||||
flake8_unused_arguments, flake8_use_pathlib, flynt, mccabe, numpy, pandas_vet, pep8_naming,
|
||||
pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
|
||||
};
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::settings::{flags, Settings};
|
||||
|
@ -682,14 +682,16 @@ where
|
|||
pylint::rules::return_in_init(self, stmt);
|
||||
}
|
||||
}
|
||||
Stmt::ClassDef(ast::StmtClassDef {
|
||||
name,
|
||||
bases,
|
||||
keywords,
|
||||
decorator_list,
|
||||
body,
|
||||
range: _,
|
||||
}) => {
|
||||
Stmt::ClassDef(
|
||||
class_def @ ast::StmtClassDef {
|
||||
name,
|
||||
bases,
|
||||
keywords,
|
||||
decorator_list,
|
||||
body,
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
if self.enabled(Rule::DjangoNullableModelStringField) {
|
||||
self.diagnostics
|
||||
.extend(flake8_django::rules::nullable_model_string_field(
|
||||
|
@ -818,6 +820,18 @@ where
|
|||
if self.enabled(Rule::DuplicateBases) {
|
||||
pylint::rules::duplicate_bases(self, name, bases);
|
||||
}
|
||||
|
||||
if self.enabled(Rule::NoSlotsInStrSubclass) {
|
||||
flake8_slots::rules::no_slots_in_str_subclass(self, stmt, class_def);
|
||||
}
|
||||
|
||||
if self.enabled(Rule::NoSlotsInTupleSubclass) {
|
||||
flake8_slots::rules::no_slots_in_tuple_subclass(self, stmt, class_def);
|
||||
}
|
||||
|
||||
if self.enabled(Rule::NoSlotsInNamedtupleSubclass) {
|
||||
flake8_slots::rules::no_slots_in_namedtuple_subclass(self, stmt, class_def);
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if self.enabled(Rule::MultipleImportsOnOneLine) {
|
||||
|
|
|
@ -782,6 +782,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8Fixme, "003") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsXxx),
|
||||
(Flake8Fixme, "004") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsHack),
|
||||
|
||||
// flake8-slots
|
||||
(Flake8Slots, "000") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInStrSubclass),
|
||||
(Flake8Slots, "001") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInTupleSubclass),
|
||||
(Flake8Slots, "002") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -137,6 +137,9 @@ pub enum Linter {
|
|||
/// [flake8-self](https://pypi.org/project/flake8-self/)
|
||||
#[prefix = "SLF"]
|
||||
Flake8Self,
|
||||
/// [flake8-slots](https://pypi.org/project/flake8-slots/)
|
||||
#[prefix = "SLOT"]
|
||||
Flake8Slots,
|
||||
/// [flake8-simplify](https://pypi.org/project/flake8-simplify/)
|
||||
#[prefix = "SIM"]
|
||||
Flake8Simplify,
|
||||
|
|
27
crates/ruff/src/rules/flake8_slots/mod.rs
Normal file
27
crates/ruff/src/rules/flake8_slots/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
//! Rules from [flake8-slots](https://pypi.org/project/flake8-slots/).
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::NoSlotsInStrSubclass, Path::new("SLOT000.py"))]
|
||||
#[test_case(Rule::NoSlotsInTupleSubclass, Path::new("SLOT001.py"))]
|
||||
#[test_case(Rule::NoSlotsInNamedtupleSubclass, Path::new("SLOT002.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_slots").join(path).as_path(),
|
||||
&settings::Settings::for_rule(rule_code),
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
27
crates/ruff/src/rules/flake8_slots/rules/helpers.rs
Normal file
27
crates/ruff/src/rules/flake8_slots/rules/helpers.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use rustpython_parser::ast::{self, Expr, Stmt};
|
||||
|
||||
/// Return `true` if the given body contains a `__slots__` assignment.
|
||||
pub(super) fn has_slots(body: &[Stmt]) -> bool {
|
||||
for stmt in body {
|
||||
match stmt {
|
||||
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
|
||||
for target in targets {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target {
|
||||
if id.as_str() == "__slots__" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
if id.as_str() == "__slots__" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
10
crates/ruff/src/rules/flake8_slots/rules/mod.rs
Normal file
10
crates/ruff/src/rules/flake8_slots/rules/mod.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
pub(crate) use no_slots_in_namedtuple_subclass::{
|
||||
no_slots_in_namedtuple_subclass, NoSlotsInNamedtupleSubclass,
|
||||
};
|
||||
pub(crate) use no_slots_in_str_subclass::{no_slots_in_str_subclass, NoSlotsInStrSubclass};
|
||||
pub(crate) use no_slots_in_tuple_subclass::{no_slots_in_tuple_subclass, NoSlotsInTupleSubclass};
|
||||
|
||||
mod helpers;
|
||||
mod no_slots_in_namedtuple_subclass;
|
||||
mod no_slots_in_str_subclass;
|
||||
mod no_slots_in_tuple_subclass;
|
|
@ -0,0 +1,84 @@
|
|||
use rustpython_parser::ast;
|
||||
use rustpython_parser::ast::{Expr, StmtClassDef};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::identifier_range;
|
||||
use ruff_python_ast::prelude::Stmt;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_slots::rules::helpers::has_slots;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for subclasses of `collections.namedtuple` that lack a `__slots__`
|
||||
/// definition.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In Python, the `__slots__` attribute allows you to explicitly define the
|
||||
/// attributes (instance variables) that a class can have. By default, Python
|
||||
/// uses a dictionary to store an object's attributes, which incurs some memory
|
||||
/// overhead. However, when `__slots__` is defined, Python uses a more compact
|
||||
/// internal structure to store the object's attributes, resulting in memory
|
||||
/// savings.
|
||||
///
|
||||
/// Subclasses of `namedtuple` inherit all the attributes and methods of the
|
||||
/// built-in `namedtuple` class. Since tuples are typically immutable, they
|
||||
/// don't require additional attributes beyond what the `namedtuple` class
|
||||
/// provides. Defining `__slots__` for subclasses of `namedtuple` prevents the
|
||||
/// creation of a dictionary for each instance, reducing memory consumption.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from collections import namedtuple
|
||||
///
|
||||
///
|
||||
/// class Foo(namedtuple("foo", ["str", "int"])):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from collections import namedtuple
|
||||
///
|
||||
///
|
||||
/// class Foo(namedtuple("foo", ["str", "int"])):
|
||||
/// __slots__ = ()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots)
|
||||
#[violation]
|
||||
pub struct NoSlotsInNamedtupleSubclass;
|
||||
|
||||
impl Violation for NoSlotsInNamedtupleSubclass {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Subclasses of `collections.namedtuple()` should define `__slots__`")
|
||||
}
|
||||
}
|
||||
|
||||
/// SLOT002
|
||||
pub(crate) fn no_slots_in_namedtuple_subclass(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
class: &StmtClassDef,
|
||||
) {
|
||||
if class.bases.iter().any(|base| {
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = base else {
|
||||
return false;
|
||||
};
|
||||
checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(func)
|
||||
.map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["collections", "namedtuple"])
|
||||
})
|
||||
}) {
|
||||
if !has_slots(&class.body) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NoSlotsInNamedtupleSubclass,
|
||||
identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
use rustpython_parser::ast::{Stmt, StmtClassDef};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::identifier_range;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_slots::rules::helpers::has_slots;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for subclasses of `str` that lack a `__slots__` definition.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In Python, the `__slots__` attribute allows you to explicitly define the
|
||||
/// attributes (instance variables) that a class can have. By default, Python
|
||||
/// uses a dictionary to store an object's attributes, which incurs some memory
|
||||
/// overhead. However, when `__slots__` is defined, Python uses a more compact
|
||||
/// internal structure to store the object's attributes, resulting in memory
|
||||
/// savings.
|
||||
///
|
||||
/// Subclasses of `str` inherit all the attributes and methods of the built-in
|
||||
/// `str` class. Since strings are typically immutable, they don't require
|
||||
/// additional attributes beyond what the `str` class provides. Defining
|
||||
/// `__slots__` for subclasses of `str` prevents the creation of a dictionary
|
||||
/// for each instance, reducing memory consumption.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo(str):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Foo(str):
|
||||
/// __slots__ = ()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots)
|
||||
#[violation]
|
||||
pub struct NoSlotsInStrSubclass;
|
||||
|
||||
impl Violation for NoSlotsInStrSubclass {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Subclasses of `str` should define `__slots__`")
|
||||
}
|
||||
}
|
||||
|
||||
/// SLOT000
|
||||
pub(crate) fn no_slots_in_str_subclass(checker: &mut Checker, stmt: &Stmt, class: &StmtClassDef) {
|
||||
if class.bases.iter().any(|base| {
|
||||
checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(base)
|
||||
.map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["" | "builtins", "str"])
|
||||
})
|
||||
}) {
|
||||
if !has_slots(&class.body) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NoSlotsInStrSubclass,
|
||||
identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
use rustpython_parser::ast::{Stmt, StmtClassDef};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::{identifier_range, map_subscript};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_slots::rules::helpers::has_slots;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for subclasses of `tuple` that lack a `__slots__` definition.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In Python, the `__slots__` attribute allows you to explicitly define the
|
||||
/// attributes (instance variables) that a class can have. By default, Python
|
||||
/// uses a dictionary to store an object's attributes, which incurs some memory
|
||||
/// overhead. However, when `__slots__` is defined, Python uses a more compact
|
||||
/// internal structure to store the object's attributes, resulting in memory
|
||||
/// savings.
|
||||
///
|
||||
/// Subclasses of `tuple` inherit all the attributes and methods of the
|
||||
/// built-in `tuple` class. Since tuples are typically immutable, they don't
|
||||
/// require additional attributes beyond what the `tuple` class provides.
|
||||
/// Defining `__slots__` for subclasses of `tuple` prevents the creation of a
|
||||
/// dictionary for each instance, reducing memory consumption.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo(tuple):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Foo(tuple):
|
||||
/// __slots__ = ()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `__slots__`](https://docs.python.org/3.7/reference/datamodel.html#slots)
|
||||
#[violation]
|
||||
pub struct NoSlotsInTupleSubclass;
|
||||
|
||||
impl Violation for NoSlotsInTupleSubclass {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Subclasses of `tuple` should define `__slots__`")
|
||||
}
|
||||
}
|
||||
|
||||
/// SLOT001
|
||||
pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, class: &StmtClassDef) {
|
||||
if class.bases.iter().any(|base| {
|
||||
checker
|
||||
.semantic_model()
|
||||
.resolve_call_path(map_subscript(base))
|
||||
.map_or(false, |call_path| {
|
||||
matches!(call_path.as_slice(), ["" | "builtins", "tuple"])
|
||||
|| checker
|
||||
.semantic_model()
|
||||
.match_typing_call_path(&call_path, "Tuple")
|
||||
})
|
||||
}) {
|
||||
if !has_slots(&class.body) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NoSlotsInTupleSubclass,
|
||||
identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_slots/mod.rs
|
||||
---
|
||||
SLOT000.py:1:7: SLOT000 Subclasses of `str` should define `__slots__`
|
||||
|
|
||||
1 | class Bad(str): # SLOT000
|
||||
| ^^^ SLOT000
|
||||
2 | pass
|
||||
|
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_slots/mod.rs
|
||||
---
|
||||
SLOT001.py:1:7: SLOT001 Subclasses of `tuple` should define `__slots__`
|
||||
|
|
||||
1 | class Bad(tuple): # SLOT001
|
||||
| ^^^ SLOT001
|
||||
2 | pass
|
||||
|
|
||||
|
||||
SLOT001.py:12:7: SLOT001 Subclasses of `tuple` should define `__slots__`
|
||||
|
|
||||
12 | class Bad(Tuple): # SLOT001
|
||||
| ^^^ SLOT001
|
||||
13 | pass
|
||||
|
|
||||
|
||||
SLOT001.py:16:7: SLOT001 Subclasses of `tuple` should define `__slots__`
|
||||
|
|
||||
16 | class Bad(Tuple[str, int, float]): # SLOT001
|
||||
| ^^^ SLOT001
|
||||
17 | pass
|
||||
|
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_slots/mod.rs
|
||||
---
|
||||
SLOT002.py:5:7: SLOT002 Subclasses of `collections.namedtuple()` should define `__slots__`
|
||||
|
|
||||
5 | class Bad(namedtuple("foo", ["str", "int"])): # SLOT002
|
||||
| ^^^ SLOT002
|
||||
6 | pass
|
||||
|
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ pub mod flake8_raise;
|
|||
pub mod flake8_return;
|
||||
pub mod flake8_self;
|
||||
pub mod flake8_simplify;
|
||||
pub mod flake8_slots;
|
||||
pub mod flake8_tidy_imports;
|
||||
pub mod flake8_todos;
|
||||
pub mod flake8_type_checking;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue