diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B027.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B027.py index d7d563c365..34690ef536 100644 --- a/crates/ruff/resources/test/fixtures/flake8_bugbear/B027.py +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B027.py @@ -4,7 +4,12 @@ B027 - on lines 13, 16, 19, 23 """ import abc from abc import ABC -from abc import abstractmethod, abstractproperty +from abc import ( + abstractmethod, + abstractproperty, + abstractclassmethod, + abstractstaticmethod, +) from abc import abstractmethod as notabstract from abc import abstractproperty as notabstract_property @@ -55,6 +60,22 @@ class AbstractClass(ABC): def abstract_6(self): ... + @abstractclassmethod + def abstract_7(self): + pass + + @abc.abstractclassmethod + def abstract_8(self): + ... + + @abstractstaticmethod + def abstract_9(self): + pass + + @abc.abstractstaticmethod + def abstract_10(self): + ... + def body_1(self): print("foo") ... diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B027_B027.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B027_B027.py.snap index 31830c9e92..364d496179 100644 --- a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B027_B027.py.snap +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B027_B027.py.snap @@ -1,96 +1,96 @@ --- source: crates/ruff/src/rules/flake8_bugbear/mod.rs --- -B027.py:13:5: B027 [*] `AbstractClass.empty_1` is an empty method in an abstract base class, but has no abstract decorator +B027.py:18:5: B027 [*] `AbstractClass.empty_1` is an empty method in an abstract base class, but has no abstract decorator | -13 | class AbstractClass(ABC): -14 | def empty_1(self): # error +18 | class AbstractClass(ABC): +19 | def empty_1(self): # error | _____^ -15 | | ... +20 | | ... | |___________^ B027 -16 | -17 | def empty_2(self): # error +21 | +22 | def empty_2(self): # error | = help: Add the `@abstractmethod` decorator ℹ Suggested fix -10 10 | -11 11 | -12 12 | class AbstractClass(ABC): - 13 |+ @notabstract -13 14 | def empty_1(self): # error -14 15 | ... -15 16 | - -B027.py:16:5: B027 [*] `AbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract decorator - | -16 | ... -17 | -18 | def empty_2(self): # error - | _____^ -19 | | pass - | |____________^ B027 -20 | -21 | def empty_3(self): # error - | - = help: Add the `@abstractmethod` decorator - -ℹ Suggested fix -13 13 | def empty_1(self): # error -14 14 | ... 15 15 | - 16 |+ @notabstract -16 17 | def empty_2(self): # error -17 18 | pass -18 19 | +16 16 | +17 17 | class AbstractClass(ABC): + 18 |+ @notabstract +18 19 | def empty_1(self): # error +19 20 | ... +20 21 | -B027.py:19:5: B027 [*] `AbstractClass.empty_3` is an empty method in an abstract base class, but has no abstract decorator +B027.py:21:5: B027 [*] `AbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract decorator | -19 | pass -20 | -21 | def empty_3(self): # error +21 | ... +22 | +23 | def empty_2(self): # error | _____^ -22 | | """docstring""" -23 | | ... - | |___________^ B027 -24 | -25 | def empty_4(self): # error - | - = help: Add the `@abstractmethod` decorator - -ℹ Suggested fix -16 16 | def empty_2(self): # error -17 17 | pass -18 18 | - 19 |+ @notabstract -19 20 | def empty_3(self): # error -20 21 | """docstring""" -21 22 | ... - -B027.py:23:5: B027 [*] `AbstractClass.empty_4` is an empty method in an abstract base class, but has no abstract decorator - | -23 | ... -24 | -25 | def empty_4(self): # error - | _____^ -26 | | """multiple ellipsis/pass""" -27 | | ... -28 | | pass -29 | | ... -30 | | pass +24 | | pass | |____________^ B027 -31 | -32 | @notabstract +25 | +26 | def empty_3(self): # error | = help: Add the `@abstractmethod` decorator ℹ Suggested fix -20 20 | """docstring""" -21 21 | ... -22 22 | - 23 |+ @notabstract -23 24 | def empty_4(self): # error -24 25 | """multiple ellipsis/pass""" -25 26 | ... +18 18 | def empty_1(self): # error +19 19 | ... +20 20 | + 21 |+ @notabstract +21 22 | def empty_2(self): # error +22 23 | pass +23 24 | + +B027.py:24:5: B027 [*] `AbstractClass.empty_3` is an empty method in an abstract base class, but has no abstract decorator + | +24 | pass +25 | +26 | def empty_3(self): # error + | _____^ +27 | | """docstring""" +28 | | ... + | |___________^ B027 +29 | +30 | def empty_4(self): # error + | + = help: Add the `@abstractmethod` decorator + +ℹ Suggested fix +21 21 | def empty_2(self): # error +22 22 | pass +23 23 | + 24 |+ @notabstract +24 25 | def empty_3(self): # error +25 26 | """docstring""" +26 27 | ... + +B027.py:28:5: B027 [*] `AbstractClass.empty_4` is an empty method in an abstract base class, but has no abstract decorator + | +28 | ... +29 | +30 | def empty_4(self): # error + | _____^ +31 | | """multiple ellipsis/pass""" +32 | | ... +33 | | pass +34 | | ... +35 | | pass + | |____________^ B027 +36 | +37 | @notabstract + | + = help: Add the `@abstractmethod` decorator + +ℹ Suggested fix +25 25 | """docstring""" +26 26 | ... +27 27 | + 28 |+ @notabstract +28 29 | def empty_4(self): # error +29 30 | """multiple ellipsis/pass""" +30 31 | ... diff --git a/crates/ruff_python_semantic/src/analyze/visibility.rs b/crates/ruff_python_semantic/src/analyze/visibility.rs index 3d707de69f..83d55bed2b 100644 --- a/crates/ruff_python_semantic/src/analyze/visibility.rs +++ b/crates/ruff_python_semantic/src/analyze/visibility.rs @@ -60,13 +60,21 @@ pub fn is_override(ctx: &Context, decorator_list: &[Expr]) -> bool { .any(|expr| ctx.match_typing_expr(map_callable(expr), "override")) } -/// Returns `true` if a function definition is an `@abstractmethod`. +/// Returns `true` if a function definition is an abstract method based on its decorators. pub fn is_abstract(ctx: &Context, decorator_list: &[Expr]) -> bool { decorator_list.iter().any(|expr| { ctx.resolve_call_path(map_callable(expr)) .map_or(false, |call_path| { - call_path.as_slice() == ["abc", "abstractmethod"] - || call_path.as_slice() == ["abc", "abstractproperty"] + matches!( + call_path.as_slice(), + [ + "abc", + "abstractmethod" + | "abstractclassmethod" + | "abstractstaticmethod" + | "abstractproperty" + ] + ) }) }) }