Skip to content

Commit 3938e1c

Browse files
committed
Fix format literals
1 parent a637b8b commit 3938e1c

File tree

8 files changed

+374
-102
lines changed

8 files changed

+374
-102
lines changed

crates/ruff/resources/test/fixtures/pyupgrade/UP030_0.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,30 @@
3232
)
3333

3434
'{' '0}'.format(1)
35+
36+
args = list(range(10))
37+
kwargs = {x: x for x in range(10)}
38+
39+
"{0}".format(*args)
40+
41+
"{0}".format(**kwargs)
42+
43+
"{0}_{1}".format(*args)
44+
45+
"{0}_{1}".format(1, *args)
46+
47+
"{0}_{1}".format(1, 2, *args)
48+
49+
"{0}_{1}".format(*args, 1, 2)
50+
51+
"{0}_{1}_{2}".format(1, **kwargs)
52+
53+
"{0}_{1}_{2}".format(1, 2, **kwargs)
54+
55+
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
56+
57+
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)
58+
59+
"{1}_{0}".format(1, 2, *args)
60+
61+
"{1}_{0}".format(1, 2)

crates/ruff/resources/test/fixtures/pyupgrade/UP030_1.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,17 @@
1515
print(f"{0}".format(1))
1616

1717
''.format(1)
18+
19+
'{1} {0}'.format(*args)
20+
21+
"{1}_{0}".format(*args, 1)
22+
23+
"{1}_{0}".format(*args, 1, 2)
24+
25+
"{1}_{0}".format(1, **kwargs)
26+
27+
"{1}_{0}".format(1, foo=2)
28+
29+
"{1}_{0}".format(1, 2, **kwargs)
30+
31+
"{1}_{0}".format(1, 2, foo=3, bar=4)

crates/ruff/resources/test/fixtures/pyupgrade/UP030_2.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

crates/ruff/src/checkers/ast/analyze/expression.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
410410
);
411411
}
412412
if checker.enabled(Rule::FormatLiterals) {
413-
pyupgrade::rules::format_literals(checker, &summary, expr);
413+
pyupgrade::rules::format_literals(checker, &summary, call);
414414
}
415415
if checker.enabled(Rule::FString) {
416416
pyupgrade::rules::f_strings(

crates/ruff/src/rules/pyupgrade/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ mod tests {
3030
#[test_case(Rule::FString, Path::new("UP032_2.py"))]
3131
#[test_case(Rule::FormatLiterals, Path::new("UP030_0.py"))]
3232
#[test_case(Rule::FormatLiterals, Path::new("UP030_1.py"))]
33-
#[test_case(Rule::FormatLiterals, Path::new("UP030_2.py"))]
3433
#[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_0.py"))]
3534
#[test_case(Rule::LRUCacheWithMaxsizeNone, Path::new("UP033_1.py"))]
3635
#[test_case(Rule::LRUCacheWithoutParameters, Path::new("UP011.py"))]

crates/ruff/src/rules/pyupgrade/rules/format_literals.rs

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
22
use libcst_native::{Arg, Expression};
33
use once_cell::sync::Lazy;
44
use regex::Regex;
5-
use ruff_python_ast::{Expr, Ranged};
5+
use ruff_python_ast::{self as ast, Expr, Ranged};
66

77
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
88
use ruff_macros::{derive_message_formats, violation};
@@ -55,6 +55,77 @@ impl Violation for FormatLiterals {
5555
}
5656
}
5757

58+
/// UP030
59+
pub(crate) fn format_literals(
60+
checker: &mut Checker,
61+
summary: &FormatSummary,
62+
call: &ast::ExprCall,
63+
) {
64+
// The format we expect is, e.g.: `"{0} {1}".format(...)`
65+
if summary.has_nested_parts {
66+
return;
67+
}
68+
if !summary.keywords.is_empty() {
69+
return;
70+
}
71+
if !summary.autos.is_empty() {
72+
return;
73+
}
74+
if summary.indices.is_empty() {
75+
return;
76+
}
77+
if (0..summary.indices.len()).any(|index| !summary.indices.contains(&index)) {
78+
return;
79+
}
80+
81+
// If the positional indices aren't sequential (e.g., `"{1} {0}".format(1, 2)`), then we
82+
// need to reorder the function arguments; so we need to ensure that the function
83+
// arguments aren't splatted (e.g., `"{1} {0}".format(*foo)`), that there are a sufficient
84+
// number of them, etc.
85+
let arguments = if is_sequential(&summary.indices) {
86+
Arguments::Preserve
87+
} else {
88+
// Ex) `"{1} {0}".format(foo=1, bar=2)`
89+
if !call.arguments.keywords.is_empty() {
90+
return;
91+
}
92+
93+
// Ex) `"{1} {0}".format(foo)`
94+
if call.arguments.args.len() < summary.indices.len() {
95+
return;
96+
}
97+
98+
// Ex) `"{1} {0}".format(*foo)`
99+
if call
100+
.arguments
101+
.args
102+
.iter()
103+
.take(summary.indices.len())
104+
.any(Expr::is_starred_expr)
105+
{
106+
return;
107+
}
108+
109+
Arguments::Reorder(&summary.indices)
110+
};
111+
112+
let mut diagnostic = Diagnostic::new(FormatLiterals, call.range());
113+
if checker.patch(diagnostic.kind.rule()) {
114+
diagnostic.try_set_fix(|| {
115+
Ok(Fix::suggested(Edit::range_replacement(
116+
generate_call(call, arguments, checker.locator(), checker.stylist())?,
117+
call.range(),
118+
)))
119+
});
120+
}
121+
checker.diagnostics.push(diagnostic);
122+
}
123+
124+
/// Returns true if the indices are sequential.
125+
fn is_sequential(indices: &[usize]) -> bool {
126+
indices.iter().enumerate().all(|(idx, value)| idx == *value)
127+
}
128+
58129
// An opening curly brace, followed by any integer, followed by any text,
59130
// followed by a closing brace.
60131
static FORMAT_SPECIFIER: Lazy<Regex> =
@@ -118,26 +189,30 @@ fn generate_arguments<'a>(arguments: &[Arg<'a>], order: &'a [usize]) -> Result<V
118189
Ok(new_arguments)
119190
}
120191

