[refurb] Check for subclasses includes subscript expressions (FURB189) (#16155)

## Summary

Added checks for subscript expressions on builtin classes as in FURB189.
The object is changed to use the collections objects and the types from
the subscript are kept.

Resolves #16130 

> Note: Added some comments in the code explaining why
## Test Plan


- Added a subscript dict and list class to the test file.
- Tested locally to check that the symbols are changed and the types are
kept.
- No modifications changed on optional `str` values.
This commit is contained in:
Vlad Nedelcu 2025-02-14 21:21:26 +02:00 committed by GitHub
parent f58a54f043
commit 219712860c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 65 additions and 6 deletions

View file

@ -8,7 +8,7 @@ class SetOnceMappingMixin:
if key in self:
raise KeyError(str(key) + ' already set')
return super().__setitem__(key, value)
class CaseInsensitiveEnumMeta(EnumMeta):
pass
@ -23,6 +23,12 @@ class L(list):
class S(str):
pass
class SubscriptDict(dict[str, str]):
pass
class SubscriptList(list[str]):
pass
# currently not detected
class SetOnceDict(SetOnceMappingMixin, dict):
pass

View file

@ -1,6 +1,6 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{Arguments, StmtClassDef};
use ruff_python_ast::{helpers::map_subscript, Arguments, StmtClassDef};
use ruff_text_size::Ranged;
use crate::{checkers::ast::Checker, importer::ImportRequest};
@ -70,11 +70,16 @@ pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) {
return;
};
// Expect only one base class else return
let [base] = &**bases else {
return;
};
let Some(symbol) = checker.semantic().resolve_builtin_symbol(base) else {
// Check if the base class is a subscript expression so that only the name expr
// is checked and modified.
let base_expr = map_subscript(base);
let Some(symbol) = checker.semantic().resolve_builtin_symbol(base_expr) else {
return;
};
@ -89,7 +94,7 @@ pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) {
subclass: symbol.to_string(),
replacement: user_symbol.to_string(),
},
base.range(),
base_expr.range(),
);
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
@ -97,7 +102,7 @@ pub(crate) fn subclass_builtin(checker: &Checker, class: &StmtClassDef) {
base.start(),
checker.semantic(),
)?;
let other_edit = Edit::range_replacement(binding, base.range());
let other_edit = Edit::range_replacement(binding, base_expr.range());
Ok(Fix::unsafe_edits(import_edit, [other_edit]))
});
checker.report_diagnostic(diagnostic);

View file

@ -74,4 +74,52 @@ FURB189.py:23:9: FURB189 [*] Subclassing `str` can be error prone, use `collecti
23 |+class S(UserString):
24 24 | pass
25 25 |
26 26 | # currently not detected
26 26 | class SubscriptDict(dict[str, str]):
FURB189.py:26:21: FURB189 [*] Subclassing `dict` can be error prone, use `collections.UserDict` instead
|
24 | pass
25 |
26 | class SubscriptDict(dict[str, str]):
| ^^^^ FURB189
27 | pass
|
= help: Replace with `collections.UserDict`
Unsafe fix
1 1 | # setup
2 2 | from enum import Enum, EnumMeta
3 |-from collections import UserList as UL
3 |+from collections import UserList as UL, UserDict
4 4 |
5 5 | class SetOnceMappingMixin:
6 6 | __slots__ = ()
--------------------------------------------------------------------------------
23 23 | class S(str):
24 24 | pass
25 25 |
26 |-class SubscriptDict(dict[str, str]):
26 |+class SubscriptDict(UserDict[str, str]):
27 27 | pass
28 28 |
29 29 | class SubscriptList(list[str]):
FURB189.py:29:21: FURB189 [*] Subclassing `list` can be error prone, use `collections.UserList` instead
|
27 | pass
28 |
29 | class SubscriptList(list[str]):
| ^^^^ FURB189
30 | pass
|
= help: Replace with `collections.UserList`
Unsafe fix
26 26 | class SubscriptDict(dict[str, str]):
27 27 | pass
28 28 |
29 |-class SubscriptList(list[str]):
29 |+class SubscriptList(UL[str]):
30 30 | pass
31 31 |
32 32 | # currently not detected