diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py index c863347069..12fe577263 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py @@ -84,3 +84,9 @@ def in_type_def(): # https://github.com/astral-sh/ruff/issues/18860 def fuzz_bug(): c('{\t"i}') + +# Test case for backslash handling in f-strings +# Should not trigger RUF027 for Python < 3.12 due to backslashes +def backslash_test(): + x = "test" + print("Hello {x}\\n") # Should not trigger RUF027 for Python < 3.12 diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index 229abff0d7..fd756a61ba 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -2,7 +2,7 @@ use memchr::memchr2_iter; use rustc_hash::FxHashSet; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast, PythonVersion}; use ruff_python_literal::format::FormatSpec; use ruff_python_parser::parse_expression; use ruff_python_semantic::analyze::logging::is_logger_candidate; @@ -116,7 +116,7 @@ pub(crate) fn missing_fstring_syntax(checker: &Checker, literal: &ast::StringLit return; } - if should_be_fstring(literal, checker.locator(), semantic) { + if should_be_fstring(literal, checker.locator(), semantic, checker) { checker .report_diagnostic(MissingFStringSyntax, literal.range()) .set_fix(fix_fstring_syntax(literal.range())); @@ -180,11 +180,18 @@ fn should_be_fstring( literal: &ast::StringLiteral, locator: &Locator, semantic: &SemanticModel, + checker: &Checker, ) -> bool { if !has_brackets(&literal.value) { return false; } + // Check if the string contains backslashes and target Python version is < 3.12 + // F-strings with backslashes are only valid in Python 3.12+ + if literal.value.contains('\\') && checker.target_version() < PythonVersion::PY312 { + return false; + } + let fstring_expr = format!("f{}", locator.slice(literal)); let Ok(parsed) = parse_expression(&fstring_expr) else { return false; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap index fcac406153..70348ca744 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap @@ -320,3 +320,19 @@ help: Add `f` prefix 76 | # fstrings are never correct as type definitions 77 | # so we should always skip those note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:92:11 + | +90 | def backslash_test(): +91 | x = "test" +92 | print("Hello {x}\\n") # Should not trigger RUF027 for Python < 3.12 + | ^^^^^^^^^^^^^^ + | +help: Add `f` prefix +89 | # Should not trigger RUF027 for Python < 3.12 due to backslashes +90 | def backslash_test(): +91 | x = "test" + - print("Hello {x}\\n") # Should not trigger RUF027 for Python < 3.12 +92 + print(f"Hello {x}\\n") # Should not trigger RUF027 for Python < 3.12 +note: This is an unsafe fix and may change runtime behavior