-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[refurb
] Implement metaclass_abcmeta
(FURB180
)
#9658
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import abc | ||
from abc import abstractmethod, ABCMeta | ||
|
||
|
||
# Errors | ||
|
||
class A0(metaclass=abc.ABCMeta): | ||
@abstractmethod | ||
def foo(self): pass | ||
|
||
|
||
class A1(metaclass=ABCMeta): | ||
@abstractmethod | ||
def foo(self): pass | ||
|
||
|
||
class B0: | ||
def __init_subclass__(cls, **kwargs): | ||
super().__init_subclass__() | ||
|
||
|
||
class B1: | ||
pass | ||
|
||
|
||
class A2(B0, B1, metaclass=ABCMeta): | ||
@abstractmethod | ||
def foo(self): pass | ||
|
||
|
||
class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): | ||
pass | ||
|
||
|
||
# OK | ||
|
||
class Meta(type): | ||
def __new__(cls, *args, **kwargs): | ||
return super().__new__(cls, *args) | ||
|
||
|
||
class A4(metaclass=Meta, no_metaclass=ABCMeta): | ||
@abstractmethod | ||
def foo(self): pass | ||
|
||
|
||
class A5(metaclass=Meta): | ||
pass | ||
|
||
|
||
class A6(abc.ABC): | ||
@abstractmethod | ||
def foo(self): pass | ||
|
||
|
||
class A7(B0, abc.ABC, B1): | ||
@abstractmethod | ||
def foo(self): pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
use itertools::Itertools; | ||
|
||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::StmtClassDef; | ||
use ruff_text_size::{Ranged, TextRange}; | ||
|
||
use crate::checkers::ast::Checker; | ||
use crate::importer::ImportRequest; | ||
|
||
/// ## What it does | ||
/// Checks for uses of `metaclass=abc.ABCMeta` to define abstract base classes | ||
/// (ABCs). | ||
/// | ||
/// ## Why is this bad? | ||
/// | ||
/// Instead of `class C(metaclass=abc.ABCMeta): ...`, use `class C(ABC): ...` | ||
/// to define an abstract base class. Inheriting from the `ABC` wrapper class | ||
/// is semantically identical to setting `metaclass=abc.ABCMeta`, but more | ||
/// succinct. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// class C(metaclass=ABCMeta): | ||
/// pass | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// class C(ABC): | ||
/// pass | ||
/// ``` | ||
/// | ||
/// ## References | ||
/// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC) | ||
/// - [Python documentation: `abc.ABCMeta`](https://docs.python.org/3/library/abc.html#abc.ABCMeta) | ||
#[violation] | ||
pub struct MetaClassABCMeta; | ||
|
||
impl AlwaysFixableViolation for MetaClassABCMeta { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
format!("Use of `metaclass=abc.ABCMeta` to define abstract base class") | ||
} | ||
|
||
fn fix_title(&self) -> String { | ||
format!("Replace with `abc.ABC`") | ||
} | ||
} | ||
|
||
/// FURB180 | ||
pub(crate) fn metaclass_abcmeta(checker: &mut Checker, class_def: &StmtClassDef) { | ||
// Identify the `metaclass` keyword. | ||
let Some((position, keyword)) = class_def.keywords().iter().find_position(|&keyword| { | ||
keyword | ||
.arg | ||
.as_ref() | ||
.is_some_and(|arg| arg.as_str() == "metaclass") | ||
}) else { | ||
return; | ||
}; | ||
|
||
// Determine whether it's assigned to `abc.ABCMeta`. | ||
if !checker | ||
.semantic() | ||
.resolve_call_path(&keyword.value) | ||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["abc", "ABCMeta"])) | ||
{ | ||
return; | ||
} | ||
|
||
let mut diagnostic = Diagnostic::new(MetaClassABCMeta, keyword.range); | ||
|
||
diagnostic.try_set_fix(|| { | ||
let (import_edit, binding) = checker.importer().get_or_import_symbol( | ||
&ImportRequest::import("abc", "ABC"), | ||
keyword.range.start(), | ||
checker.semantic(), | ||
)?; | ||
Ok(if position > 0 { | ||
// When the `abc.ABCMeta` is not the first keyword, put `abc.ABC` before the first | ||
// keyword. | ||
Fix::safe_edits( | ||
// Delete from the previous argument, to the end of the `metaclass` argument. | ||
Edit::range_deletion(TextRange::new( | ||
class_def.keywords()[position - 1].end(), | ||
keyword.end(), | ||
)), | ||
// Insert `abc.ABC` before the first keyword. | ||
[ | ||
Edit::insertion(format!("{binding}, "), class_def.keywords()[0].start()), | ||
import_edit, | ||
], | ||
) | ||
} else { | ||
Fix::safe_edits( | ||
Edit::range_replacement(binding, keyword.range), | ||
[import_edit], | ||
) | ||
}) | ||
}); | ||
|
||
checker.diagnostics.push(diagnostic); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
...ter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/refurb/mod.rs | ||
--- | ||
FURB180.py:7:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | ||
| | ||
5 | # Errors | ||
6 | | ||
7 | class A0(metaclass=abc.ABCMeta): | ||
| ^^^^^^^^^^^^^^^^^^^^^ FURB180 | ||
8 | @abstractmethod | ||
9 | def foo(self): pass | ||
| | ||
= help: Replace with `abc.ABC` | ||
|
||
ℹ Safe fix | ||
4 4 | | ||
5 5 | # Errors | ||
6 6 | | ||
7 |-class A0(metaclass=abc.ABCMeta): | ||
7 |+class A0(abc.ABC): | ||
8 8 | @abstractmethod | ||
9 9 | def foo(self): pass | ||
10 10 | | ||
|
||
FURB180.py:12:10: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | ||
| | ||
12 | class A1(metaclass=ABCMeta): | ||
| ^^^^^^^^^^^^^^^^^ FURB180 | ||
13 | @abstractmethod | ||
14 | def foo(self): pass | ||
| | ||
= help: Replace with `abc.ABC` | ||
|
||
ℹ Safe fix | ||
9 9 | def foo(self): pass | ||
10 10 | | ||
11 11 | | ||
12 |-class A1(metaclass=ABCMeta): | ||
12 |+class A1(abc.ABC): | ||
13 13 | @abstractmethod | ||
14 14 | def foo(self): pass | ||
15 15 | | ||
|
||
FURB180.py:26:18: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | ||
| | ||
26 | class A2(B0, B1, metaclass=ABCMeta): | ||
| ^^^^^^^^^^^^^^^^^ FURB180 | ||
27 | @abstractmethod | ||
28 | def foo(self): pass | ||
| | ||
= help: Replace with `abc.ABC` | ||
|
||
ℹ Safe fix | ||
23 23 | pass | ||
24 24 | | ||
25 25 | | ||
26 |-class A2(B0, B1, metaclass=ABCMeta): | ||
26 |+class A2(B0, B1, abc.ABC): | ||
27 27 | @abstractmethod | ||
28 28 | def foo(self): pass | ||
29 29 | | ||
|
||
FURB180.py:31:34: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class | ||
| | ||
31 | class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): | ||
| ^^^^^^^^^^^^^^^^^^^^^ FURB180 | ||
32 | pass | ||
| | ||
= help: Replace with `abc.ABC` | ||
|
||
ℹ Safe fix | ||
28 28 | def foo(self): pass | ||
29 29 | | ||
30 30 | | ||
31 |-class A3(B0, before_metaclass=1, metaclass=abc.ABCMeta): | ||
31 |+class A3(B0, abc.ABC, before_metaclass=1): | ||
32 32 | pass | ||
33 33 | | ||
34 34 | | ||
|
||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I split this condition out of the
find_position
, I found it a little easier to read them when enforced separately.