This commit is contained in:
Dan Parizher 2025-12-16 16:40:21 -05:00 committed by GitHub
commit 45af9a6acb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 352 additions and 2 deletions

View File

@ -84,3 +84,9 @@ def in_type_def():
# https://github.com/astral-sh/ruff/issues/18860 # https://github.com/astral-sh/ruff/issues/18860
def fuzz_bug(): def fuzz_bug():
c('{\t"i}') c('{\t"i}')
# Test case for backslash handling in f-string interpolations
# Should not trigger RUF027 for Python < 3.12 due to backslashes in interpolations
def backslash_test():
x = "test"
print("Hello {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12

View File

@ -129,6 +129,21 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn missing_fstring_syntax_backslash_py311() -> Result<()> {
let diagnostics = test_path(
Path::new("ruff/RUF027_0.py"),
&LinterSettings {
unresolved_target_version: PythonVersion::PY311.into(),
..LinterSettings::for_rule(Rule::MissingFStringSyntax)
},
)?;
// With Python 3.11, backslashes in interpolations should NOT trigger RUF027
// (only the backslash_test function should be skipped)
assert_diagnostics!(diagnostics);
Ok(())
}
#[test] #[test]
fn prefer_parentheses_getitem_tuple() -> Result<()> { fn prefer_parentheses_getitem_tuple() -> Result<()> {
let diagnostics = test_path( let diagnostics = test_path(

View File

@ -2,7 +2,7 @@ use memchr::memchr2_iter;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use ruff_macros::{ViolationMetadata, derive_message_formats}; 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_literal::format::FormatSpec;
use ruff_python_parser::parse_expression; use ruff_python_parser::parse_expression;
use ruff_python_semantic::analyze::logging::is_logger_candidate; use ruff_python_semantic::analyze::logging::is_logger_candidate;
@ -116,7 +116,12 @@ pub(crate) fn missing_fstring_syntax(checker: &Checker, literal: &ast::StringLit
return; return;
} }
if should_be_fstring(literal, checker.locator(), semantic) { if should_be_fstring(
literal,
checker.locator(),
semantic,
checker.target_version(),
) {
checker checker
.report_diagnostic(MissingFStringSyntax, literal.range()) .report_diagnostic(MissingFStringSyntax, literal.range())
.set_fix(fix_fstring_syntax(literal.range())); .set_fix(fix_fstring_syntax(literal.range()));
@ -180,6 +185,7 @@ fn should_be_fstring(
literal: &ast::StringLiteral, literal: &ast::StringLiteral,
locator: &Locator, locator: &Locator,
semantic: &SemanticModel, semantic: &SemanticModel,
target_version: PythonVersion,
) -> bool { ) -> bool {
if !has_brackets(&literal.value) { if !has_brackets(&literal.value) {
return false; return false;
@ -216,6 +222,13 @@ fn should_be_fstring(
for f_string in value.f_strings() { for f_string in value.f_strings() {
let mut has_name = false; let mut has_name = false;
for element in f_string.elements.interpolations() { 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 interpolation_text.contains('\\') && target_version < PythonVersion::PY312 {
return false;
}
if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() { if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() {
if arg_names.contains(id) { if arg_names.contains(id) {
return false; return false;

View File

@ -320,3 +320,19 @@ help: Add `f` prefix
76 | # fstrings are never correct as type definitions 76 | # fstrings are never correct as type definitions
77 | # so we should always skip those 77 | # so we should always skip those
note: This is an unsafe fix and may change runtime behavior 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 {'\\n'}{x}") # Should not trigger RUF027 for Python < 3.12
| ^^^^^^^^^^^^^^^^^^
|
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
note: This is an unsafe fix and may change runtime behavior

View File

@ -0,0 +1,300 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:5:7
|
3 | "always ignore this: {val}"
4 |
5 | print("but don't ignore this: {val}") # RUF027
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `f` prefix
2 |
3 | "always ignore this: {val}"
4 |
- print("but don't ignore this: {val}") # RUF027
5 + print(f"but don't ignore this: {val}") # RUF027
6 |
7 |
8 | def simple_cases():
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:10:9
|
8 | def simple_cases():
9 | a = 4
10 | b = "{a}" # RUF027
| ^^^^^
11 | c = "{a} {b} f'{val}' " # RUF027
|
help: Add `f` prefix
7 |
8 | def simple_cases():
9 | a = 4
- b = "{a}" # RUF027
10 + b = f"{a}" # RUF027
11 | c = "{a} {b} f'{val}' " # RUF027
12 |
13 |
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:11:9
|
9 | a = 4
10 | b = "{a}" # RUF027
11 | c = "{a} {b} f'{val}' " # RUF027
| ^^^^^^^^^^^^^^^^^^^
|
help: Add `f` prefix
8 | def simple_cases():
9 | a = 4
10 | b = "{a}" # RUF027
- c = "{a} {b} f'{val}' " # RUF027
11 + c = f"{a} {b} f'{val}' " # RUF027
12 |
13 |
14 | def escaped_string():
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:21:9
|
19 | def raw_string():
20 | a = 4
21 | b = r"raw string with formatting: {a}" # RUF027
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
22 | c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
|
help: Add `f` prefix
18 |
19 | def raw_string():
20 | a = 4
- b = r"raw string with formatting: {a}" # RUF027
21 + b = fr"raw string with formatting: {a}" # RUF027
22 | c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
23 |
24 |
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:22:9
|
20 | a = 4
21 | b = r"raw string with formatting: {a}" # RUF027
22 | c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `f` prefix
19 | def raw_string():
20 | a = 4
21 | b = r"raw string with formatting: {a}" # RUF027
- c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
22 + c = fr"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027
23 |
24 |
25 | def print_name(name: str):
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:27:11
|
25 | def print_name(name: str):
26 | a = 4
27 | print("Hello, {name}!") # RUF027
| ^^^^^^^^^^^^^^^^
28 | print("The test value we're using today is {a}") # RUF027
|
help: Add `f` prefix
24 |
25 | def print_name(name: str):
26 | a = 4
- print("Hello, {name}!") # RUF027
27 + print(f"Hello, {name}!") # RUF027
28 | print("The test value we're using today is {a}") # RUF027
29 |
30 |
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:28:11
|
26 | a = 4
27 | print("Hello, {name}!") # RUF027
28 | print("The test value we're using today is {a}") # RUF027
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `f` prefix
25 | def print_name(name: str):
26 | a = 4
27 | print("Hello, {name}!") # RUF027
- print("The test value we're using today is {a}") # RUF027
28 + print(f"The test value we're using today is {a}") # RUF027
29 |
30 |
31 | def nested_funcs():
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:33:33
|
31 | def nested_funcs():
32 | a = 4
33 | print(do_nothing(do_nothing("{a}"))) # RUF027
| ^^^^^
|
help: Add `f` prefix
30 |
31 | def nested_funcs():
32 | a = 4
- print(do_nothing(do_nothing("{a}"))) # RUF027
33 + print(do_nothing(do_nothing(f"{a}"))) # RUF027
34 |
35 |
36 | def tripled_quoted():
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:39:19
|
37 | a = 4
38 | c = a
39 | single_line = """ {a} """ # RUF027
| ^^^^^^^^^^^
40 | # RUF027
41 | multi_line = a = """b { # comment
|
help: Add `f` prefix
36 | def tripled_quoted():
37 | a = 4
38 | c = a
- single_line = """ {a} """ # RUF027
39 + single_line = f""" {a} """ # RUF027
40 | # RUF027
41 | multi_line = a = """b { # comment
42 | c} d
note: This is an unsafe fix and may change runtime behavior
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:56:9
|
54 | def implicit_concat():
55 | a = 4
56 | b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
| ^^^^^
57 | print(f"{a}" "{a}" f"{b}") # RUF027
|
help: Add `f` prefix
53 |
54 | def implicit_concat():
55 | a = 4
- b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
56 + b = f"{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
57 | print(f"{a}" "{a}" f"{b}") # RUF027
58 |
59 |
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:57:18
|
55 | a = 4
56 | b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
57 | print(f"{a}" "{a}" f"{b}") # RUF027
| ^^^^^
|
help: Add `f` prefix
54 | def implicit_concat():
55 | a = 4
56 | b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only
- print(f"{a}" "{a}" f"{b}") # RUF027
57 + print(f"{a}" f"{a}" f"{b}") # RUF027
58 |
59 |
60 | def escaped_chars():
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:62:9
|
60 | def escaped_chars():
61 | a = 4
62 | b = "\"not escaped:\" '{a}' \"escaped:\": '{{c}}'" # RUF027
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add `f` prefix
59 |
60 | def escaped_chars():
61 | a = 4
- b = "\"not escaped:\" '{a}' \"escaped:\": '{{c}}'" # RUF027
62 + b = f"\"not escaped:\" '{a}' \"escaped:\": '{{c}}'" # RUF027
63 |
64 |
65 | def method_calls():
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:70:18
|
68 | first = "Wendy"
69 | last = "Appleseed"
70 | value.method("{first} {last}") # RUF027
| ^^^^^^^^^^^^^^^^
71 |
72 | def format_specifiers():
|
help: Add `f` prefix
67 | value.method = print_name
68 | first = "Wendy"
69 | last = "Appleseed"
- value.method("{first} {last}") # RUF027
70 + value.method(f"{first} {last}") # RUF027
71 |
72 | def format_specifiers():
73 | a = 4
note: This is an unsafe fix and may change runtime behavior
RUF027 [*] Possible f-string without an `f` prefix
--> RUF027_0.py:74:9
|
72 | def format_specifiers():
73 | a = 4
74 | b = "{a:b} {a:^5}"
| ^^^^^^^^^^^^^^
75 |
76 | # fstrings are never correct as type definitions
|
help: Add `f` prefix
71 |
72 | def format_specifiers():
73 | a = 4
- b = "{a:b} {a:^5}"
74 + b = f"{a:b} {a:^5}"
75 |
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