From 9ce83c215d2a25713a5c044965e9b573faa105b0 Mon Sep 17 00:00:00 2001 From: chiri Date: Wed, 28 May 2025 10:22:44 +0300 Subject: [PATCH] [`pyupgrade`]: new rule UP050 (`useless-class-metaclass-type`) (#18334) Co-authored-by: Micha Reiser --- .../test/fixtures/pyupgrade/UP050.py | 84 +++++++ .../src/checkers/ast/analyze/statement.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/pyupgrade/mod.rs | 1 + .../src/rules/pyupgrade/rules/mod.rs | 2 + .../rules/useless_class_metaclass_type.rs | 79 ++++++ ...er__rules__pyupgrade__tests__UP050.py.snap | 237 ++++++++++++++++++ ruff.schema.json | 2 + 8 files changed, 409 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP050.py create mode 100644 crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP050.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP050.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP050.py new file mode 100644 index 0000000000..6a04a4acc3 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP050.py @@ -0,0 +1,84 @@ +class A: + ... + + +class A(metaclass=type): + ... + + +class A( + metaclass=type +): + ... + + +class A( + metaclass=type + # +): + ... + + +class A( + # + metaclass=type +): + ... + + +class A( + metaclass=type, + # +): + ... + + +class A( + # + metaclass=type, + # +): + ... + + +class B(A, metaclass=type): + ... + + +class B( + A, + metaclass=type, +): + ... + + +class B( + A, + # comment + metaclass=type, +): + ... + + +def foo(): + class A(metaclass=type): + ... + + +class A( + metaclass=type # comment + , +): + ... + + +type = str + +class Foo(metaclass=type): + ... + + +import builtins + +class A(metaclass=builtins.type): + ... \ No newline at end of file diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 84563726a2..059c831a17 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -439,6 +439,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::UselessObjectInheritance) { pyupgrade::rules::useless_object_inheritance(checker, class_def); } + if checker.enabled(Rule::UselessClassMetaclassType) { + pyupgrade::rules::useless_class_metaclass_type(checker, class_def); + } if checker.enabled(Rule::ReplaceStrEnum) { if checker.target_version() >= PythonVersion::PY311 { pyupgrade::rules::replace_str_enum(checker, class_def); diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 63f81714dc..c96881fbaf 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -552,6 +552,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericClass), (Pyupgrade, "047") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericFunction), (Pyupgrade, "049") => (RuleGroup::Preview, rules::pyupgrade::rules::PrivateTypeParameter), + (Pyupgrade, "050") => (RuleGroup::Preview, rules::pyupgrade::rules::UselessClassMetaclassType), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index dfaf0ece7a..3f0388787b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -111,6 +111,7 @@ mod tests { #[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))] #[test_case(Rule::PrivateTypeParameter, Path::new("UP049_0.py"))] #[test_case(Rule::PrivateTypeParameter, Path::new("UP049_1.py"))] + #[test_case(Rule::UselessClassMetaclassType, Path::new("UP050.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs index c8f5f1b04b..aef1d1d168 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs @@ -37,6 +37,7 @@ pub(crate) use unpacked_list_comprehension::*; pub(crate) use use_pep585_annotation::*; pub(crate) use use_pep604_annotation::*; pub(crate) use use_pep604_isinstance::*; +pub(crate) use useless_class_metaclass_type::*; pub(crate) use useless_metaclass_type::*; pub(crate) use useless_object_inheritance::*; pub(crate) use yield_in_for_loop::*; @@ -80,6 +81,7 @@ mod unpacked_list_comprehension; mod use_pep585_annotation; mod use_pep604_annotation; mod use_pep604_isinstance; +mod useless_class_metaclass_type; mod useless_metaclass_type; mod useless_object_inheritance; mod yield_in_for_loop; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs new file mode 100644 index 0000000000..c4b26a2a84 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/useless_class_metaclass_type.rs @@ -0,0 +1,79 @@ +use crate::checkers::ast::Checker; +use crate::fix::edits::{Parentheses, remove_argument}; +use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::StmtClassDef; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks for `metaclass=type` in class definitions. +/// +/// ## Why is this bad? +/// Since Python 3, the default metaclass is `type`, so specifying it explicitly is redundant. +/// +/// Even though `__prepare__` is not required, the default metaclass (`type`) implements it, +/// for the convenience of subclasses calling it via `super()`. +/// ## Example +/// +/// ```python +/// class Foo(metaclass=type): ... +/// ``` +/// +/// Use instead: +/// +/// ```python +/// class Foo: ... +/// ``` +/// +/// ## References +/// - [PEP 3115 – Metaclasses in Python 3000](https://peps.python.org/pep-3115/) +#[derive(ViolationMetadata)] +pub(crate) struct UselessClassMetaclassType { + name: String, +} + +impl Violation for UselessClassMetaclassType { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let UselessClassMetaclassType { name } = self; + format!("Class `{name}` uses `metaclass=type`, which is redundant") + } + + fn fix_title(&self) -> Option { + Some("Remove `metaclass=type`".to_string()) + } +} + +/// UP050 +pub(crate) fn useless_class_metaclass_type(checker: &Checker, class_def: &StmtClassDef) { + let Some(arguments) = class_def.arguments.as_deref() else { + return; + }; + + for keyword in &arguments.keywords { + if let (Some("metaclass"), expr) = (keyword.arg.as_deref(), &keyword.value) { + if checker.semantic().match_builtin_expr(expr, "type") { + let mut diagnostic = Diagnostic::new( + UselessClassMetaclassType { + name: class_def.name.to_string(), + }, + keyword.range(), + ); + + diagnostic.try_set_fix(|| { + remove_argument( + keyword, + arguments, + Parentheses::Remove, + checker.locator().contents(), + ) + .map(Fix::safe_edit) + }); + + checker.report_diagnostic(diagnostic); + } + } + } +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP050.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP050.py.snap new file mode 100644 index 0000000000..73074b663e --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP050.py.snap @@ -0,0 +1,237 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP050.py:5:9: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +5 | class A(metaclass=type): + | ^^^^^^^^^^^^^^ UP050 +6 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +2 2 | ... +3 3 | +4 4 | +5 |-class A(metaclass=type): + 5 |+class A: +6 6 | ... +7 7 | +8 8 | + +UP050.py:10:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | + 9 | class A( +10 | metaclass=type + | ^^^^^^^^^^^^^^ UP050 +11 | ): +12 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +6 6 | ... +7 7 | +8 8 | +9 |-class A( +10 |- metaclass=type +11 |-): + 9 |+class A: +12 10 | ... +13 11 | +14 12 | + +UP050.py:16:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +15 | class A( +16 | metaclass=type + | ^^^^^^^^^^^^^^ UP050 +17 | # +18 | ): + | + = help: Remove `metaclass=type` + +ℹ Safe fix +12 12 | ... +13 13 | +14 14 | +15 |-class A( +16 |- metaclass=type +17 |- # +18 |-): + 15 |+class A: +19 16 | ... +20 17 | +21 18 | + +UP050.py:24:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +22 | class A( +23 | # +24 | metaclass=type + | ^^^^^^^^^^^^^^ UP050 +25 | ): +26 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +19 19 | ... +20 20 | +21 21 | +22 |-class A( +23 |- # +24 |- metaclass=type +25 |-): + 22 |+class A: +26 23 | ... +27 24 | +28 25 | + +UP050.py:30:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +29 | class A( +30 | metaclass=type, + | ^^^^^^^^^^^^^^ UP050 +31 | # +32 | ): + | + = help: Remove `metaclass=type` + +ℹ Safe fix +26 26 | ... +27 27 | +28 28 | +29 |-class A( +30 |- metaclass=type, +31 |- # +32 |-): + 29 |+class A: +33 30 | ... +34 31 | +35 32 | + +UP050.py:38:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +36 | class A( +37 | # +38 | metaclass=type, + | ^^^^^^^^^^^^^^ UP050 +39 | # +40 | ): + | + = help: Remove `metaclass=type` + +ℹ Safe fix +33 33 | ... +34 34 | +35 35 | +36 |-class A( +37 |- # +38 |- metaclass=type, +39 |- # +40 |-): + 36 |+class A: +41 37 | ... +42 38 | +43 39 | + +UP050.py:44:12: UP050 [*] Class `B` uses `metaclass=type`, which is redundant + | +44 | class B(A, metaclass=type): + | ^^^^^^^^^^^^^^ UP050 +45 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +41 41 | ... +42 42 | +43 43 | +44 |-class B(A, metaclass=type): + 44 |+class B(A): +45 45 | ... +46 46 | +47 47 | + +UP050.py:50:5: UP050 [*] Class `B` uses `metaclass=type`, which is redundant + | +48 | class B( +49 | A, +50 | metaclass=type, + | ^^^^^^^^^^^^^^ UP050 +51 | ): +52 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +47 47 | +48 48 | class B( +49 49 | A, +50 |- metaclass=type, +51 50 | ): +52 51 | ... +53 52 | + +UP050.py:58:5: UP050 [*] Class `B` uses `metaclass=type`, which is redundant + | +56 | A, +57 | # comment +58 | metaclass=type, + | ^^^^^^^^^^^^^^ UP050 +59 | ): +60 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +54 54 | +55 55 | class B( +56 56 | A, +57 |- # comment +58 |- metaclass=type, +59 57 | ): +60 58 | ... +61 59 | + +UP050.py:69:5: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +68 | class A( +69 | metaclass=type # comment + | ^^^^^^^^^^^^^^ UP050 +70 | , +71 | ): + | + = help: Remove `metaclass=type` + +ℹ Safe fix +65 65 | ... +66 66 | +67 67 | +68 |-class A( +69 |- metaclass=type # comment +70 |- , +71 |-): + 68 |+class A: +72 69 | ... +73 70 | +74 71 | + +UP050.py:83:9: UP050 [*] Class `A` uses `metaclass=type`, which is redundant + | +81 | import builtins +82 | +83 | class A(metaclass=builtins.type): + | ^^^^^^^^^^^^^^^^^^^^^^^ UP050 +84 | ... + | + = help: Remove `metaclass=type` + +ℹ Safe fix +80 80 | +81 81 | import builtins +82 82 | +83 |-class A(metaclass=builtins.type): + 83 |+class A: +84 84 | ... diff --git a/ruff.schema.json b/ruff.schema.json index 0059b7bb2f..e0f8ce55fb 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4299,6 +4299,8 @@ "UP046", "UP047", "UP049", + "UP05", + "UP050", "W", "W1", "W19",