mirror of https://github.com/astral-sh/ruff
Update B027 to support autofixing (#4178)
This commit is contained in:
parent
494e807315
commit
d78287540d
|
|
@ -0,0 +1,39 @@
|
|||
"""
|
||||
Should emit:
|
||||
B027 - on lines 13, 16, 19, 23
|
||||
"""
|
||||
from abc import ABC
|
||||
|
||||
|
||||
class AbstractClass(ABC):
|
||||
def empty_1(self): # error
|
||||
...
|
||||
|
||||
def empty_2(self): # error
|
||||
pass
|
||||
|
||||
def body_1(self):
|
||||
print("foo")
|
||||
...
|
||||
|
||||
def body_2(self):
|
||||
self.body_1()
|
||||
|
||||
|
||||
def foo():
|
||||
class InnerAbstractClass(ABC):
|
||||
def empty_1(self): # error
|
||||
...
|
||||
|
||||
def empty_2(self): # error
|
||||
pass
|
||||
|
||||
def body_1(self):
|
||||
print("foo")
|
||||
...
|
||||
|
||||
def body_2(self):
|
||||
self.body_1()
|
||||
|
||||
return InnerAbstractClass
|
||||
|
||||
|
|
@ -42,6 +42,7 @@ mod tests {
|
|||
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"); "B026")]
|
||||
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.py"); "B027")]
|
||||
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.pyi"); "B027_pyi")]
|
||||
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027_extended.py"); "B027_extended")]
|
||||
#[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"); "B028")]
|
||||
#[test_case(Rule::ExceptWithEmptyTuple, Path::new("B029.py"); "B029")]
|
||||
#[test_case(Rule::ExceptWithNonExceptionClasses, Path::new("B030.py"); "B030")]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_ast::whitespace::indentation;
|
||||
use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload};
|
||||
use ruff_python_semantic::context::Context;
|
||||
|
||||
use crate::autofix::actions::get_or_import_symbol;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::Importer;
|
||||
use crate::registry::Rule;
|
||||
|
||||
#[violation]
|
||||
|
|
@ -19,12 +25,13 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod {
|
|||
format!("`{name}` is an abstract base class, but it has no abstract methods")
|
||||
}
|
||||
}
|
||||
|
||||
#[violation]
|
||||
pub struct EmptyMethodWithoutAbstractDecorator {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Violation for EmptyMethodWithoutAbstractDecorator {
|
||||
impl AlwaysAutofixableViolation for EmptyMethodWithoutAbstractDecorator {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let EmptyMethodWithoutAbstractDecorator { name } = self;
|
||||
|
|
@ -32,24 +39,26 @@ impl Violation for EmptyMethodWithoutAbstractDecorator {
|
|||
"`{name}` is an empty method in an abstract base class, but has no abstract decorator"
|
||||
)
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
"Add the `@abstractmethod` decorator".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_abc_class(checker: &Checker, bases: &[Expr], keywords: &[Keyword]) -> bool {
|
||||
fn is_abc_class(context: &Context, bases: &[Expr], keywords: &[Keyword]) -> bool {
|
||||
keywords.iter().any(|keyword| {
|
||||
keyword
|
||||
.node
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |arg| arg == "metaclass")
|
||||
&& checker
|
||||
.ctx
|
||||
&& context
|
||||
.resolve_call_path(&keyword.node.value)
|
||||
.map_or(false, |call_path| {
|
||||
call_path.as_slice() == ["abc", "ABCMeta"]
|
||||
})
|
||||
}) || bases.iter().any(|base| {
|
||||
checker
|
||||
.ctx
|
||||
context
|
||||
.resolve_call_path(base)
|
||||
.map_or(false, |call_path| call_path.as_slice() == ["abc", "ABC"])
|
||||
})
|
||||
|
|
@ -68,6 +77,28 @@ fn is_empty_body(body: &[Stmt]) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
fn fix_abstractmethod_missing(
|
||||
context: &Context,
|
||||
importer: &Importer,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
stmt: &Stmt,
|
||||
) -> Result<Fix> {
|
||||
let indent = indentation(locator, stmt).ok_or(anyhow!("Unable to detect indentation"))?;
|
||||
let (import_edit, binding) =
|
||||
get_or_import_symbol("abc", "abstractmethod", context, importer, locator)?;
|
||||
let reference_edit = Edit::insertion(
|
||||
format!(
|
||||
"@{binding}{line_ending}{indent}",
|
||||
line_ending = stylist.line_ending().as_str(),
|
||||
),
|
||||
stmt.range().start(),
|
||||
);
|
||||
Ok(Fix::from_iter([import_edit, reference_edit]))
|
||||
}
|
||||
|
||||
/// B024
|
||||
/// B027
|
||||
pub fn abstract_base_class(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
|
|
@ -79,7 +110,7 @@ pub fn abstract_base_class(
|
|||
if bases.len() + keywords.len() != 1 {
|
||||
return;
|
||||
}
|
||||
if !is_abc_class(checker, bases, keywords) {
|
||||
if !is_abc_class(&checker.ctx, bases, keywords) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -123,12 +154,24 @@ pub fn abstract_base_class(
|
|||
&& is_empty_body(body)
|
||||
&& !is_overload(&checker.ctx, decorator_list)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
EmptyMethodWithoutAbstractDecorator {
|
||||
name: format!("{name}.{method_name}"),
|
||||
},
|
||||
stmt.range(),
|
||||
));
|
||||
);
|
||||
if checker.patch(Rule::EmptyMethodWithoutAbstractDecorator) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
fix_abstractmethod_missing(
|
||||
&checker.ctx,
|
||||
&checker.importer,
|
||||
checker.locator,
|
||||
checker.stylist,
|
||||
stmt,
|
||||
)
|
||||
});
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
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:13: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
|
||||
|
|
@ -11,8 +11,18 @@ B027.py:13:5: B027 `AbstractClass.empty_1` is an empty method in an abstract bas
|
|||
16 |
|
||||
17 | def empty_2(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
B027.py:16:5: B027 `AbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract 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 |
|
||||
|
|
@ -23,8 +33,18 @@ B027.py:16:5: B027 `AbstractClass.empty_2` is an empty method in an abstract bas
|
|||
20 |
|
||||
21 | def empty_3(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
B027.py:19:5: B027 `AbstractClass.empty_3` is an empty method in an abstract base class, but has no abstract 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 |
|
||||
|
||||
B027.py:19:5: B027 [*] `AbstractClass.empty_3` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
19 | pass
|
||||
20 |
|
||||
|
|
@ -36,8 +56,18 @@ B027.py:19:5: B027 `AbstractClass.empty_3` is an empty method in an abstract bas
|
|||
24 |
|
||||
25 | def empty_4(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
B027.py:23:5: B027 `AbstractClass.empty_4` is an empty method in an abstract base class, but has no abstract 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 |
|
||||
|
|
@ -52,5 +82,15 @@ B027.py:23:5: B027 `AbstractClass.empty_4` is an empty method in an abstract bas
|
|||
31 |
|
||||
32 | @notabstract
|
||||
|
|
||||
= 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 | ...
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
|
||||
---
|
||||
B027_extended.py:9:5: B027 [*] `AbstractClass.empty_1` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
9 | class AbstractClass(ABC):
|
||||
10 | def empty_1(self): # error
|
||||
| _____^
|
||||
11 | | ...
|
||||
| |___________^ B027
|
||||
12 |
|
||||
13 | def empty_2(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 | Should emit:
|
||||
3 3 | B027 - on lines 13, 16, 19, 23
|
||||
4 4 | """
|
||||
5 |-from abc import ABC
|
||||
5 |+from abc import ABC, abstractmethod
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | class AbstractClass(ABC):
|
||||
9 |+ @abstractmethod
|
||||
9 10 | def empty_1(self): # error
|
||||
10 11 | ...
|
||||
11 12 |
|
||||
|
||||
B027_extended.py:12:5: B027 [*] `AbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
12 | ...
|
||||
13 |
|
||||
14 | def empty_2(self): # error
|
||||
| _____^
|
||||
15 | | pass
|
||||
| |____________^ B027
|
||||
16 |
|
||||
17 | def body_1(self):
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 | Should emit:
|
||||
3 3 | B027 - on lines 13, 16, 19, 23
|
||||
4 4 | """
|
||||
5 |-from abc import ABC
|
||||
5 |+from abc import ABC, abstractmethod
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | class AbstractClass(ABC):
|
||||
9 9 | def empty_1(self): # error
|
||||
10 10 | ...
|
||||
11 11 |
|
||||
12 |+ @abstractmethod
|
||||
12 13 | def empty_2(self): # error
|
||||
13 14 | pass
|
||||
14 15 |
|
||||
|
||||
B027_extended.py:25:9: B027 [*] `InnerAbstractClass.empty_1` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
25 | def foo():
|
||||
26 | class InnerAbstractClass(ABC):
|
||||
27 | def empty_1(self): # error
|
||||
| _________^
|
||||
28 | | ...
|
||||
| |_______________^ B027
|
||||
29 |
|
||||
30 | def empty_2(self): # error
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 | Should emit:
|
||||
3 3 | B027 - on lines 13, 16, 19, 23
|
||||
4 4 | """
|
||||
5 |-from abc import ABC
|
||||
5 |+from abc import ABC, abstractmethod
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | class AbstractClass(ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
22 22 |
|
||||
23 23 | def foo():
|
||||
24 24 | class InnerAbstractClass(ABC):
|
||||
25 |+ @abstractmethod
|
||||
25 26 | def empty_1(self): # error
|
||||
26 27 | ...
|
||||
27 28 |
|
||||
|
||||
B027_extended.py:28:9: B027 [*] `InnerAbstractClass.empty_2` is an empty method in an abstract base class, but has no abstract decorator
|
||||
|
|
||||
28 | ...
|
||||
29 |
|
||||
30 | def empty_2(self): # error
|
||||
| _________^
|
||||
31 | | pass
|
||||
| |________________^ B027
|
||||
32 |
|
||||
33 | def body_1(self):
|
||||
|
|
||||
= help: Add the `@abstractmethod` decorator
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 | Should emit:
|
||||
3 3 | B027 - on lines 13, 16, 19, 23
|
||||
4 4 | """
|
||||
5 |-from abc import ABC
|
||||
5 |+from abc import ABC, abstractmethod
|
||||
6 6 |
|
||||
7 7 |
|
||||
8 8 | class AbstractClass(ABC):
|
||||
--------------------------------------------------------------------------------
|
||||
25 25 | def empty_1(self): # error
|
||||
26 26 | ...
|
||||
27 27 |
|
||||
28 |+ @abstractmethod
|
||||
28 29 | def empty_2(self): # error
|
||||
29 30 | pass
|
||||
30 31 |
|
||||
|
||||
|
||||
Loading…
Reference in New Issue