mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:48 +00:00
Avoid syntax errors when removing f-string prefixes (#5319)
Closes https://github.com/astral-sh/ruff/issues/5281. Closes https://github.com/astral-sh/ruff/issues/4827.
This commit is contained in:
parent
4a81cfc51a
commit
7819b95d7f
4 changed files with 118 additions and 31 deletions
|
@ -37,6 +37,9 @@ f"{{test}}"
|
||||||
f'{{ 40 }}'
|
f'{{ 40 }}'
|
||||||
f"{{a {{x}}"
|
f"{{a {{x}}"
|
||||||
f"{{{{x}}}}"
|
f"{{{{x}}}}"
|
||||||
|
""f""
|
||||||
|
''f""
|
||||||
|
(""f""r"")
|
||||||
|
|
||||||
# To be fixed
|
# To be fixed
|
||||||
# Error: f-string: single '}' is not allowed at line 41 column 8
|
# Error: f-string: single '}' is not allowed at line 41 column 8
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use rustpython_parser::ast::{self, Expr, Stmt};
|
use rustpython_parser::ast::{self, Stmt};
|
||||||
|
|
||||||
use ruff_diagnostics::{Diagnostic, Violation};
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
@ -50,9 +50,9 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
|
||||||
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
|
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Expr::JoinedStr ( _) = value.as_ref() else {
|
if !value.is_joined_str_expr() {
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
checker
|
checker
|
||||||
.diagnostics
|
.diagnostics
|
||||||
.push(Diagnostic::new(FStringDocstring, stmt.identifier()));
|
.push(Diagnostic::new(FStringDocstring, stmt.identifier()));
|
||||||
|
|
|
@ -79,23 +79,6 @@ fn find_useless_f_strings<'a>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unescape_f_string(content: &str) -> String {
|
|
||||||
content.replace("{{", "{").replace("}}", "}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fix_f_string_missing_placeholders(
|
|
||||||
prefix_range: TextRange,
|
|
||||||
tok_range: TextRange,
|
|
||||||
checker: &mut Checker,
|
|
||||||
) -> Fix {
|
|
||||||
let content = &checker.locator.contents()[TextRange::new(prefix_range.end(), tok_range.end())];
|
|
||||||
Fix::automatic(Edit::replacement(
|
|
||||||
unescape_f_string(content),
|
|
||||||
prefix_range.start(),
|
|
||||||
tok_range.end(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// F541
|
/// F541
|
||||||
pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) {
|
pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) {
|
||||||
if !values
|
if !values
|
||||||
|
@ -105,13 +88,51 @@ pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checke
|
||||||
for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator) {
|
for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator) {
|
||||||
let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range);
|
let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range);
|
||||||
if checker.patch(diagnostic.kind.rule()) {
|
if checker.patch(diagnostic.kind.rule()) {
|
||||||
diagnostic.set_fix(fix_f_string_missing_placeholders(
|
diagnostic.set_fix(convert_f_string_to_regular_string(
|
||||||
prefix_range,
|
prefix_range,
|
||||||
tok_range,
|
tok_range,
|
||||||
checker,
|
checker.locator,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unescape an f-string body by replacing `{{` with `{` and `}}` with `}`.
|
||||||
|
///
|
||||||
|
/// In Python, curly-brace literals within f-strings must be escaped by doubling the braces.
|
||||||
|
/// When rewriting an f-string to a regular string, we need to unescape any curly-brace literals.
|
||||||
|
/// For example, given `{{Hello, world!}}`, return `{Hello, world!}`.
|
||||||
|
fn unescape_f_string(content: &str) -> String {
|
||||||
|
content.replace("{{", "{").replace("}}", "}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a [`Fix`] to rewrite an f-string as a regular string.
|
||||||
|
fn convert_f_string_to_regular_string(
|
||||||
|
prefix_range: TextRange,
|
||||||
|
tok_range: TextRange,
|
||||||
|
locator: &Locator,
|
||||||
|
) -> Fix {
|
||||||
|
// Extract the f-string body.
|
||||||
|
let mut content =
|
||||||
|
unescape_f_string(locator.slice(TextRange::new(prefix_range.end(), tok_range.end())));
|
||||||
|
|
||||||
|
// If the preceding character is equivalent to the quote character, insert a space to avoid a
|
||||||
|
// syntax error. For example, when removing the `f` prefix in `""f""`, rewrite to `"" ""`
|
||||||
|
// instead of `""""`.
|
||||||
|
if locator
|
||||||
|
.slice(TextRange::up_to(prefix_range.start()))
|
||||||
|
.chars()
|
||||||
|
.last()
|
||||||
|
.map_or(false, |char| content.starts_with(char))
|
||||||
|
{
|
||||||
|
content.insert(0, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
Fix::automatic(Edit::replacement(
|
||||||
|
content,
|
||||||
|
prefix_range.start(),
|
||||||
|
tok_range.end(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
@ -269,7 +269,7 @@ F541.py:37:1: F541 [*] f-string without any placeholders
|
||||||
37 |+'{ 40 }'
|
37 |+'{ 40 }'
|
||||||
38 38 | f"{{a {{x}}"
|
38 38 | f"{{a {{x}}"
|
||||||
39 39 | f"{{{{x}}}}"
|
39 39 | f"{{{{x}}}}"
|
||||||
40 40 |
|
40 40 | ""f""
|
||||||
|
|
||||||
F541.py:38:1: F541 [*] f-string without any placeholders
|
F541.py:38:1: F541 [*] f-string without any placeholders
|
||||||
|
|
|
|
||||||
|
@ -278,6 +278,7 @@ F541.py:38:1: F541 [*] f-string without any placeholders
|
||||||
38 | f"{{a {{x}}"
|
38 | f"{{a {{x}}"
|
||||||
| ^^^^^^^^^^^^ F541
|
| ^^^^^^^^^^^^ F541
|
||||||
39 | f"{{{{x}}}}"
|
39 | f"{{{{x}}}}"
|
||||||
|
40 | ""f""
|
||||||
|
|
|
|
||||||
= help: Remove extraneous `f` prefix
|
= help: Remove extraneous `f` prefix
|
||||||
|
|
||||||
|
@ -288,8 +289,8 @@ F541.py:38:1: F541 [*] f-string without any placeholders
|
||||||
38 |-f"{{a {{x}}"
|
38 |-f"{{a {{x}}"
|
||||||
38 |+"{a {x}"
|
38 |+"{a {x}"
|
||||||
39 39 | f"{{{{x}}}}"
|
39 39 | f"{{{{x}}}}"
|
||||||
40 40 |
|
40 40 | ""f""
|
||||||
41 41 | # To be fixed
|
41 41 | ''f""
|
||||||
|
|
||||||
F541.py:39:1: F541 [*] f-string without any placeholders
|
F541.py:39:1: F541 [*] f-string without any placeholders
|
||||||
|
|
|
|
||||||
|
@ -297,8 +298,8 @@ F541.py:39:1: F541 [*] f-string without any placeholders
|
||||||
38 | f"{{a {{x}}"
|
38 | f"{{a {{x}}"
|
||||||
39 | f"{{{{x}}}}"
|
39 | f"{{{{x}}}}"
|
||||||
| ^^^^^^^^^^^^ F541
|
| ^^^^^^^^^^^^ F541
|
||||||
40 |
|
40 | ""f""
|
||||||
41 | # To be fixed
|
41 | ''f""
|
||||||
|
|
|
|
||||||
= help: Remove extraneous `f` prefix
|
= help: Remove extraneous `f` prefix
|
||||||
|
|
||||||
|
@ -308,8 +309,70 @@ F541.py:39:1: F541 [*] f-string without any placeholders
|
||||||
38 38 | f"{{a {{x}}"
|
38 38 | f"{{a {{x}}"
|
||||||
39 |-f"{{{{x}}}}"
|
39 |-f"{{{{x}}}}"
|
||||||
39 |+"{{x}}"
|
39 |+"{{x}}"
|
||||||
40 40 |
|
40 40 | ""f""
|
||||||
41 41 | # To be fixed
|
41 41 | ''f""
|
||||||
42 42 | # Error: f-string: single '}' is not allowed at line 41 column 8
|
42 42 | (""f""r"")
|
||||||
|
|
||||||
|
F541.py:40:3: F541 [*] f-string without any placeholders
|
||||||
|
|
|
||||||
|
38 | f"{{a {{x}}"
|
||||||
|
39 | f"{{{{x}}}}"
|
||||||
|
40 | ""f""
|
||||||
|
| ^^^ F541
|
||||||
|
41 | ''f""
|
||||||
|
42 | (""f""r"")
|
||||||
|
|
|
||||||
|
= help: Remove extraneous `f` prefix
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
37 37 | f'{{ 40 }}'
|
||||||
|
38 38 | f"{{a {{x}}"
|
||||||
|
39 39 | f"{{{{x}}}}"
|
||||||
|
40 |-""f""
|
||||||
|
40 |+"" ""
|
||||||
|
41 41 | ''f""
|
||||||
|
42 42 | (""f""r"")
|
||||||
|
43 43 |
|
||||||
|
|
||||||
|
F541.py:41:3: F541 [*] f-string without any placeholders
|
||||||
|
|
|
||||||
|
39 | f"{{{{x}}}}"
|
||||||
|
40 | ""f""
|
||||||
|
41 | ''f""
|
||||||
|
| ^^^ F541
|
||||||
|
42 | (""f""r"")
|
||||||
|
|
|
||||||
|
= help: Remove extraneous `f` prefix
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
38 38 | f"{{a {{x}}"
|
||||||
|
39 39 | f"{{{{x}}}}"
|
||||||
|
40 40 | ""f""
|
||||||
|
41 |-''f""
|
||||||
|
41 |+''""
|
||||||
|
42 42 | (""f""r"")
|
||||||
|
43 43 |
|
||||||
|
44 44 | # To be fixed
|
||||||
|
|
||||||
|
F541.py:42:4: F541 [*] f-string without any placeholders
|
||||||
|
|
|
||||||
|
40 | ""f""
|
||||||
|
41 | ''f""
|
||||||
|
42 | (""f""r"")
|
||||||
|
| ^^^ F541
|
||||||
|
43 |
|
||||||
|
44 | # To be fixed
|
||||||
|
|
|
||||||
|
= help: Remove extraneous `f` prefix
|
||||||
|
|
||||||
|
ℹ Fix
|
||||||
|
39 39 | f"{{{{x}}}}"
|
||||||
|
40 40 | ""f""
|
||||||
|
41 41 | ''f""
|
||||||
|
42 |-(""f""r"")
|
||||||
|
42 |+("" ""r"")
|
||||||
|
43 43 |
|
||||||
|
44 44 | # To be fixed
|
||||||
|
45 45 | # Error: f-string: single '}' is not allowed at line 41 column 8
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue