From 2bb32ee943ef8ea4fa34d7b1f309b1eee6b1cfc7 Mon Sep 17 00:00:00 2001 From: qdegraaf <34540841+qdegraaf@users.noreply.github.com> Date: Fri, 9 Jun 2023 06:14:16 +0200 Subject: [PATCH] [`flake8-slots`] Add plugin, add `SLOT000`, `SLOT001` and `SLOT002` (#4909) --- LICENSE | 23 +++++ README.md | 1 + .../test/fixtures/flake8_slots/SLOT000.py | 6 ++ .../test/fixtures/flake8_slots/SLOT001.py | 21 +++++ .../test/fixtures/flake8_slots/SLOT002.py | 14 ++++ crates/ruff/src/checkers/ast/mod.rs | 36 +++++--- crates/ruff/src/codes.rs | 5 ++ crates/ruff/src/registry.rs | 3 + crates/ruff/src/rules/flake8_slots/mod.rs | 27 ++++++ .../src/rules/flake8_slots/rules/helpers.rs | 27 ++++++ .../ruff/src/rules/flake8_slots/rules/mod.rs | 10 +++ .../rules/no_slots_in_namedtuple_subclass.rs | 84 +++++++++++++++++++ .../rules/no_slots_in_str_subclass.rs | 68 +++++++++++++++ .../rules/no_slots_in_tuple_subclass.rs | 71 ++++++++++++++++ ...ake8_slots__tests__SLOT000_SLOT000.py.snap | 11 +++ ...ake8_slots__tests__SLOT001_SLOT001.py.snap | 25 ++++++ ...ake8_slots__tests__SLOT002_SLOT002.py.snap | 11 +++ crates/ruff/src/rules/mod.rs | 1 + docs/faq.md | 2 + ruff.schema.json | 6 ++ 20 files changed, 441 insertions(+), 11 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/flake8_slots/SLOT000.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_slots/SLOT001.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_slots/SLOT002.py create mode 100644 crates/ruff/src/rules/flake8_slots/mod.rs create mode 100644 crates/ruff/src/rules/flake8_slots/rules/helpers.rs create mode 100644 crates/ruff/src/rules/flake8_slots/rules/mod.rs create mode 100644 crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs create mode 100644 crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs create mode 100644 crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs create mode 100644 crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT000_SLOT000.py.snap create mode 100644 crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT001_SLOT001.py.snap create mode 100644 crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT002_SLOT002.py.snap diff --git a/LICENSE b/LICENSE index 534e335742..932ce42b6b 100644 --- a/LICENSE +++ b/LICENSE @@ -354,6 +354,29 @@ are: SOFTWARE. """ +- flake8-slots, licensed as follows: + """ + Copyright (c) 2021 Dominic Davis-Foster + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + OR OTHER DEALINGS IN THE SOFTWARE. + """ + - flake8-todos, licensed as follows: """ Copyright (c) 2019 EclecticIQ. All rights reserved. diff --git a/README.md b/README.md index 51e79568eb..b23d42d610 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ quality tools, including: - [flake8-return](https://pypi.org/project/flake8-return/) - [flake8-self](https://pypi.org/project/flake8-self/) - [flake8-simplify](https://pypi.org/project/flake8-simplify/) +- [flake8-slots](https://pypi.org/project/flake8-slots/) - [flake8-super](https://pypi.org/project/flake8-super/) - [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/) - [flake8-todos](https://pypi.org/project/flake8-todos/) diff --git a/crates/ruff/resources/test/fixtures/flake8_slots/SLOT000.py b/crates/ruff/resources/test/fixtures/flake8_slots/SLOT000.py new file mode 100644 index 0000000000..5436ad13ca --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_slots/SLOT000.py @@ -0,0 +1,6 @@ +class Bad(str): # SLOT000 + pass + + +class Good(str): # Ok + __slots__ = ["foo"] diff --git a/crates/ruff/resources/test/fixtures/flake8_slots/SLOT001.py b/crates/ruff/resources/test/fixtures/flake8_slots/SLOT001.py new file mode 100644 index 0000000000..d0533df9d7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_slots/SLOT001.py @@ -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",) diff --git a/crates/ruff/resources/test/fixtures/flake8_slots/SLOT002.py b/crates/ruff/resources/test/fixtures/flake8_slots/SLOT002.py new file mode 100644 index 0000000000..59a9ba3f55 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_slots/SLOT002.py @@ -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 diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 8d5a7f6509..bcbc31f923 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -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) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 9f5306301f..9e4f05bd97 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -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, }) } diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index c57390c059..5100ec9384 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -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, diff --git a/crates/ruff/src/rules/flake8_slots/mod.rs b/crates/ruff/src/rules/flake8_slots/mod.rs new file mode 100644 index 0000000000..4367fbb5f1 --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/mod.rs @@ -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(()) + } +} diff --git a/crates/ruff/src/rules/flake8_slots/rules/helpers.rs b/crates/ruff/src/rules/flake8_slots/rules/helpers.rs new file mode 100644 index 0000000000..7d0035d15f --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/rules/helpers.rs @@ -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 +} diff --git a/crates/ruff/src/rules/flake8_slots/rules/mod.rs b/crates/ruff/src/rules/flake8_slots/rules/mod.rs new file mode 100644 index 0000000000..d7a91ce438 --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/rules/mod.rs @@ -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; diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs new file mode 100644 index 0000000000..2928f8b9af --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -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), + )); + } + } +} diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs new file mode 100644 index 0000000000..76580bb931 --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_str_subclass.rs @@ -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), + )); + } + } +} diff --git a/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs new file mode 100644 index 0000000000..3dea2b0057 --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/rules/no_slots_in_tuple_subclass.rs @@ -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), + )); + } + } +} diff --git a/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT000_SLOT000.py.snap b/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT000_SLOT000.py.snap new file mode 100644 index 0000000000..70807294d3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT000_SLOT000.py.snap @@ -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 + | + + diff --git a/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT001_SLOT001.py.snap b/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT001_SLOT001.py.snap new file mode 100644 index 0000000000..f0a8c0f9e8 --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT001_SLOT001.py.snap @@ -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 + | + + diff --git a/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT002_SLOT002.py.snap b/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT002_SLOT002.py.snap new file mode 100644 index 0000000000..56ed809c2a --- /dev/null +++ b/crates/ruff/src/rules/flake8_slots/snapshots/ruff__rules__flake8_slots__tests__SLOT002_SLOT002.py.snap @@ -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 + | + + diff --git a/crates/ruff/src/rules/mod.rs b/crates/ruff/src/rules/mod.rs index 172631145c..23083cb2ab 100644 --- a/crates/ruff/src/rules/mod.rs +++ b/crates/ruff/src/rules/mod.rs @@ -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; diff --git a/docs/faq.md b/docs/faq.md index f05c58a27e..3ee9baca50 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -62,6 +62,7 @@ natively, including: - [flake8-return](https://pypi.org/project/flake8-return/) - [flake8-self](https://pypi.org/project/flake8-self/) - [flake8-simplify](https://pypi.org/project/flake8-simplify/) +- [flake8-slots](https://pypi.org/project/flake8-slots/) - [flake8-super](https://pypi.org/project/flake8-super/) - [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/) - [flake8-todos](https://pypi.org/project/flake8-todos/) @@ -162,6 +163,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [flake8-return](https://pypi.org/project/flake8-return/) - [flake8-self](https://pypi.org/project/flake8-self/) - [flake8-simplify](https://pypi.org/project/flake8-simplify/) +- [flake8-slots](https://pypi.org/project/flake8-slots/) - [flake8-super](https://pypi.org/project/flake8-super/) - [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/) - [flake8-todos](https://pypi.org/project/flake8-todos/) diff --git a/ruff.schema.json b/ruff.schema.json index 4eeb099348..69702b9f17 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2417,6 +2417,12 @@ "SLF0", "SLF00", "SLF001", + "SLOT", + "SLOT0", + "SLOT00", + "SLOT000", + "SLOT001", + "SLOT002", "T", "T1", "T10",