diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_slots/SLOT002.py b/crates/ruff_linter/resources/test/fixtures/flake8_slots/SLOT002.py index 11f2782b77ac2..149ba7332bbeb 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_slots/SLOT002.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_slots/SLOT002.py @@ -1,4 +1,5 @@ from collections import namedtuple +from enum import Enum from typing import NamedTuple @@ -20,3 +21,15 @@ class Good(namedtuple("foo", ["str", "int"])): # OK class Good(NamedTuple): # Ok pass + + +class Good(namedtuple("foo", ["str", "int"]), Enum): + pass + + +class UnusualButStillBad(namedtuple("foo", ["str", "int"]), NamedTuple("foo", [("x", int, "y", int)])): + pass + + +class UnusualButStillBad(namedtuple("foo", ["str", "int"]), object): + pass diff --git a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs index fdbf87efcc74d..dd84fd04dcfe5 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs +++ b/crates/ruff_linter/src/rules/flake8_slots/rules/no_slots_in_namedtuple_subclass.rs @@ -92,23 +92,25 @@ pub(crate) fn no_slots_in_namedtuple_subclass( } } -/// If the class has a call-based namedtuple in its bases, -/// return the kind of namedtuple it is -/// (either `collections.namedtuple()`, or `typing.NamedTuple()`). -/// Else, return `None`. +/// If the class's bases consist solely of named tuples, return the kind of named tuple +/// (either `collections.namedtuple()`, or `typing.NamedTuple()`). Otherwise, return `None`. fn namedtuple_base(bases: &[Expr], semantic: &SemanticModel) -> Option { + let mut kind = None; for base in bases { - let Expr::Call(ast::ExprCall { func, .. }) = base else { - continue; - }; - let Some(qualified_name) = semantic.resolve_qualified_name(func) else { - continue; - }; - match qualified_name.segments() { - ["collections", "namedtuple"] => return Some(NamedTupleKind::Collections), - ["typing", "NamedTuple"] => return Some(NamedTupleKind::Typing), - _ => continue, + if let Expr::Call(ast::ExprCall { func, .. }) = base { + // Ex) `collections.namedtuple()` + let qualified_name = semantic.resolve_qualified_name(func)?; + match qualified_name.segments() { + ["collections", "namedtuple"] => kind = kind.or(Some(NamedTupleKind::Collections)), + ["typing", "NamedTuple"] => kind = kind.or(Some(NamedTupleKind::Typing)), + // Ex) `enum.Enum` + _ => return None, + } + } else if !semantic.match_builtin_expr(base, "object") { + // Allow inheriting from `object`. + + return None; } } - None + kind } diff --git a/crates/ruff_linter/src/rules/flake8_slots/snapshots/ruff_linter__rules__flake8_slots__tests__SLOT002_SLOT002.py.snap b/crates/ruff_linter/src/rules/flake8_slots/snapshots/ruff_linter__rules__flake8_slots__tests__SLOT002_SLOT002.py.snap index d7670abd56fe8..d59497d98b52b 100644 --- a/crates/ruff_linter/src/rules/flake8_slots/snapshots/ruff_linter__rules__flake8_slots__tests__SLOT002_SLOT002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_slots/snapshots/ruff_linter__rules__flake8_slots__tests__SLOT002_SLOT002.py.snap @@ -1,16 +1,30 @@ --- source: crates/ruff_linter/src/rules/flake8_slots/mod.rs --- -SLOT002.py:5:7: SLOT002 Subclasses of `collections.namedtuple()` should define `__slots__` +SLOT002.py:6:7: SLOT002 Subclasses of `collections.namedtuple()` should define `__slots__` | -5 | class Bad(namedtuple("foo", ["str", "int"])): # SLOT002 +6 | class Bad(namedtuple("foo", ["str", "int"])): # SLOT002 | ^^^ SLOT002 -6 | pass +7 | pass | -SLOT002.py:9:7: SLOT002 Subclasses of call-based `typing.NamedTuple()` should define `__slots__` +SLOT002.py:10:7: SLOT002 Subclasses of call-based `typing.NamedTuple()` should define `__slots__` | - 9 | class UnusualButStillBad(NamedTuple("foo", [("x", int, "y", int)])): # SLOT002 +10 | class UnusualButStillBad(NamedTuple("foo", [("x", int, "y", int)])): # SLOT002 | ^^^^^^^^^^^^^^^^^^ SLOT002 -10 | pass +11 | pass + | + +SLOT002.py:30:7: SLOT002 Subclasses of `collections.namedtuple()` should define `__slots__` + | +30 | class UnusualButStillBad(namedtuple("foo", ["str", "int"]), NamedTuple("foo", [("x", int, "y", int)])): + | ^^^^^^^^^^^^^^^^^^ SLOT002 +31 | pass + | + +SLOT002.py:34:7: SLOT002 Subclasses of `collections.namedtuple()` should define `__slots__` + | +34 | class UnusualButStillBad(namedtuple("foo", ["str", "int"]), object): + | ^^^^^^^^^^^^^^^^^^ SLOT002 +35 | pass |