From 9b8ceb9a2e1e48430dabcb193a93da6307d09809 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 8 Dec 2024 18:51:37 +0000 Subject: [PATCH] [`pyupgrade`] Mark fixes for `convert-typed-dict-functional-to-class` and `convert-named-tuple-functional-to-class` as unsafe if they will remove comments (`UP013`, `UP014`) (#14842) --- .../test/fixtures/pyupgrade/UP013.py | 5 +++ .../test/fixtures/pyupgrade/UP014.py | 6 ++++ ...convert_named_tuple_functional_to_class.rs | 25 ++++++++++--- .../convert_typed_dict_functional_to_class.rs | 35 +++++++++++++------ ...er__rules__pyupgrade__tests__UP013.py.snap | 26 +++++++++++++- ...er__rules__pyupgrade__tests__UP014.py.snap | 22 +++++++++++- 6 files changed, 102 insertions(+), 17 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP013.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP013.py index 0f2628dddd..32cc0ec4a0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP013.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP013.py @@ -41,3 +41,8 @@ MyType = TypedDict("MyType", {}) # Empty dict call MyType = TypedDict("MyType", dict()) + +# Unsafe fix if comments are present +X = TypedDict("X", { + "some_config": int, # important +}) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP014.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP014.py index a30c8ee39c..db3622e20e 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP014.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP014.py @@ -31,3 +31,9 @@ S3File = NamedTuple( ("dataHPK",* str), ], ) + +# Unsafe fix if comments are present +X = NamedTuple("X", [ + ("some_config", int), # important +]) + diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index 80710ed40e..514f288865 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -1,6 +1,6 @@ use log::debug; -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::name::Name; @@ -8,6 +8,7 @@ use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Identifier, Key use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::identifiers::is_identifier; +use ruff_python_trivia::CommentRanges; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; @@ -41,6 +42,11 @@ use crate::checkers::ast::Checker; /// b: str /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there are any comments within the +/// range of the `NamedTuple` definition, as these will be dropped by the +/// autofix. +/// /// ## References /// - [Python documentation: `typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) #[derive(ViolationMetadata)] @@ -121,6 +127,7 @@ pub(crate) fn convert_named_tuple_functional_to_class( fields, base_class, checker.generator(), + checker.comment_ranges(), )); } checker.diagnostics.push(diagnostic); @@ -241,9 +248,17 @@ fn convert_to_class( body: Vec, base_class: &Expr, generator: Generator, + comment_ranges: &CommentRanges, ) -> Fix { - Fix::safe_edit(Edit::range_replacement( - generator.stmt(&create_class_def_stmt(typename, body, base_class)), - stmt.range(), - )) + Fix::applicable_edit( + Edit::range_replacement( + generator.stmt(&create_class_def_stmt(typename, body, base_class)), + stmt.range(), + ), + if comment_ranges.intersects(stmt.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + ) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index 1621f3a5ed..ef3e3768e4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -1,10 +1,11 @@ -use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Identifier, Keyword, Stmt}; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; use ruff_python_stdlib::identifiers::is_identifier; +use ruff_python_trivia::CommentRanges; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; @@ -37,6 +38,11 @@ use crate::checkers::ast::Checker; /// b: str /// ``` /// +/// ## Fix safety +/// This rule's fix is marked as unsafe if there are any comments within the +/// range of the `TypedDict` definition, as these will be dropped by the +/// autofix. +/// /// ## References /// - [Python documentation: `typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) #[derive(ViolationMetadata)] @@ -91,6 +97,7 @@ pub(crate) fn convert_typed_dict_functional_to_class( total_keyword, base_class, checker.generator(), + checker.comment_ranges(), )); } checker.diagnostics.push(diagnostic); @@ -265,14 +272,22 @@ fn convert_to_class( total_keyword: Option<&Keyword>, base_class: &Expr, generator: Generator, + comment_ranges: &CommentRanges, ) -> Fix { - Fix::safe_edit(Edit::range_replacement( - generator.stmt(&create_class_def_stmt( - class_name, - body, - total_keyword, - base_class, - )), - stmt.range(), - )) + Fix::applicable_edit( + Edit::range_replacement( + generator.stmt(&create_class_def_stmt( + class_name, + body, + total_keyword, + base_class, + )), + stmt.range(), + ), + if comment_ranges.intersects(stmt.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }, + ) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP013.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP013.py.snap index 73d1bf929c..0e03e843a1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP013.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP013.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs -snapshot_kind: text --- UP013.py:5:1: UP013 [*] Convert `MyType` from `TypedDict` functional to class syntax | @@ -242,6 +241,8 @@ UP013.py:43:1: UP013 [*] Convert `MyType` from `TypedDict` functional to class s 42 | # Empty dict call 43 | MyType = TypedDict("MyType", dict()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP013 +44 | +45 | # Unsafe fix if comments are present | = help: Convert `MyType` to class syntax @@ -252,3 +253,26 @@ UP013.py:43:1: UP013 [*] Convert `MyType` from `TypedDict` functional to class s 43 |-MyType = TypedDict("MyType", dict()) 43 |+class MyType(TypedDict): 44 |+ pass +44 45 | +45 46 | # Unsafe fix if comments are present +46 47 | X = TypedDict("X", { + +UP013.py:46:1: UP013 [*] Convert `X` from `TypedDict` functional to class syntax + | +45 | # Unsafe fix if comments are present +46 | / X = TypedDict("X", { +47 | | "some_config": int, # important +48 | | }) + | |__^ UP013 + | + = help: Convert `X` to class syntax + +ℹ Unsafe fix +43 43 | MyType = TypedDict("MyType", dict()) +44 44 | +45 45 | # Unsafe fix if comments are present +46 |-X = TypedDict("X", { +47 |- "some_config": int, # important +48 |-}) + 46 |+class X(TypedDict): + 47 |+ some_config: int diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap index 9697b8e885..9a049ab22a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs -snapshot_kind: text --- UP014.py:5:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class syntax | @@ -109,3 +108,24 @@ UP014.py:20:1: UP014 [*] Convert `MyType` from `NamedTuple` functional to class 21 23 | 22 24 | # unfixable 23 25 | MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)]) + +UP014.py:36:1: UP014 [*] Convert `X` from `NamedTuple` functional to class syntax + | +35 | # Unsafe fix if comments are present +36 | / X = NamedTuple("X", [ +37 | | ("some_config", int), # important +38 | | ]) + | |__^ UP014 + | + = help: Convert `X` to class syntax + +ℹ Unsafe fix +33 33 | ) +34 34 | +35 35 | # Unsafe fix if comments are present +36 |-X = NamedTuple("X", [ +37 |- ("some_config", int), # important +38 |-]) + 36 |+class X(NamedTuple): + 37 |+ some_config: int +39 38 |