Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 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 @@ -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-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
15 changes: 15 additions & 0 deletions crates/ruff_linter/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,21 @@ mod tests {
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]
fn prefer_parentheses_getitem_tuple() -> Result<()> {
let diagnostics = test_path(
Expand Down
17 changes: 15 additions & 2 deletions crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -116,7 +116,12 @@ 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.target_version(),
) {
checker
.report_diagnostic(MissingFStringSyntax, literal.range())
.set_fix(fix_fstring_syntax(literal.range()));
Expand Down Expand Up @@ -180,6 +185,7 @@ fn should_be_fstring(
literal: &ast::StringLiteral,
locator: &Locator,
semantic: &SemanticModel,
target_version: PythonVersion,
) -> bool {
if !has_brackets(&literal.value) {
return false;
Expand Down Expand Up @@ -216,6 +222,13 @@ 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 interpolation_text.contains('\\') && target_version < PythonVersion::PY312 {
return false;
}

if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() {
if arg_names.contains(id) {
return false;
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a variant of this test where we use a Python version before 3.12. This is still showing a diagnostic being emitted.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {'\\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
Original file line number Diff line number Diff line change
@@ -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