fix: Avoid autofix for raw strings with \N escapes

This commit is contained in:
Denys Zhak 2025-12-22 23:47:41 +01:00
parent 4a937543b9
commit f8a26700f0
3 changed files with 42 additions and 7 deletions

View file

@ -276,5 +276,8 @@ if __name__ == "__main__":
string = "{}".format(number := number + 1)
print(string)
# Unicode escape
# Unicode escape in regular string, should convert.
"\N{angle}AOB = {angle}°".format(angle=180)
# Unicode escape in raw string, should not convert - would change semantics.
r"\N{angle}AOB = {angle}°".format(angle=180)

View file

@ -229,6 +229,9 @@ enum FStringConversion {
/// The format call uses arguments with side effects which are repeated within the
/// format string. For example: `"{x} {x}".format(x=foo())`.
SideEffects,
/// The format string is a raw string containing `\N{...}` which would be
/// misinterpreted in an f-string.
UnsafeRawString,
/// The format string should be converted to an f-string.
Convert(String),
}
@ -286,6 +289,15 @@ impl FStringConversion {
return Ok(Self::NonEmptyLiteral);
}
// `\N{...}` is literal in raw strings but becomes a Unicode escape in f-strings.
if raw
&& format_string.format_parts.iter().any(
|part| matches!(part, FormatPart::Literal(literal) if literal.contains("\\N{")),
)
{
return Ok(Self::UnsafeRawString);
}
let mut converted = String::with_capacity(contents.len());
let mut seen = FxHashSet::default();
for part in format_string.format_parts {
@ -416,6 +428,7 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma
let mut patches: Vec<(TextRange, FStringConversion)> = vec![];
let mut tokens = checker.tokens().in_range(call.func.range()).iter();
let mut unsafe_conversion = false;
let end = loop {
let Some(token) = tokens.next() else {
unreachable!("Should break from the `Tok::Dot` arm");
@ -441,6 +454,11 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma
// If the format string contains side effects that would need to be repeated,
// we can't convert it to an f-string.
Ok(FStringConversion::SideEffects) => return,
// If the format string is a raw string with `\N{...}`, conversion would be unsafe.
// We still want to emit the diagnostic, but without offering a fix.
Ok(FStringConversion::UnsafeRawString) => {
unsafe_conversion = true;
}
// If any of the segments fail to convert, then we can't convert the entire
// expression.
Err(_) => return,
@ -451,7 +469,7 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma
_ => {}
}
};
if patches.is_empty() {
if patches.is_empty() && !unsafe_conversion {
return;
}
@ -466,7 +484,7 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma
Some(curly_unescape(checker.locator().slice(range)).to_string())
}
// We handled this in the previous loop.
FStringConversion::SideEffects => unreachable!(),
FStringConversion::SideEffects | FStringConversion::UnsafeRawString => unreachable!(),
};
if let Some(fstring) = fstring {
contents.push_str(
@ -520,7 +538,7 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma
// ```
let has_comments = checker.comment_ranges().intersects(call.arguments.range());
if !has_comments {
if !has_comments && !unsafe_conversion {
if contents.is_empty() {
// Ex) `''.format(self.project)`
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(

View file

@ -1370,18 +1370,32 @@ help: Convert to f-string
276 + string = f"{(number := number + 1)}"
277 | print(string)
278 |
279 | # Unicode escape
279 | # Unicode escape in regular string, should convert.
UP032 [*] Use f-string instead of `format` call
--> UP032_0.py:280:1
|
279 | # Unicode escape
279 | # Unicode escape in regular string, should convert.
280 | "\N{angle}AOB = {angle}°".format(angle=180)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
281 |
282 | # Unicode escape in raw string, should not convert - would change semantics.
|
help: Convert to f-string
277 | print(string)
278 |
279 | # Unicode escape
279 | # Unicode escape in regular string, should convert.
- "\N{angle}AOB = {angle}°".format(angle=180)
280 + f"\N{angle}AOB = {180}°"
281 |
282 | # Unicode escape in raw string, should not convert - would change semantics.
283 | r"\N{angle}AOB = {angle}°".format(angle=180)
UP032 Use f-string instead of `format` call
--> UP032_0.py:283:1
|
282 | # Unicode escape in raw string, should not convert - would change semantics.
283 | r"\N{angle}AOB = {angle}°".format(angle=180)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to f-string