@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
2
2
use libcst_native:: { Arg , Expression } ;
3
3
use once_cell:: sync:: Lazy ;
4
4
use regex:: Regex ;
5
- use ruff_python_ast:: { Expr , Ranged } ;
5
+ use ruff_python_ast:: { self as ast , Expr , Ranged } ;
6
6
7
7
use ruff_diagnostics:: { AutofixKind , Diagnostic , Edit , Fix , Violation } ;
8
8
use ruff_macros:: { derive_message_formats, violation} ;
@@ -55,6 +55,77 @@ impl Violation for FormatLiterals {
55
55
}
56
56
}
57
57
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
+
58
129
// An opening curly brace, followed by any integer, followed by any text,
59
130
// followed by a closing brace.
60
131
static FORMAT_SPECIFIER : Lazy < Regex > =
@@ -118,26 +189,30 @@ fn generate_arguments<'a>(arguments: &[Arg<'a>], order: &'a [usize]) -> Result<V
118
189
Ok ( new_arguments)
119
190
}
120
191
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 ] ) ,
124
199
}
125
200
126
201
/// Returns the corrected function call.
127
202
fn generate_call (
128
- expr : & Expr ,
129
- correct_order : & [ usize ] ,
203
+ call : & ast :: ExprCall ,
204
+ arguments : Arguments ,
130
205
locator : & Locator ,
131
206
stylist : & Stylist ,
132
207
) -> Result < String > {
133
- let content = locator. slice ( expr . range ( ) ) ;
208
+ let content = locator. slice ( call . range ( ) ) ;
134
209
let parenthesized_content = format ! ( "({content})" ) ;
135
210
let mut expression = match_expression ( & parenthesized_content) ?;
136
211
137
212
// Fix the call arguments.
138
213
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 ) ?;
141
216
}
142
217
143
218
// Fix the string itself.
@@ -157,34 +232,3 @@ fn generate_call(
157
232
158
233
Ok ( output)
159
234
}
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