From 03a8ece9548f94abe19460ec85b89372beed486a Mon Sep 17 00:00:00 2001 From: Harutaka Kawamura Date: Tue, 3 Jan 2023 12:28:32 +0900 Subject: [PATCH] Implement autofix for F541 (#1577) --- resources/test/fixtures/pyflakes/F541.py | 8 + src/ast/helpers.rs | 38 +++++ src/checkers/ast.rs | 10 +- .../plugins/f_string_missing_placeholders.rs | 25 +++ src/pyflakes/plugins/mod.rs | 2 + .../ruff__pyflakes__tests__F541_F541.py.snap | 142 ++++++++++++++++-- 6 files changed, 206 insertions(+), 19 deletions(-) create mode 100644 src/pyflakes/plugins/f_string_missing_placeholders.rs diff --git a/resources/test/fixtures/pyflakes/F541.py b/resources/test/fixtures/pyflakes/F541.py index ac6a20baa0..b0e4b5c0ca 100644 --- a/resources/test/fixtures/pyflakes/F541.py +++ b/resources/test/fixtures/pyflakes/F541.py @@ -9,6 +9,14 @@ e = ( f"def" + "ghi" ) +f = ( + f"a" + F"b" + "c" + rf"d" + fr"e" +) +g = f"" # OK g = f"ghi{123:{45}}" diff --git a/src/ast/helpers.rs b/src/ast/helpers.rs index 37cec82701..4714f5e075 100644 --- a/src/ast/helpers.rs +++ b/src/ast/helpers.rs @@ -9,6 +9,7 @@ use rustpython_ast::{ }; use rustpython_parser::lexer; use rustpython_parser::lexer::Tok; +use rustpython_parser::token::StringKind; use crate::ast::types::Range; use crate::SourceCodeLocator; @@ -470,6 +471,43 @@ pub fn except_range(handler: &Excepthandler, locator: &SourceCodeLocator) -> Ran range } +/// Find f-strings that don't contain any formatted values in a `JoinedStr`. +pub fn find_useless_f_strings(expr: &Expr, locator: &SourceCodeLocator) -> Vec<(Range, Range)> { + let contents = locator.slice_source_code_range(&Range::from_located(expr)); + lexer::make_tokenizer_located(&contents, expr.location) + .flatten() + .filter_map(|(location, tok, end_location)| match tok { + Tok::String { + kind: StringKind::FString | StringKind::RawFString, + .. + } => { + let first_char = locator.slice_source_code_range(&Range { + location, + end_location: Location::new(location.row(), location.column() + 1), + }); + // f"..." => f_position = 0 + // fr"..." => f_position = 0 + // rf"..." => f_position = 1 + let f_position = usize::from(!(first_char == "f" || first_char == "F")); + Some(( + Range { + location: Location::new(location.row(), location.column() + f_position), + end_location: Location::new( + location.row(), + location.column() + f_position + 1, + ), + }, + Range { + location, + end_location, + }, + )) + } + _ => None, + }) + .collect() +} + /// Return the `Range` of `else` in `For`, `AsyncFor`, and `While` statements. pub fn else_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Option { match &stmt.node { diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index c4215865bf..1bc396a7a4 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -2280,15 +2280,7 @@ where } ExprKind::JoinedStr { values } => { if self.settings.enabled.contains(&CheckCode::F541) { - if !values - .iter() - .any(|value| matches!(value.node, ExprKind::FormattedValue { .. })) - { - self.add_check(Check::new( - CheckKind::FStringMissingPlaceholders, - Range::from_located(expr), - )); - } + pyflakes::plugins::f_string_missing_placeholders(expr, values, self); } } ExprKind::BinOp { diff --git a/src/pyflakes/plugins/f_string_missing_placeholders.rs b/src/pyflakes/plugins/f_string_missing_placeholders.rs new file mode 100644 index 0000000000..73ed5413e5 --- /dev/null +++ b/src/pyflakes/plugins/f_string_missing_placeholders.rs @@ -0,0 +1,25 @@ +use rustpython_ast::{Expr, ExprKind}; + +use crate::ast::helpers::find_useless_f_strings; +use crate::autofix::Fix; +use crate::checkers::ast::Checker; +use crate::registry::{Check, CheckCode, CheckKind}; + +/// F541 +pub fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) { + if !values + .iter() + .any(|value| matches!(value.node, ExprKind::FormattedValue { .. })) + { + for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator) { + let mut check = Check::new(CheckKind::FStringMissingPlaceholders, tok_range); + if checker.patch(&CheckCode::F541) { + check.amend(Fix::deletion( + prefix_range.location, + prefix_range.end_location, + )); + } + checker.add_check(check); + } + } +} diff --git a/src/pyflakes/plugins/mod.rs b/src/pyflakes/plugins/mod.rs index 2a25c35e48..49909cf592 100644 --- a/src/pyflakes/plugins/mod.rs +++ b/src/pyflakes/plugins/mod.rs @@ -1,4 +1,5 @@ pub use assert_tuple::assert_tuple; +pub use f_string_missing_placeholders::f_string_missing_placeholders; pub use if_tuple::if_tuple; pub use invalid_literal_comparisons::invalid_literal_comparison; pub use invalid_print_syntax::invalid_print_syntax; @@ -13,6 +14,7 @@ pub(crate) use strings::{ }; mod assert_tuple; +mod f_string_missing_placeholders; mod if_tuple; mod invalid_literal_comparisons; mod invalid_print_syntax; diff --git a/src/pyflakes/snapshots/ruff__pyflakes__tests__F541_F541.py.snap b/src/pyflakes/snapshots/ruff__pyflakes__tests__F541_F541.py.snap index a1c01ffdff..cf2fbd5f5e 100644 --- a/src/pyflakes/snapshots/ruff__pyflakes__tests__F541_F541.py.snap +++ b/src/pyflakes/snapshots/ruff__pyflakes__tests__F541_F541.py.snap @@ -9,7 +9,14 @@ expression: checks end_location: row: 6 column: 10 - fix: ~ + fix: + content: "" + location: + row: 6 + column: 4 + end_location: + row: 6 + column: 5 parent: ~ - kind: FStringMissingPlaceholders location: @@ -18,7 +25,14 @@ expression: checks end_location: row: 7 column: 10 - fix: ~ + fix: + content: "" + location: + row: 7 + column: 4 + end_location: + row: 7 + column: 5 parent: ~ - kind: FStringMissingPlaceholders location: @@ -27,7 +41,62 @@ expression: checks end_location: row: 9 column: 10 - fix: ~ + fix: + content: "" + location: + row: 9 + column: 4 + end_location: + row: 9 + column: 5 + parent: ~ +- kind: FStringMissingPlaceholders + location: + row: 13 + column: 4 + end_location: + row: 13 + column: 8 + fix: + content: "" + location: + row: 13 + column: 4 + end_location: + row: 13 + column: 5 + parent: ~ +- kind: FStringMissingPlaceholders + location: + row: 14 + column: 4 + end_location: + row: 14 + column: 8 + fix: + content: "" + location: + row: 14 + column: 4 + end_location: + row: 14 + column: 5 + parent: ~ +- kind: FStringMissingPlaceholders + location: + row: 16 + column: 4 + end_location: + row: 16 + column: 9 + fix: + content: "" + location: + row: 16 + column: 5 + end_location: + row: 16 + column: 6 parent: ~ - kind: FStringMissingPlaceholders location: @@ -35,25 +104,78 @@ expression: checks column: 4 end_location: row: 17 + column: 9 + fix: + content: "" + location: + row: 17 + column: 4 + end_location: + row: 17 + column: 5 + parent: ~ +- kind: FStringMissingPlaceholders + location: + row: 19 + column: 4 + end_location: + row: 19 + column: 7 + fix: + content: "" + location: + row: 19 + column: 4 + end_location: + row: 19 + column: 5 + parent: ~ +- kind: FStringMissingPlaceholders + location: + row: 25 + column: 12 + end_location: + row: 25 column: 16 - fix: ~ + fix: + content: "" + location: + row: 25 + column: 12 + end_location: + row: 25 + column: 13 parent: ~ - kind: FStringMissingPlaceholders location: - row: 26 + row: 34 column: 6 end_location: - row: 26 + row: 34 column: 13 - fix: ~ + fix: + content: "" + location: + row: 34 + column: 6 + end_location: + row: 34 + column: 7 parent: ~ - kind: FStringMissingPlaceholders location: - row: 27 + row: 35 column: 3 end_location: - row: 27 + row: 35 column: 6 - fix: ~ + fix: + content: "" + location: + row: 35 + column: 3 + end_location: + row: 35 + column: 4 parent: ~