diff --git a/crates/ruff_linter/resources/test/fixtures/flynt/FLY002.py b/crates/ruff_linter/resources/test/fixtures/flynt/FLY002.py index 965ed165ee..0496cd82af 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 carriage return escape - would change behavior +nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r # 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 ddc73d5bcb..d98d124740 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 @@ -2,7 +2,9 @@ use ast::FStringFlags; use itertools::Itertools; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{self as ast, Arguments, Expr, StringFlags, str::Quote}; +use ruff_python_ast::{ + self as ast, Arguments, Expr, StringFlags, str::Quote, str_prefix::StringLiteralPrefix, +}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -74,7 +76,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option< // If all elements are string constants, join them into a single string. if joinees.iter().all(Expr::is_string_literal_expr) { let mut flags: Option = None; - let mut first_raw: Option = None; + let mut any_raw = false; for expr in joinees { if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr { @@ -83,12 +85,9 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option< if flags.is_none() { flags = Some(curr_flags); - first_raw = Some(is_raw); - } else if first_raw != Some(is_raw) { - // If not all strings have the same raw/non-raw status, bail. - // Mixing raw and non-raw strings can cause syntax errors or - // behavior changes when creating the joined string. - return None; + any_raw = is_raw; + } else if is_raw { + any_raw = true; } } } @@ -106,6 +105,21 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option< let mut flags = flags?; + // If any input was raw but the content cannot be safely represented as a raw string, + // 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']) || { + // Check if content contains characters that would break raw string syntax + let quote_char = flags.quote_str(); + content.contains(quote_char) || content.contains('\\') + }; + + if needs_non_raw { + flags = flags.with_prefix(StringLiteralPrefix::Empty); + } + } + // If the result is a raw string and contains a newline, use triple quotes. if flags.prefix().is_raw() && content.contains(['\n', '\r']) { flags = flags.with_triple_quotes(ruff_python_ast::str::TripleQuotes::Yes); 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 8790d266e9..7491c77f6c 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,6 +125,47 @@ 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.py:20:8 + | +18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string) +19 | # https://github.com/astral-sh/ruff/issues/19887 +20 | nok8 = '\n'.join([r'line1','line2']) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail) + | +help: Replace with `'line1\nline2'` +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 +note: This is an unsafe fix and may change runtime behavior + +FLY002 [*] Consider `"raw string\n<\"\"\">\n<'''>"` instead of string join + --> FLY002.py:21:8 + | +19 | # https://github.com/astral-sh/ruff/issues/19887 +20 | nok8 = '\n'.join([r'line1','line2']) +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 + | +help: Replace with `"raw string\n<\"\"\">\n<'''>"` +18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string) +19 | # https://github.com/astral-sh/ruff/issues/19887 +20 | nok8 = '\n'.join([r'line1','line2']) + - nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail) +21 + nok9 = "raw string\n<\"\"\">\n<'''>" # Not OK (both triple-quote delimiters appear; should bail) +22 | +23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197 +24 | def create_file_public_url(url, filename): +note: This is an unsafe fix and may change runtime behavior + FLY002 [*] Consider `f"{url}{filename}"` instead of string join --> FLY002.py:25:11 | @@ -187,6 +228,110 @@ help: Replace with `f"{foo}'"` 32 | note: This is an unsafe fix and may change runtime behavior +FLY002 [*] Consider `'"'` instead of string join + --> FLY002.py:35:9 + | +33 | # Regression test for: https://github.com/astral-sh/ruff/issues/21082 +34 | # Mixing raw and non-raw strings can cause syntax errors or behavior changes +35 | nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax + | ^^^^^^^^^^^^^^^^^^^ +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 + | +help: Replace with `'"'` +32 | +33 | # Regression test for: https://github.com/astral-sh/ruff/issues/21082 +34 | # Mixing raw and non-raw strings can cause syntax errors or behavior changes + - nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax +35 + nok14 = '"' # First is raw, second is not - would break syntax +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 +note: This is an unsafe fix and may change runtime behavior + +FLY002 [*] Consider `"\\"` instead of string join + --> FLY002.py:36:9 + | +34 | # Mixing raw and non-raw strings can cause syntax errors or behavior changes +35 | nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax +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 + | +help: Replace with `"\\"` +33 | # Regression test for: https://github.com/astral-sh/ruff/issues/21082 +34 | # Mixing raw and non-raw strings can cause syntax errors or behavior changes +35 | nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax + - nok15 = "".join((r"", "\\")) # First is raw, second has backslash - would break syntax +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 +note: This is an unsafe fix and may change runtime behavior + +FLY002 [*] Consider `"\x00"` instead of string join + --> FLY002.py:37:9 + | +35 | nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax +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 +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r + | +help: Replace with `"\x00"` +34 | # Mixing raw and non-raw strings can cause syntax errors or behavior changes +35 | nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax +36 | 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 +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 +40 | +note: This is an unsafe fix and may change runtime behavior + +FLY002 [*] Consider `"\r"` instead of string join + --> FLY002.py:38:9 + | +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 + | ^^^^^^^^^^^^^^^^^^^^ +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r + | +help: Replace with `"\r"` +35 | nok14 = "".join((r"", '"')) # First is raw, second is not - would break syntax +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 + - 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 +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.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 + | ^^^^^^^^^^^^^^^^^^^^^ +40 | +41 | # Test that all-raw strings still work (should be OK) + | +help: Replace with `"\\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 +40 | +41 | # Test that all-raw strings still work (should be OK) +42 | ok7 = "".join((r"", r"something")) # Both are raw - OK +note: This is an unsafe fix and may change runtime behavior + FLY002 [*] Consider `r"something"` instead of string join --> FLY002.py:42:7 | @@ -196,7 +341,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 carriage return escape - would change behavior +39 | nok18 = "".join((r"", "\\r")) # First is raw, second has backslash escape followed by r 40 | 41 | # Test that all-raw strings still work (should be OK) - ok7 = "".join((r"", r"something")) # Both are raw - OK @@ -206,7 +351,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 f-string instead of string join +FLY002 [*] Consider `"line1\nline2"` instead of string join --> FLY002.py:43:7 | 41 | # Test that all-raw strings still work (should be OK) @@ -216,16 +361,15 @@ FLY002 [*] Consider f-string instead of string join 44 | 45 | # Test that all-non-raw strings still work (should be OK) | -help: Replace with f-string +help: Replace with `"line1\nline2"` 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 = 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 +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 note: This is an unsafe fix and may change runtime behavior FLY002 [*] Consider `'"'` instead of string join