Update B027 to support autofixing (#4178)

This commit is contained in:
Aaron Cunningham 2023-05-04 12:36:32 -04:00 committed by GitHub
parent 494e807315
commit d78287540d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 259 additions and 14 deletions

View File

@ -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

View File

@ -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")]

View File

@ -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

View File

@ -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 | ...

View File

@ -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 |