From 197885c71f1fee5bf3531edb6f175790ad3ed233 Mon Sep 17 00:00:00 2001 From: Zanie Date: Thu, 16 Nov 2023 08:55:03 -0600 Subject: [PATCH 1/2] Add failing test case for escaped curly braces bug in UP032 --- .../test/fixtures/pyupgrade/UP032_0.py | 17 ++++ ...__rules__pyupgrade__tests__UP032_0.py.snap | 88 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py index 19868afac3284..c5f21e0b7d93d 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP032_0.py @@ -226,3 +226,20 @@ async def c(): "".format(new_dict, d) ) + +# The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped +( + "{}" + "{{}}" +).format(a) + +("{}" "{{}}").format(a) + + +# Both strings will be converted to an f-string and the curly braces in the second should left escaped +( + "{}" + "{{{}}}" +).format(a, b) + +("{}" "{{{}}}").format(a, b) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap index 4538fe9b917de..7cf99c42418d1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap @@ -1066,5 +1066,93 @@ UP032_0.py:225:5: UP032 [*] Use f-string instead of `format` call 225 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}" 227 226 | 228 227 | ) +229 228 | + +UP032_0.py:231:1: UP032 [*] Use f-string instead of `format` call + | +230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped +231 | / ( +232 | | "{}" +233 | | "{{}}" +234 | | ).format(a) + | |___________^ UP032 +235 | +236 | ("{}" "{{}}").format(a) + | + = help: Convert to f-string + +ℹ Safe fix +229 229 | +230 230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped +231 231 | ( +232 |- "{}" + 232 |+ f"{a}" +233 233 | "{{}}" +234 |-).format(a) + 234 |+) +235 235 | +236 236 | ("{}" "{{}}").format(a) +237 237 | + +UP032_0.py:236:1: UP032 [*] Use f-string instead of `format` call + | +234 | ).format(a) +235 | +236 | ("{}" "{{}}").format(a) + | ^^^^^^^^^^^^^^^^^^^^^^^ UP032 + | + = help: Convert to f-string + +ℹ Safe fix +233 233 | "{{}}" +234 234 | ).format(a) +235 235 | +236 |-("{}" "{{}}").format(a) + 236 |+(f"{a}" "{{}}") +237 237 | +238 238 | +239 239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped + +UP032_0.py:240:1: UP032 [*] Use f-string instead of `format` call + | +239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped +240 | / ( +241 | | "{}" +242 | | "{{{}}}" +243 | | ).format(a, b) + | |______________^ UP032 +244 | +245 | ("{}" "{{{}}}").format(a, b) + | + = help: Convert to f-string + +ℹ Safe fix +238 238 | +239 239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped +240 240 | ( +241 |- "{}" +242 |- "{{{}}}" +243 |-).format(a, b) + 241 |+ f"{a}" + 242 |+ f"{{{b}}}" + 243 |+) +244 244 | +245 245 | ("{}" "{{{}}}").format(a, b) + +UP032_0.py:245:1: UP032 [*] Use f-string instead of `format` call + | +243 | ).format(a, b) +244 | +245 | ("{}" "{{{}}}").format(a, b) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032 + | + = help: Convert to f-string + +ℹ Safe fix +242 242 | "{{{}}}" +243 243 | ).format(a, b) +244 244 | +245 |-("{}" "{{{}}}").format(a, b) + 245 |+(f"{a}" f"{{{b}}}") From 59791aae3af34bdf0bdb30cbaaa1879cf6385361 Mon Sep 17 00:00:00 2001 From: Zanie Date: Thu, 16 Nov 2023 08:55:59 -0600 Subject: [PATCH 2/2] Fix bug --- .../src/rules/pyupgrade/helpers.rs | 13 +++++++++ .../src/rules/pyupgrade/rules/f_strings.rs | 10 ++++--- ...__rules__pyupgrade__tests__UP032_0.py.snap | 29 ++++++++++--------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/helpers.rs b/crates/ruff_linter/src/rules/pyupgrade/helpers.rs index 03f48d877aa0d..07196a9127332 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/helpers.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/helpers.rs @@ -19,3 +19,16 @@ pub(super) fn curly_escape(text: &str) -> Cow<'_, str> { } }) } + +static DOUBLE_CURLY_BRACES: Lazy = Lazy::new(|| Regex::new(r"((\{\{)|(\}\}))").unwrap()); + +pub(super) fn curly_unescape(text: &str) -> Cow<'_, str> { + // Match all double curly braces and replace with a single + DOUBLE_CURLY_BRACES.replace_all(text, |caps: &Captures| { + if &caps[1] == "{{" { + "{".to_string() + } else { + "}".to_string() + } + }) +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs index 0ce52785c4139..e7cefb982f898 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs @@ -18,7 +18,7 @@ use crate::checkers::ast::Checker; use crate::fix::edits::fits_or_shrinks; use crate::rules::pyflakes::format::FormatSummary; -use crate::rules::pyupgrade::helpers::curly_escape; +use crate::rules::pyupgrade::helpers::{curly_escape, curly_unescape}; /// ## What it does /// Checks for `str.format` calls that can be replaced with f-strings. @@ -357,9 +357,11 @@ pub(crate) fn f_strings( Some((Tok::String { .. }, range)) => { match try_convert_to_f_string(range, &mut summary, checker.locator()) { Ok(Some(fstring)) => patches.push((range, fstring)), - // Skip any strings that don't require conversion (e.g., literal segments of an - // implicit concatenation). - Ok(None) => continue, + // Convert escaped curly brackets e.g. `{{` to `{` in literal string parts + Ok(None) => patches.push(( + range, + curly_unescape(checker.locator().slice(range)).to_string(), + )), // If any of the segments fail to convert, then we can't convert the entire // expression. Err(_) => return, diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap index 7cf99c42418d1..83e87da819fd2 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap @@ -995,10 +995,11 @@ UP032_0.py:212:18: UP032 [*] Use f-string instead of `format` call 211 211 | # When fixing, trim the trailing empty string. 212 |-raise ValueError("Conflicting configuration dicts: {!r} {!r}" 213 |- "".format(new_dict, d)) - 212 |+raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}") -214 213 | -215 214 | # When fixing, trim the trailing empty string. -216 215 | raise ValueError("Conflicting configuration dicts: {!r} {!r}" + 212 |+raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}" + 213 |+ "") +214 214 | +215 215 | # When fixing, trim the trailing empty string. +216 216 | raise ValueError("Conflicting configuration dicts: {!r} {!r}" UP032_0.py:216:18: UP032 [*] Use f-string instead of `format` call | @@ -1041,9 +1042,10 @@ UP032_0.py:220:5: UP032 [*] Use f-string instead of `format` call 220 |- "Conflicting configuration dicts: {!r} {!r}" 221 |- "".format(new_dict, d) 220 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}" -222 221 | ) -223 222 | -224 223 | raise ValueError( + 221 |+ "" +222 222 | ) +223 223 | +224 224 | raise ValueError( UP032_0.py:225:5: UP032 [*] Use f-string instead of `format` call | @@ -1064,9 +1066,10 @@ UP032_0.py:225:5: UP032 [*] Use f-string instead of `format` call 225 |- "Conflicting configuration dicts: {!r} {!r}" 226 |- "".format(new_dict, d) 225 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}" -227 226 | -228 227 | ) -229 228 | + 226 |+ "" +227 227 | +228 228 | ) +229 229 | UP032_0.py:231:1: UP032 [*] Use f-string instead of `format` call | @@ -1085,9 +1088,9 @@ UP032_0.py:231:1: UP032 [*] Use f-string instead of `format` call 229 229 | 230 230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped 231 231 | ( -232 |- "{}" 232 |+ f"{a}" -233 233 | "{{}}" +232 233 | "{}" +233 |- "{{}}" 234 |-).format(a) 234 |+) 235 235 | @@ -1108,7 +1111,7 @@ UP032_0.py:236:1: UP032 [*] Use f-string instead of `format` call 234 234 | ).format(a) 235 235 | 236 |-("{}" "{{}}").format(a) - 236 |+(f"{a}" "{{}}") + 236 |+(f"{a}" "{}") 237 237 | 238 238 | 239 239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped