1
- use anyhow :: { Result , bail } ;
1
+ use std :: fmt :: Display ;
2
2
3
+ use anyhow:: Result ;
4
+
5
+ use libcst_native:: { LeftParen , ParenthesizedNode , RightParen } ;
3
6
use ruff_macros:: { ViolationMetadata , derive_message_formats} ;
4
- use ruff_python_ast:: { self as ast, Arguments , Expr } ;
5
- use ruff_python_codegen :: Stylist ;
7
+ use ruff_python_ast:: { self as ast, Expr , OperatorPrecedence } ;
8
+ use ruff_python_parser :: TokenKind ;
6
9
use ruff_text_size:: Ranged ;
7
10
8
- use crate :: Locator ;
9
11
use crate :: checkers:: ast:: Checker ;
12
+ use crate :: cst:: helpers:: space;
10
13
use crate :: cst:: matchers:: {
11
- match_call_mut, match_formatted_string, match_formatted_string_expression, match_name,
12
- transform_expression,
14
+ match_call_mut, match_formatted_string, match_formatted_string_expression, transform_expression,
13
15
} ;
14
- use crate :: { AlwaysFixableViolation , Edit , Fix } ;
16
+ use crate :: { Edit , Fix , FixAvailability , Violation } ;
15
17
16
18
/// ## What it does
17
19
/// Checks for uses of `str()`, `repr()`, and `ascii()` as explicit type
@@ -40,14 +42,16 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
40
42
#[ derive( ViolationMetadata ) ]
41
43
pub ( crate ) struct ExplicitFStringTypeConversion ;
42
44
43
- impl AlwaysFixableViolation for ExplicitFStringTypeConversion {
45
+ impl Violation for ExplicitFStringTypeConversion {
46
+ const FIX_AVAILABILITY : FixAvailability = FixAvailability :: Sometimes ;
47
+
44
48
#[ derive_message_formats]
45
49
fn message ( & self ) -> String {
46
50
"Use explicit conversion flag" . to_string ( )
47
51
}
48
52
49
- fn fix_title ( & self ) -> String {
50
- "Replace with conversion flag" . to_string ( )
53
+ fn fix_title ( & self ) -> Option < String > {
54
+ Some ( "Replace with conversion flag" . to_string ( ) )
51
55
}
52
56
}
53
57
@@ -68,84 +72,142 @@ pub(crate) fn explicit_f_string_type_conversion(checker: &Checker, f_string: &as
68
72
continue ;
69
73
}
70
74
71
- let Expr :: Call ( ast:: ExprCall {
72
- func,
73
- arguments :
74
- Arguments {
75
- args,
76
- keywords,
77
- range : _,
78
- node_index : _,
79
- } ,
80
- ..
81
- } ) = expression. as_ref ( )
82
- else {
75
+ let Expr :: Call ( call) = expression. as_ref ( ) else {
83
76
continue ;
84
77
} ;
85
78
86
- // Can't be a conversion otherwise.
87
- if !keywords. is_empty ( ) {
79
+ let Some ( conversion) = checker
80
+ . semantic ( )
81
+ . resolve_builtin_symbol ( & call. func )
82
+ . and_then ( Conversion :: from_str)
83
+ else {
88
84
continue ;
89
- }
85
+ } ;
86
+ let arg = match conversion {
87
+ // Handles the cases: `f"{str(object=arg)}"` and `f"{str(arg)}"`
88
+ Conversion :: Str if call. arguments . len ( ) == 1 => {
89
+ let Some ( arg) = call. arguments . find_argument_value ( "object" , 0 ) else {
90
+ continue ;
91
+ } ;
92
+ arg
93
+ }
94
+ Conversion :: Str | Conversion :: Repr | Conversion :: Ascii => {
95
+ // Can't be a conversion otherwise.
96
+ if !call. arguments . keywords . is_empty ( ) {
97
+ continue ;
98
+ }
90
99
91
- // Can't be a conversion otherwise.
92
- let [ arg] = & * * args else {
93
- continue ;
100
+ // Can't be a conversion otherwise.
101
+ let [ arg] = call. arguments . args . as_ref ( ) else {
102
+ continue ;
103
+ } ;
104
+ arg
105
+ }
94
106
} ;
95
107
96
- // Avoid attempting to rewrite, e.g., `f"{str({})}"`; the curly braces are problematic.
97
- if matches ! (
98
- arg,
99
- Expr :: Dict ( _) | Expr :: Set ( _) | Expr :: DictComp ( _) | Expr :: SetComp ( _)
100
- ) {
101
- continue ;
108
+ // Suppress lint for starred expressions.
109
+ if arg. is_starred_expr ( ) {
110
+ return ;
102
111
}
103
112
104
- if !checker
105
- . semantic ( )
106
- . resolve_builtin_symbol ( func)
107
- . is_some_and ( |builtin| matches ! ( builtin, "str" | "repr" | "ascii" ) )
113
+ let mut diagnostic =
114
+ checker. report_diagnostic ( ExplicitFStringTypeConversion , expression. range ( ) ) ;
115
+
116
+ // Don't support fixing f-string with debug text.
117
+ if element
118
+ . as_interpolation ( )
119
+ . is_some_and ( |interpolation| interpolation. debug_text . is_some ( ) )
108
120
{
109
- continue ;
121
+ return ;
110
122
}
111
123
112
- let mut diagnostic =
113
- checker. report_diagnostic ( ExplicitFStringTypeConversion , expression. range ( ) ) ;
114
124
diagnostic. try_set_fix ( || {
115
- convert_call_to_conversion_flag ( f_string , index , checker . locator ( ) , checker . stylist ( ) )
125
+ convert_call_to_conversion_flag ( checker , conversion , f_string , index , arg )
116
126
} ) ;
117
127
}
118
128
}
119
129
120
130
/// Generate a [`Fix`] to replace an explicit type conversion with a conversion flag.
121
131
fn convert_call_to_conversion_flag (
132
+ checker : & Checker ,
133
+ conversion : Conversion ,
122
134
f_string : & ast:: FString ,
123
135
index : usize ,
124
- locator : & Locator ,
125
- stylist : & Stylist ,
136
+ arg : & Expr ,
126
137
) -> Result < Fix > {
127
- let source_code = locator. slice ( f_string) ;
128
- transform_expression ( source_code, stylist, |mut expression| {
138
+ let source_code = checker . locator ( ) . slice ( f_string) ;
139
+ transform_expression ( source_code, checker . stylist ( ) , |mut expression| {
129
140
let formatted_string = match_formatted_string ( & mut expression) ?;
130
141
// Replace the formatted call expression at `index` with a conversion flag.
131
142
let formatted_string_expression =
132
143
match_formatted_string_expression ( & mut formatted_string. parts [ index] ) ?;
133
144
let call = match_call_mut ( & mut formatted_string_expression. expression ) ?;
134
- let name = match_name ( & call. func ) ?;
135
- match name. value {
136
- "str" => {
137
- formatted_string_expression. conversion = Some ( "s" ) ;
138
- }
139
- "repr" => {
140
- formatted_string_expression. conversion = Some ( "r" ) ;
141
- }
142
- "ascii" => {
143
- formatted_string_expression. conversion = Some ( "a" ) ;
144
- }
145
- _ => bail ! ( "Unexpected function call: `{:?}`" , name. value) ,
145
+
146
+ formatted_string_expression. conversion = Some ( conversion. as_str ( ) ) ;
147
+
148
+ if starts_with_brace ( checker, arg) {
149
+ formatted_string_expression. whitespace_before_expression = space ( ) ;
146
150
}
147
- formatted_string_expression. expression = call. args [ 0 ] . value . clone ( ) ;
151
+
152
+ formatted_string_expression. expression = if needs_paren ( OperatorPrecedence :: from_expr ( arg) )
153
+ {
154
+ call. args [ 0 ]
155
+ . value
156
+ . clone ( )
157
+ . with_parens ( LeftParen :: default ( ) , RightParen :: default ( ) )
158
+ } else {
159
+ call. args [ 0 ] . value . clone ( )
160
+ } ;
161
+
148
162
Ok ( expression)
149
163
} )
150
164
. map ( |output| Fix :: safe_edit ( Edit :: range_replacement ( output, f_string. range ( ) ) ) )
151
165
}
166
+
167
+ fn starts_with_brace ( checker : & Checker , arg : & Expr ) -> bool {
168
+ checker
169
+ . tokens ( )
170
+ . in_range ( arg. range ( ) )
171
+ . iter ( )
172
+ // Skip the trivia tokens
173
+ . find ( |token| !token. kind ( ) . is_trivia ( ) )
174
+ . is_some_and ( |token| matches ! ( token. kind( ) , TokenKind :: Lbrace ) )
175
+ }
176
+
177
+ fn needs_paren ( precedence : OperatorPrecedence ) -> bool {
178
+ precedence <= OperatorPrecedence :: Lambda
179
+ }
180
+
181
+ /// Represents the three built-in Python conversion functions that can be replaced
182
+ /// with f-string conversion flags.
183
+ #[ derive( Copy , Clone ) ]
184
+ enum Conversion {
185
+ Ascii ,
186
+ Str ,
187
+ Repr ,
188
+ }
189
+
190
+ impl Conversion {
191
+ fn from_str ( value : & str ) -> Option < Self > {
192
+ Some ( match value {
193
+ "ascii" => Self :: Ascii ,
194
+ "str" => Self :: Str ,
195
+ "repr" => Self :: Repr ,
196
+ _ => return None ,
197
+ } )
198
+ }
199
+
200
+ fn as_str ( self ) -> & ' static str {
201
+ match self {
202
+ Conversion :: Ascii => "a" ,
203
+ Conversion :: Str => "s" ,
204
+ Conversion :: Repr => "r" ,
205
+ }
206
+ }
207
+ }
208
+
209
+ impl Display for Conversion {
210
+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
211
+ write ! ( f, "{}" , self . as_str( ) )
212
+ }
213
+ }
0 commit comments