Skip to content

Commit 9ce83c2

Browse files
[pyupgrade]: new rule UP050 (useless-class-metaclass-type) (#18334)
Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent 602dd5c commit 9ce83c2

File tree

8 files changed

+409
-0
lines changed

8 files changed

+409
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
class A:
2+
...
3+
4+
5+
class A(metaclass=type):
6+
...
7+
8+
9+
class A(
10+
metaclass=type
11+
):
12+
...
13+
14+
15+
class A(
16+
metaclass=type
17+
#
18+
):
19+
...
20+
21+
22+
class A(
23+
#
24+
metaclass=type
25+
):
26+
...
27+
28+
29+
class A(
30+
metaclass=type,
31+
#
32+
):
33+
...
34+
35+
36+
class A(
37+
#
38+
metaclass=type,
39+
#
40+
):
41+
...
42+
43+
44+
class B(A, metaclass=type):
45+
...
46+
47+
48+
class B(
49+
A,
50+
metaclass=type,
51+
):
52+
...
53+
54+
55+
class B(
56+
A,
57+
# comment
58+
metaclass=type,
59+
):
60+
...
61+
62+
63+
def foo():
64+
class A(metaclass=type):
65+
...
66+
67+
68+
class A(
69+
metaclass=type # comment
70+
,
71+
):
72+
...
73+
74+
75+
type = str
76+
77+
class Foo(metaclass=type):
78+
...
79+
80+
81+
import builtins
82+
83+
class A(metaclass=builtins.type):
84+
...

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
439439
if checker.enabled(Rule::UselessObjectInheritance) {
440440
pyupgrade::rules::useless_object_inheritance(checker, class_def);
441441
}
442+
if checker.enabled(Rule::UselessClassMetaclassType) {
443+
pyupgrade::rules::useless_class_metaclass_type(checker, class_def);
444+
}
442445
if checker.enabled(Rule::ReplaceStrEnum) {
443446
if checker.target_version() >= PythonVersion::PY311 {
444447
pyupgrade::rules::replace_str_enum(checker, class_def);

crates/ruff_linter/src/codes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
552552
(Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericClass),
553553
(Pyupgrade, "047") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericFunction),
554554
(Pyupgrade, "049") => (RuleGroup::Preview, rules::pyupgrade::rules::PrivateTypeParameter),
555+
(Pyupgrade, "050") => (RuleGroup::Preview, rules::pyupgrade::rules::UselessClassMetaclassType),
555556

556557
// pydocstyle
557558
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),

crates/ruff_linter/src/rules/pyupgrade/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ mod tests {
111111
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))]
112112
#[test_case(Rule::PrivateTypeParameter, Path::new("UP049_0.py"))]
113113
#[test_case(Rule::PrivateTypeParameter, Path::new("UP049_1.py"))]
114+
#[test_case(Rule::UselessClassMetaclassType, Path::new("UP050.py"))]
114115
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
115116
let snapshot = path.to_string_lossy().to_string();
116117
let diagnostics = test_path(

crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub(crate) use unpacked_list_comprehension::*;
3737
pub(crate) use use_pep585_annotation::*;
3838
pub(crate) use use_pep604_annotation::*;
3939
pub(crate) use use_pep604_isinstance::*;
40+
pub(crate) use useless_class_metaclass_type::*;
4041
pub(crate) use useless_metaclass_type::*;
4142
pub(crate) use useless_object_inheritance::*;
4243
pub(crate) use yield_in_for_loop::*;
@@ -80,6 +81,7 @@ mod unpacked_list_comprehension;
8081
mod use_pep585_annotation;
8182
mod use_pep604_annotation;
8283
mod use_pep604_isinstance;
84+
mod useless_class_metaclass_type;
8385
mod useless_metaclass_type;
8486
mod useless_object_inheritance;
8587
mod yield_in_for_loop;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use crate::checkers::ast::Checker;
2+
use crate::fix::edits::{Parentheses, remove_argument};
3+
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
4+
use ruff_macros::{ViolationMetadata, derive_message_formats};
5+
use ruff_python_ast::StmtClassDef;
6+
use ruff_text_size::Ranged;
7+
8+
/// ## What it does
9+
/// Checks for `metaclass=type` in class definitions.
10+
///
11+
/// ## Why is this bad?
12+
/// Since Python 3, the default metaclass is `type`, so specifying it explicitly is redundant.
13+
///
14+
/// Even though `__prepare__` is not required, the default metaclass (`type`) implements it,
15+
/// for the convenience of subclasses calling it via `super()`.
16+
/// ## Example
17+
///
18+
/// ```python
19+
/// class Foo(metaclass=type): ...
20+
/// ```
21+
///
22+
/// Use instead:
23+
///
24+
/// ```python
25+
/// class Foo: ...
26+
/// ```
27+
///
28+
/// ## References
29+
/// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/)
30+
#[derive(ViolationMetadata)]
31+
pub(crate) struct UselessClassMetaclassType {
32+
name: String,
33+
}
34+
35+
impl Violation for UselessClassMetaclassType {
36+
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
37+
38+
#[derive_message_formats]
39+
fn message(&self) -> String {
40+
let UselessClassMetaclassType { name } = self;
41+
format!("Class `{name}` uses `metaclass=type`, which is redundant")
42+
}
43+
44+
fn fix_title(&self) -> Option<String> {
45+
Some("Remove `metaclass=type`".to_string())
46+
}
47+
}
48+
49+
/// UP050
50+
pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtClassDef) {
51+
let Some(arguments) = class_def.arguments.as_deref() else {
52+
return;
53+
};
54+
55+
for keyword in &arguments.keywords {
56+
if let (Some("metaclass"), expr) = (keyword.arg.as_deref(), &keyword.value) {
57+
if checker.semantic().match_builtin_expr(expr, "type") {
58+
let mut diagnostic = Diagnostic::new(
59+
UselessClassMetaclassType {
60+
name: class_def.name.to_string(),
61+
},
62+
keyword.range(),
63+
);
64+
65+
diagnostic.try_set_fix(|| {
66+
remove_argument(
67+
keyword,
68+
arguments,
69+
Parentheses::Remove,
70+
checker.locator().contents(),
71+
)
72+
.map(Fix::safe_edit)
73+
});
74+
75+
checker.report_diagnostic(diagnostic);
76+
}
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)