[refurb] Mark FURB180 fix unsafe when class has bases (#18149)

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

Mark `FURB180`'s fix as unsafe if the class already has base classes.
This is because the base classes might validate the other base classes
(like `typing.Protocol` does) or otherwise alter runtime behavior if
more base classes are added.

## Test Plan

The existing snapshot test covers this case already.

## References

Partially addresses https://github.com/astral-sh/ruff/issues/13307 (left
out way to permit certain exceptions)

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
Robsdedude 2025-06-03 00:51:09 +00:00 committed by GitHub
parent e677863787
commit 14c42a8ddf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 17 additions and 4 deletions

View file

@ -1,5 +1,6 @@
use itertools::Itertools;
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::StmtClassDef;
use ruff_text_size::{Ranged, TextRange};
@ -31,6 +32,11 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
/// pass
/// ```
///
/// ## Fix safety
/// The rule's fix is unsafe if the class has base classes. This is because the base classes might
/// be validating the class's other base classes (e.g., `typing.Protocol` does this) or otherwise
/// alter runtime behavior if more base classes are added.
///
/// ## 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)
@ -69,6 +75,11 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) {
return;
}
let applicability = if class_def.bases().is_empty() {
Applicability::Safe
} else {
Applicability::Unsafe
};
let mut diagnostic = checker.report_diagnostic(MetaClassABCMeta, keyword.range);
diagnostic.try_set_fix(|| {
@ -80,7 +91,7 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) {
Ok(if position > 0 {
// When the `abc.ABCMeta` is not the first keyword, put `abc.ABC` before the first
// keyword.
Fix::safe_edits(
Fix::applicable_edits(
// Delete from the previous argument, to the end of the `metaclass` argument.
Edit::range_deletion(TextRange::new(
class_def.keywords()[position - 1].end(),
@ -91,11 +102,13 @@ pub(crate) fn metaclass_abcmeta(checker: &Checker, class_def: &StmtClassDef) {
Edit::insertion(format!("{binding}, "), class_def.keywords()[0].start()),
import_edit,
],
applicability,
)
} else {
Fix::safe_edits(
Fix::applicable_edits(
Edit::range_replacement(binding, keyword.range),
[import_edit],
applicability,
)
})
});

View file

@ -50,7 +50,7 @@ FURB180.py:26:18: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract
|
= help: Replace with `abc.ABC`
Safe fix
Unsafe fix
23 23 | pass
24 24 |
25 25 |
@ -68,7 +68,7 @@ FURB180.py:31:34: FURB180 [*] Use of `metaclass=abc.ABCMeta` to define abstract
|
= help: Replace with `abc.ABC`
Safe fix
Unsafe fix
28 28 | def foo(self): pass
29 29 |
30 30 |