121-
/// Returns true if the indices are sequential.
122-
fn is_sequential(indices: &[usize]) -> bool {
123-
indices.iter().enumerate().all(|(idx, value)| idx == *value)
192+
#[derive(Debug, Copy, Clone)]
193+
enum Arguments<'a> {
194+
/// Preserve the arguments to the `.format(...)` call.
195+
Preserve,
196+
/// Reorder the arguments to the `.format(...)` call, based on the given
197+
/// indices.
198+
Reorder(&'a [usize]),
124199
}
125200

126201
/// Returns the corrected function call.
127202
fn generate_call(
128-
expr: &Expr,
129-
correct_order: &[usize],
203+
call: &ast::ExprCall,
204+
arguments: Arguments,
130205
locator: &Locator,
131206
stylist: &Stylist,
132207
) -> Result<String> {
133-
let content = locator.slice(expr.range());
208+
let content = locator.slice(call.range());
134209
let parenthesized_content = format!("({content})");
135210
let mut expression = match_expression(&parenthesized_content)?;
136211

137212
// Fix the call arguments.
138213
let call = match_call_mut(&mut expression)?;
139-
if !is_sequential(correct_order) {
140-
call.args = generate_arguments(&call.args, correct_order)?;
214+
if let Arguments::Reorder(order) = arguments {
215+
call.args = generate_arguments(&call.args, order)?;
141216
}
142217

143218
// Fix the string itself.
@@ -157,34 +232,3 @@ fn generate_call(
157232

158233
Ok(output)
159234
}
160-
161-
/// UP030
162-
pub(crate) fn format_literals(checker: &mut Checker, summary: &FormatSummary, expr: &Expr) {
163-
// The format we expect is, e.g.: `"{0} {1}".format(...)`
164-
if summary.has_nested_parts {
165-
return;
166-
}
167-
if !summary.keywords.is_empty() {
168-
return;
169-
}
170-
if !summary.autos.is_empty() {
171-
return;
172-
}
173-
if summary.indices.is_empty() {
174-
return;
175-
}
176-
if (0..summary.indices.len()).any(|index| !summary.indices.contains(&index)) {
177-
return;
178-
}
179-
180-
let mut diagnostic = Diagnostic::new(FormatLiterals, expr.range());
181-
if checker.patch(diagnostic.kind.rule()) {
182-
diagnostic.try_set_fix(|| {
183-
Ok(Fix::suggested(Edit::range_replacement(
184-
generate_call(expr, &summary.indices, checker.locator(), checker.stylist())?,
185-
expr.range(),
186-
)))
187-
});
188-
}
189-
checker.diagnostics.push(diagnostic);
190-
}

0 commit comments

Comments
 (0)