Skip to content

Commit 0255200

Browse files
akawdmentBre
authored
[refurb] Do not add abc.ABC if already present (FURB180) (#22234)
<!-- Thank you for contributing to Ruff/ty! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? (Please prefix with `[ty]` for ty pull requests.) - Does this pull request include references to any relevant issues? --> ## Summary According to the `FURB180` rule, `abc.ABC` should be used instead of `metaclass=abc.ABCMeta`. This issue can be fixed automatically, but if the class being checked already contains `abc.ABC`, it would be added a second time. Fixes: #17162 ## Test Plan I ran `cargo test refurb`. For checking the "fix" I used this python file: <details> <summary>Python file (input)</summary> ```python import abc from abc import abstractmethod, ABCMeta, ABC from abc import ABC as ABCAnotherName class ParentClass: ... from abc import ABC, ABCMeta class Foo(metaclass=ABCMeta): ... class Foo2(abc.ABC, ParentClass, metaclass=ABCMeta): ... class Foo3(ParentClass, ABC, metaclass=ABCMeta): ... class Foo4(ABCAnotherName, metaclass=ABCMeta): ... ``` </details> The output after applying the fix is so: <details> <summary>Python file (output)</summary> ```python import abc from abc import abstractmethod, ABCMeta, ABC from abc import ABC as ABCAnotherName class ParentClass: ... from abc import ABC, ABCMeta class Foo(ABC): ... class Foo2(abc.ABC, ParentClass, ): ... class Foo3(ParentClass, ABC, ): ... class Foo4(ABCAnotherName, ): ... ``` </details> Note: Trailing commas may remain after the automatic fix. It is unclear whether this is an issue. --------- Co-authored-by: me <me@example.com> Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com> Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
1 parent fcbf6f1 commit 0255200

3 files changed

Lines changed: 206 additions & 25 deletions

File tree

crates/ruff_linter/resources/test/fixtures/refurb/FURB180.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,45 @@ def foo(self): pass
5656
class A7(B0, abc.ABC, B1):
5757
@abstractmethod
5858
def foo(self): pass
59+
60+
61+
# Regression tests for https://github.com/astral-sh/ruff/issues/17162
62+
class A8(abc.ABC, metaclass=ABCMeta): # FURB180
63+
@abstractmethod
64+
def foo(self):
65+
pass
66+
67+
68+
def a9():
69+
from abc import ABC
70+
71+
class A9(ABC, metaclass=ABCMeta): # FURB180
72+
@abstractmethod
73+
def foo(self):
74+
pass
75+
76+
77+
def a10():
78+
from abc import ABC as ABCAlternativeName
79+
80+
class A10(ABCAlternativeName, metaclass=ABCMeta): # FURB180
81+
@abstractmethod
82+
def foo(self):
83+
pass
84+
85+
86+
class MyMetaClass(abc.ABC): ...
87+
88+
89+
class A11(MyMetaClass, metaclass=ABCMeta): # FURB180
90+
@abstractmethod
91+
def foo(self):
92+
pass
93+
94+
95+
class A12(
96+
keyword_argument=1,
97+
# comment
98+
metaclass=abc.ABCMeta,
99+
): # FURB180
100+
pass

crates/ruff_linter/src/rules/refurb/rules/metaclass_abcmeta.rs

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use itertools::Itertools;
2-
32
use ruff_diagnostics::Applicability;
43
use ruff_macros::{ViolationMetadata, derive_message_formats};
54
use ruff_python_ast::StmtClassDef;
6-
use ruff_text_size::{Ranged, TextRange};
5+
use ruff_python_semantic::analyze;
6+
use ruff_text_size::Ranged;
77

88
use crate::checkers::ast::Checker;
9+
use crate::fix::edits::{Parentheses, remove_argument};
910
use crate::importer::ImportRequest;
1011
use crate::{AlwaysFixableViolation, Edit, Fix};
1112

@@ -42,6 +43,9 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
4243
/// The rule's fix is unsafe if the class has base classes. This is because the base classes might
4344
/// be validating the class's other base classes (e.g., `typing.Protocol` does this) or otherwise
4445
/// alter runtime behavior if more base classes are added.
46+
/// The rule's fix will also be marked as unsafe if the class has
47+
/// comments in its argument list that could be deleted.
48+
///
4549
///
4650
/// ## References
4751
/// - [Python documentation: `abc.ABC`](https://docs.python.org/3/library/abc.html#abc.ABC)
@@ -63,6 +67,11 @@ impl AlwaysFixableViolation for MetaClassABCMeta {
6367

6468
/// FURB180
6569
pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) {
70+
// Determine whether the class definition contains at least one argument.
71+
let Some(arguments) = &class_def.arguments.as_ref() else {
72+
return;
73+
};
74+
6675
// Identify the `metaclass` keyword.
6776
let Some((position, keyword)) = class_def.keywords().iter().find_position(|&keyword| {
6877
keyword
@@ -82,10 +91,18 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) {
8291
return;
8392
}
8493

85-
let applicability = if class_def.bases().is_empty() {
86-
Applicability::Safe
87-
} else {
94+
let applicability = if !class_def.bases().is_empty() {
95+
// The class has base arguments (not just a `metaclass` keyword).
96+
// Applying the fix may change semantics, so it is considered unsafe.
97+
Applicability::Unsafe
98+
} else if checker.comment_ranges().intersects(arguments.range()) {
99+
// The `metaclass` keyword overlaps with a comment.
100+
// Applying the fix would remove or alter the comment, so it is unsafe.
88101
Applicability::Unsafe
102+
} else {
103+
// An empty class definition with no overlapping comments.
104+
// Applying the fix is considered safe.
105+
Applicability::Safe
89106
};
90107
let mut diagnostic = checker.report_diagnostic(MetaClassABCMeta, keyword.range);
91108

@@ -95,28 +112,47 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) {
95112
keyword.range.start(),
96113
checker.semantic(),
97114
)?;
115+
116+
// Check the `abc.ABC` is in base classes.
117+
let has_abc = analyze::class::any_qualified_base_class(
118+
class_def,
119+
checker.semantic(),
120+
&|qualified_name| matches!(qualified_name.segments(), ["abc", "ABC"]),
121+
);
122+
123+
let delete_metaclass_keyword = remove_argument(
124+
keyword,
125+
arguments,
126+
Parentheses::Remove,
127+
checker.source(),
128+
checker.tokens(),
129+
)?;
130+
98131
Ok(if position > 0 {
99-
// When the `abc.ABCMeta` is not the first keyword, put `abc.ABC` before the first
100-
// keyword.
101-
Fix::applicable_edits(
102-
// Delete from the previous argument, to the end of the `metaclass` argument.
103-
Edit::range_deletion(TextRange::new(
104-
class_def.keywords()[position - 1].end(),
105-
keyword.end(),
106-
)),
107-
// Insert `abc.ABC` before the first keyword.
108-
[
109-
Edit::insertion(format!("{binding}, "), class_def.keywords()[0].start()),
110-
import_edit,
111-
],
112-
applicability,
113-
)
132+
// When the `abc.ABCMeta` is not the first keyword and `abc.ABC` is not
133+
// in base classes put `abc.ABC` before the first keyword argument.
134+
if has_abc {
135+
Fix::applicable_edit(delete_metaclass_keyword, applicability)
136+
} else {
137+
Fix::applicable_edits(
138+
delete_metaclass_keyword,
139+
[
140+
Edit::insertion(format!("{binding}, "), class_def.keywords()[0].start()),
141+
import_edit,
142+
],
143+
applicability,
144+
)
145+
}
114146
} else {
115-
Fix::applicable_edits(
116-
Edit::range_replacement(binding, keyword.range),
117-
[import_edit],
118-
applicability,
119-
)
147+
let edit_action = if has_abc {
148+
// Class already inherits the `abc.ABC`, delete the `metaclass` keyword only.
149+
delete_metaclass_keyword
150+
} else {
151+
// Replace `metaclass` keyword by `abc.ABC`.
152+
Edit::range_replacement(binding, keyword.range)
153+
};
154+
155+
Fix::applicable_edits(edit_action, [import_edit], applicability)
120156
})
121157
});
122158
}

crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB180_FURB180.py.snap

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,106 @@ help: Replace with `abc.ABC`
7575
33 |
7676
34 |
7777
note: This is an unsafe fix and may change runtime behavior
78+
79+
FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class
80+
--> FURB180.py:62:19
81+
|
82+
61 | # Regression tests for https://github.com/astral-sh/ruff/issues/17162
83+
62 | class A8(abc.ABC, metaclass=ABCMeta): # FURB180
84+
| ^^^^^^^^^^^^^^^^^
85+
63 | @abstractmethod
86+
64 | def foo(self):
87+
|
88+
help: Replace with `abc.ABC`
89+
59 |
90+
60 |
91+
61 | # Regression tests for https://github.com/astral-sh/ruff/issues/17162
92+
- class A8(abc.ABC, metaclass=ABCMeta): # FURB180
93+
62 + class A8(abc.ABC): # FURB180
94+
63 | @abstractmethod
95+
64 | def foo(self):
96+
65 | pass
97+
note: This is an unsafe fix and may change runtime behavior
98+
99+
FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class
100+
--> FURB180.py:71:19
101+
|
102+
69 | from abc import ABC
103+
70 |
104+
71 | class A9(ABC, metaclass=ABCMeta): # FURB180
105+
| ^^^^^^^^^^^^^^^^^
106+
72 | @abstractmethod
107+
73 | def foo(self):
108+
|
109+
help: Replace with `abc.ABC`
110+
68 | def a9():
111+
69 | from abc import ABC
112+
70 |
113+
- class A9(ABC, metaclass=ABCMeta): # FURB180
114+
71 + class A9(ABC): # FURB180
115+
72 | @abstractmethod
116+
73 | def foo(self):
117+
74 | pass
118+
note: This is an unsafe fix and may change runtime behavior
119+
120+
FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class
121+
--> FURB180.py:80:35
122+
|
123+
78 | from abc import ABC as ABCAlternativeName
124+
79 |
125+
80 | class A10(ABCAlternativeName, metaclass=ABCMeta): # FURB180
126+
| ^^^^^^^^^^^^^^^^^
127+
81 | @abstractmethod
128+
82 | def foo(self):
129+
|
130+
help: Replace with `abc.ABC`
131+
77 | def a10():
132+
78 | from abc import ABC as ABCAlternativeName
133+
79 |
134+
- class A10(ABCAlternativeName, metaclass=ABCMeta): # FURB180
135+
80 + class A10(ABCAlternativeName): # FURB180
136+
81 | @abstractmethod
137+
82 | def foo(self):
138+
83 | pass
139+
note: This is an unsafe fix and may change runtime behavior
140+
141+
FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class
142+
--> FURB180.py:89:24
143+
|
144+
89 | class A11(MyMetaClass, metaclass=ABCMeta): # FURB180
145+
| ^^^^^^^^^^^^^^^^^
146+
90 | @abstractmethod
147+
91 | def foo(self):
148+
|
149+
help: Replace with `abc.ABC`
150+
86 | class MyMetaClass(abc.ABC): ...
151+
87 |
152+
88 |
153+
- class A11(MyMetaClass, metaclass=ABCMeta): # FURB180
154+
89 + class A11(MyMetaClass): # FURB180
155+
90 | @abstractmethod
156+
91 | def foo(self):
157+
92 | pass
158+
note: This is an unsafe fix and may change runtime behavior
159+
160+
FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract base class
161+
--> FURB180.py:98:5
162+
|
163+
96 | keyword_argument=1,
164+
97 | # comment
165+
98 | metaclass=abc.ABCMeta,
166+
| ^^^^^^^^^^^^^^^^^^^^^
167+
99 | ): # FURB180
168+
100 | pass
169+
|
170+
help: Replace with `abc.ABC`
171+
93 |
172+
94 |
173+
95 | class A12(
174+
- keyword_argument=1,
175+
- # comment
176+
- metaclass=abc.ABCMeta,
177+
96 + abc.ABC, keyword_argument=1,
178+
97 | ): # FURB180
179+
98 | pass
180+
note: This is an unsafe fix and may change runtime behavior

0 commit comments

Comments
 (0)