Skip to content
28 changes: 28 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF027_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 13 additions & 8 deletions crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,100 @@ 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
90 | def backslash_test():
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Expand Down Expand Up @@ -40,11 +63,40 @@ 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
90 | def backslash_test():
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