mirror of https://github.com/astral-sh/ruff
Fix RUF027 handling for f-string backslashes in Python 3.11
Updates the MissingFStringSyntax rule to correctly skip diagnostics for f-string interpolations containing backslashes when targeting Python 3.11, matching Python's behavior. Adds a dedicated test and snapshot to verify the change.
This commit is contained in:
parent
4b4447664b
commit
95efd8c4dc
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -116,7 +116,12 @@ pub(crate) fn missing_fstring_syntax(checker: &Checker, literal: &ast::StringLit
|
|||
return;
|
||||
}
|
||||
|
||||
if should_be_fstring(literal, checker.locator(), semantic, checker) {
|
||||
if should_be_fstring(
|
||||
literal,
|
||||
checker.locator(),
|
||||
semantic,
|
||||
checker.target_version(),
|
||||
) {
|
||||
checker
|
||||
.report_diagnostic(MissingFStringSyntax, literal.range())
|
||||
.set_fix(fix_fstring_syntax(literal.range()));
|
||||
|
|
@ -180,7 +185,7 @@ fn should_be_fstring(
|
|||
literal: &ast::StringLiteral,
|
||||
locator: &Locator,
|
||||
semantic: &SemanticModel,
|
||||
checker: &Checker,
|
||||
target_version: PythonVersion,
|
||||
) -> bool {
|
||||
if !has_brackets(&literal.value) {
|
||||
return false;
|
||||
|
|
@ -220,8 +225,7 @@ fn should_be_fstring(
|
|||
// 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('\\') && checker.target_version() < PythonVersion::PY312
|
||||
{
|
||||
if interpolation_text.contains('\\') && target_version < PythonVersion::PY312 {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue