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 04c482bd5226e..c984bfcef7983 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py @@ -90,3 +90,31 @@ def fuzz_bug(): def backslash_test(): x = "test" print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 + +# Test case for comment handling in f-string interpolations +# Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +def comment_test(): + x = "!" + print("""{x # } +}""") + +# Test case for `#` inside a nested string literal in interpolation +# `#` inside a string is NOT a comment — should trigger RUF027 even on Python < 3.12 +def hash_in_string_test(): + x = "world" + print("Hello {'#'}{x}") # RUF027: `#` is inside a string, not a comment + print("Hello {\"#\"}{x}") # RUF027: same, double-quoted + +# Test case for `#` in format spec (e.g., `{1:#x}`) +# `#` in a format spec is NOT a comment — should trigger RUF027 even on Python < 3.12 +def hash_in_format_spec_test(): + n = 255 + print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment + print("Oct: {n:#o}") # RUF027: same + +# Test case for `#` in nested interpolation inside format spec (e.g., `{1:{x #}}`) +# The `#` is a comment inside a nested interpolation — should NOT trigger RUF027 on Python < 3.12 +def hash_in_nested_format_spec_test(): + x = 5 + print("""{1:{x #}} +}""") # 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 df64e8532bc85..5d4e9cb6e3c2b 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 @@ -4,7 +4,7 @@ use rustc_hash::FxHashSet; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, PythonVersion}; use ruff_python_literal::format::FormatSpec; -use ruff_python_parser::parse_expression; +use ruff_python_parser::{UnsupportedSyntaxErrorKind, parse_expression}; use ruff_python_semantic::analyze::logging::is_logger_candidate; use ruff_python_semantic::{Modules, SemanticModel, TypingOnlyBindingsStatus}; use ruff_text_size::{Ranged, TextRange}; @@ -200,6 +200,18 @@ fn should_be_fstring( return false; }; + // For Python < 3.12, reject if the parser detected any PEP 701 f-string + // features. + if target_version < PythonVersion::PY312 { + let has_pep701 = parsed + .unsupported_syntax_errors() + .iter() + .any(|e| matches!(e.kind, UnsupportedSyntaxErrorKind::Pep701FString(_))); + if has_pep701 { + return false; + } + } + // Note: Range offsets for `value` are based on `fstring_expr` let ast::Expr::FString(ast::ExprFString { value, .. }) = parsed.expr() else { return false; @@ -226,13 +238,6 @@ fn should_be_fstring( for f_string in value.f_strings() { let mut has_name = false; for element in f_string.elements.interpolations() { - // Check if the interpolation expression contains backslashes - // F-strings with backslashes in interpolations are only valid in Python 3.12+ - let interpolation_text = &fstring_expr[element.range()]; - if target_version < PythonVersion::PY312 && interpolation_text.contains('\\') { - return false; - } - if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() { if arg_names.contains(id) { 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 977628639da71..30ec476ed013f 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 @@ -328,6 +328,8 @@ RUF027 [*] Possible f-string without an `f` prefix 91 | x = "test" 92 | print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 | ^^^^^^^^^^^^^^^^^^ +93 | +94 | # Test case for comment handling in f-string interpolations | help: Add `f` prefix 89 | # Should not trigger RUF027 for Python < 3.12 due to backslashes in interpolations @@ -335,4 +337,91 @@ help: Add `f` prefix 91 | x = "test" - print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 92 + print(f"Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 +93 | +94 | # Test case for comment handling in f-string interpolations +95 | # Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:98:11 + | + 96 | def comment_test(): + 97 | x = "!" + 98 | print("""{x # } + | ___________^ + 99 | | }""") + | |____^ +100 | +101 | # Test case for `#` inside a nested string literal in interpolation + | +help: Add `f` prefix +95 | # Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +96 | def comment_test(): +97 | x = "!" + - print("""{x # } +98 + print(f"""{x # } +99 | }""") +100 | +101 | # Test case for `#` inside a nested string literal in interpolation +note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:105:11 + | +103 | def hash_in_string_test(): +104 | x = "world" +105 | print("Hello {'#'}{x}") # RUF027: `#` is inside a string, not a comment + | ^^^^^^^^^^^^^^^^ +106 | print("Hello {\"#\"}{x}") # RUF027: same, double-quoted + | +help: Add `f` prefix +102 | # `#` inside a string is NOT a comment — should trigger RUF027 even on Python < 3.12 +103 | def hash_in_string_test(): +104 | x = "world" + - print("Hello {'#'}{x}") # RUF027: `#` is inside a string, not a comment +105 + print(f"Hello {'#'}{x}") # RUF027: `#` is inside a string, not a comment +106 | print("Hello {\"#\"}{x}") # RUF027: same, double-quoted +107 | +108 | # Test case for `#` in format spec (e.g., `{1:#x}`) +note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:112:11 + | +110 | def hash_in_format_spec_test(): +111 | n = 255 +112 | print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment + | ^^^^^^^^^^^^^ +113 | print("Oct: {n:#o}") # RUF027: same + | +help: Add `f` prefix +109 | # `#` in a format spec is NOT a comment — should trigger RUF027 even on Python < 3.12 +110 | def hash_in_format_spec_test(): +111 | n = 255 + - print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment +112 + print(f"Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment +113 | print("Oct: {n:#o}") # RUF027: same +114 | +115 | # Test case for `#` in nested interpolation inside format spec (e.g., `{1:{x #}}`) +note: This is an unsafe fix and may change runtime behavior + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:113:11 + | +111 | n = 255 +112 | print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment +113 | print("Oct: {n:#o}") # RUF027: same + | ^^^^^^^^^^^^^ +114 | +115 | # Test case for `#` in nested interpolation inside format spec (e.g., `{1:{x #}}`) + | +help: Add `f` prefix +110 | def hash_in_format_spec_test(): +111 | n = 255 +112 | print("Hex: {n:#x}") # RUF027: `#` is in format spec, not a comment + - print("Oct: {n:#o}") # RUF027: same +113 + print(f"Oct: {n:#o}") # RUF027: same +114 | +115 | # Test case for `#` in nested interpolation inside format spec (e.g., `{1:{x #}}`) +116 | # The `#` is a comment inside a nested interpolation — should NOT trigger RUF027 on Python < 3.12 note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap index 8e6692f7a5628..beb416fa399a2 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap @@ -6,10 +6,33 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs +linter.unresolved_target_version = 3.11 --- Summary --- -Removed: 2 +Removed: 4 Added: 0 --- Removed --- +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:41:22 + | +39 | single_line = """ {a} """ # RUF027 +40 | # RUF027 +41 | multi_line = a = """b { # comment + | ______________________^ +42 | | c} d +43 | | """ + | |_______^ + | +help: Add `f` prefix +38 | c = a +39 | single_line = """ {a} """ # RUF027 +40 | # RUF027 + - multi_line = a = """b { # comment +41 + multi_line = a = f"""b { # comment +42 | c} d +43 | """ +44 | +note: This is an unsafe fix and may change runtime behavior + + RUF027 [*] Possible f-string without an `f` prefix --> RUF027_0.py:49:9 | @@ -40,6 +63,8 @@ RUF027 [*] Possible f-string without an `f` prefix 91 | x = "test" 92 | print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 | ^^^^^^^^^^^^^^^^^^ +93 | +94 | # Test case for comment handling in f-string interpolations | help: Add `f` prefix 89 | # Should not trigger RUF027 for Python < 3.12 due to backslashes in interpolations @@ -47,4 +72,31 @@ help: Add `f` prefix 91 | x = "test" - print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 92 + print(f"Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12 +93 | +94 | # Test case for comment handling in f-string interpolations +95 | # Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +note: This is an unsafe fix and may change runtime behavior + + +RUF027 [*] Possible f-string without an `f` prefix + --> RUF027_0.py:98:11 + | + 96 | def comment_test(): + 97 | x = "!" + 98 | print("""{x # } + | ___________^ + 99 | | }""") + | |____^ +100 | +101 | # Test case for `#` inside a nested string literal in interpolation + | +help: Add `f` prefix +95 | # Should not trigger RUF027 for Python < 3.12 due to comments in interpolations +96 | def comment_test(): +97 | x = "!" + - print("""{x # } +98 + print(f"""{x # } +99 | }""") +100 | +101 | # Test case for `#` inside a nested string literal in interpolation note: This is an unsafe fix and may change runtime behavior