From b6caf8c92bc64bb1e631a0728e1089740efb434a Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 28 Oct 2025 21:00:19 -0400 Subject: [PATCH] Improve raw string handling in static_join_to_fstring Refined logic to better detect when non-raw representation is needed, especially for cases involving backslashes and quote delimiters. Updated test fixtures and snapshots to clarify behavior and expected fixes for edge cases mixing raw and non-raw strings. --- .../resources/test/fixtures/flynt/FLY002.py | 2 +- .../flynt/rules/static_join_to_fstring.rs | 8 +++- ...rules__flynt__tests__FLY002_FLY002.py.snap | 48 ++++++++++--------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flynt/FLY002.py b/crates/ruff_linter/resources/test/fixtures/flynt/FLY002.py index 0496cd82af..9b4bc0519c 100644 --- a/crates/ruff_linter/resources/test/fixtures/flynt/FLY002.py +++ b/crates/ruff_linter/resources/test/fixtures/flynt/FLY002.py @@ -36,7 +36,7 @@ nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax nok15 = "".join((r"", "\\")) # First is raw, second has backslash - would break syntax nok16 = "".join((r"", "\0")) # First is raw, second has null byte - would introduce null bytes nok17 = "".join((r"", "\r")) # First is raw, second has carriage return - would change behavior -nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r +nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) # Test that all-raw strings still work (should be OK) ok7 = "".join((r"", r"something")) # Both are raw - OK diff --git a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs index d98d124740..3eaec03aa2 100644 --- a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs @@ -109,10 +109,14 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option< // use non-raw representation. This handles cases where raw strings would create invalid // syntax or behavior changes. if any_raw && !content.is_empty() { - let needs_non_raw = content.contains(['\n', '\r', '\0']) || { + let needs_non_raw = content.contains(['\r', '\0']) || { // Check if content contains characters that would break raw string syntax let quote_char = flags.quote_str(); - content.contains(quote_char) || content.contains('\\') + // A raw string cannot end with a single backslash if it's immediately + // followed by the quote delimiter, as that would be invalid syntax. + let ends_with_backslash = content.ends_with('\\') + && (content.len() == 1 || content.chars().nth_back(1) != Some('\\')); + content.contains(quote_char) || ends_with_backslash }; if needs_non_raw { diff --git a/crates/ruff_linter/src/rules/flynt/snapshots/ruff_linter__rules__flynt__tests__FLY002_FLY002.py.snap b/crates/ruff_linter/src/rules/flynt/snapshots/ruff_linter__rules__flynt__tests__FLY002_FLY002.py.snap index 7491c77f6c..f6934590b9 100644 --- a/crates/ruff_linter/src/rules/flynt/snapshots/ruff_linter__rules__flynt__tests__FLY002_FLY002.py.snap +++ b/crates/ruff_linter/src/rules/flynt/snapshots/ruff_linter__rules__flynt__tests__FLY002_FLY002.py.snap @@ -125,7 +125,7 @@ help: Replace with `f"{secrets.token_urlsafe()}a{secrets.token_hex()}"` 13 | nok2 = a.join(["1", "2", "3"]) # Not OK (not a static joiner) note: This is an unsafe fix and may change runtime behavior -FLY002 [*] Consider `'line1\nline2'` instead of string join +FLY002 [*] Consider f-string instead of string join --> FLY002.py:20:8 | 18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string) @@ -134,15 +134,16 @@ FLY002 [*] Consider `'line1\nline2'` instead of string join | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 21 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail) | -help: Replace with `'line1\nline2'` +help: Replace with f-string 17 | nok6 = "a".join(x for x in "feefoofum") # Not OK (generator) 18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string) 19 | # https://github.com/astral-sh/ruff/issues/19887 - nok8 = '\n'.join([r'line1','line2']) -20 + nok8 = 'line1\nline2' -21 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail) -22 | -23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197 +20 + nok8 = r'''line1 +21 + line2''' +22 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail) +23 | +24 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197 note: This is an unsafe fix and may change runtime behavior FLY002 [*] Consider `"raw string\n<\"\"\">\n<'''>"` instead of string join @@ -267,7 +268,7 @@ help: Replace with `"\\"` 36 + nok15 = "\\" # First is raw, second has backslash - would break syntax 37 | nok16 = "".join((r"", "\0")) # First is raw, second has null byte - would introduce null bytes 38 | nok17 = "".join((r"", "\r")) # First is raw, second has carriage return - would change behavior -39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) note: This is an unsafe fix and may change runtime behavior FLY002 [*] Consider `"\x00"` instead of string join @@ -278,7 +279,7 @@ FLY002 [*] Consider `"\x00"` instead of string join 37 | nok16 = "".join((r"", "\0")) # First is raw, second has null byte - would introduce null bytes | ^^^^^^^^^^^^^^^^^^^^ 38 | nok17 = "".join((r"", "\r")) # First is raw, second has carriage return - would change behavior -39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) | help: Replace with `"\x00"` 34 | # Mixing raw and non-raw strings can cause syntax errors or behavior changes @@ -287,7 +288,7 @@ help: Replace with `"\x00"` - nok16 = "".join((r"", "\0")) # First is raw, second has null byte - would introduce null bytes 37 + nok16 = "\x00" # First is raw, second has null byte - would introduce null bytes 38 | nok17 = "".join((r"", "\r")) # First is raw, second has carriage return - would change behavior -39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) 40 | note: This is an unsafe fix and may change runtime behavior @@ -298,7 +299,7 @@ FLY002 [*] Consider `"\r"` instead of string join 37 | nok16 = "".join((r"", "\0")) # First is raw, second has null byte - would introduce null bytes 38 | nok17 = "".join((r"", "\r")) # First is raw, second has carriage return - would change behavior | ^^^^^^^^^^^^^^^^^^^^ -39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) | help: Replace with `"\r"` 35 | nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax @@ -306,27 +307,27 @@ help: Replace with `"\r"` 37 | nok16 = "".join((r"", "\0")) # First is raw, second has null byte - would introduce null bytes - nok17 = "".join((r"", "\r")) # First is raw, second has carriage return - would change behavior 38 + nok17 = "\r" # First is raw, second has carriage return - would change behavior -39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) 40 | 41 | # Test that all-raw strings still work (should be OK) note: This is an unsafe fix and may change runtime behavior -FLY002 [*] Consider `"\\r"` instead of string join +FLY002 [*] Consider `r"\r"` instead of string join --> FLY002.py:39:9 | 37 | nok16 = "".join((r"", "\0")) # First is raw, second has null byte - would introduce null bytes 38 | nok17 = "".join((r"", "\r")) # First is raw, second has carriage return - would change behavior -39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) | ^^^^^^^^^^^^^^^^^^^^^ 40 | 41 | # Test that all-raw strings still work (should be OK) | -help: Replace with `"\\r"` +help: Replace with `r"\r"` 36 | nok15 = "".join((r"", "\\")) # First is raw, second has backslash - would break syntax 37 | nok16 = "".join((r"", "\0")) # First is raw, second has null byte - would introduce null bytes 38 | nok17 = "".join((r"", "\r")) # First is raw, second has carriage return - would change behavior - - nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r -39 + nok18 = "\\r" # First is raw, second has backslash escape followed by r + - nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) +39 + nok18 = r"\r" # First is raw, second has backslash followed by literal r - OK (no special handling needed) 40 | 41 | # Test that all-raw strings still work (should be OK) 42 | ok7 = "".join((r"", r"something")) # Both are raw - OK @@ -341,7 +342,7 @@ FLY002 [*] Consider `r"something"` instead of string join 43 | ok8 = "\n".join((r"line1", r'line2')) # Both are raw - OK | help: Replace with `r"something"` -39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash followed by literal r - OK (no special handling needed) 40 | 41 | # Test that all-raw strings still work (should be OK) - ok7 = "".join((r"", r"something")) # Both are raw - OK @@ -351,7 +352,7 @@ help: Replace with `r"something"` 45 | # Test that all-non-raw strings still work (should be OK) note: This is an unsafe fix and may change runtime behavior -FLY002 [*] Consider `"line1\nline2"` instead of string join +FLY002 [*] Consider f-string instead of string join --> FLY002.py:43:7 | 41 | # Test that all-raw strings still work (should be OK) @@ -361,15 +362,16 @@ FLY002 [*] Consider `"line1\nline2"` instead of string join 44 | 45 | # Test that all-non-raw strings still work (should be OK) | -help: Replace with `"line1\nline2"` +help: Replace with f-string 40 | 41 | # Test that all-raw strings still work (should be OK) 42 | ok7 = "".join((r"", r"something")) # Both are raw - OK - ok8 = "\n".join((r"line1", r'line2')) # Both are raw - OK -43 + ok8 = "line1\nline2" # Both are raw - OK -44 | -45 | # Test that all-non-raw strings still work (should be OK) -46 | ok9 = "".join(("", '"')) # Both are non-raw - OK +43 + ok8 = r"""line1 +44 + line2""" # Both are raw - OK +45 | +46 | # Test that all-non-raw strings still work (should be OK) +47 | ok9 = "".join(("", '"')) # Both are non-raw - OK note: This is an unsafe fix and may change runtime behavior FLY002 [*] Consider `'"'` instead of string join