[flake8-slots] Add plugin, add SLOT000, SLOT001 and SLOT002 (#4909)

This commit is contained in:
qdegraaf 2023-06-09 06:14:16 +02:00 committed by GitHub
parent ee1f094834
commit 2bb32ee943
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 441 additions and 11 deletions

View file

@ -0,0 +1,6 @@
class Bad(str): # SLOT000
pass
class Good(str): # Ok
__slots__ = ["foo"]

View 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",)

View 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

View file

@ -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) {

View file

@ -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,
})
}

View file

@ -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,

View 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(())
}
}

View 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
}

View 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;

View file

@ -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),
));
}
}
}

View file

@ -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),
));
}
}
}

View file

@ -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),
));
}
}
}

View file

@ -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
|

View file

@ -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
|

View file

@ -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
|

View file

@ -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;