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 11f2782b77ac25..a992d5f3e5bf86 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,11 @@ 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 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 fdbf87efcc74d2..3f50a261631e35 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,18 @@ 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; - }; + let ast::ExprCall { func, .. } = base.as_call_expr()?; + let qualified_name = semantic.resolve_qualified_name(func)?; match qualified_name.segments() { - ["collections", "namedtuple"] => return Some(NamedTupleKind::Collections), - ["typing", "NamedTuple"] => return Some(NamedTupleKind::Typing), - _ => continue, + ["collections", "namedtuple"] => kind = kind.or(Some(NamedTupleKind::Collections)), + ["typing", "NamedTuple"] => kind = kind.or(Some(NamedTupleKind::Typing)), + _ => 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 d7670abd56fe8c..1ce08577ec8284 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,23 @@ --- 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 |