1
+ use arrayvec:: ArrayVec ;
1
2
use clippy_utils:: diagnostics:: { span_lint_and_sugg, span_lint_and_then} ;
2
3
use clippy_utils:: is_diag_trait_item;
3
- use clippy_utils:: macros:: FormatParamKind :: { Implicit , Named , NamedInline , Numbered , Starred } ;
4
4
use clippy_utils:: macros:: {
5
- is_assert_macro , is_format_macro , is_panic , root_macro_call , Count , FormatArg , FormatArgsExpn , FormatParam ,
6
- FormatParamUsage ,
5
+ find_format_arg_expr , find_format_args , format_arg_removal_span , format_placeholder_format_span , is_assert_macro ,
6
+ is_format_macro , is_panic , root_macro_call , root_macro_call_first_node , FormatParamUsage ,
7
7
} ;
8
8
use clippy_utils:: msrvs:: { self , Msrv } ;
9
9
use clippy_utils:: source:: snippet_opt;
10
10
use clippy_utils:: ty:: { implements_trait, is_type_lang_item} ;
11
11
use if_chain:: if_chain;
12
12
use itertools:: Itertools ;
13
+ use rustc_ast:: {
14
+ FormatArgPosition , FormatArgPositionKind , FormatArgsPiece , FormatArgumentKind , FormatCount , FormatOptions ,
15
+ FormatPlaceholder , FormatTrait ,
16
+ } ;
13
17
use rustc_errors:: {
14
18
Applicability ,
15
19
SuggestionStyle :: { CompletelyHidden , ShowCode } ,
16
20
} ;
17
- use rustc_hir:: { Expr , ExprKind , HirId , LangItem , QPath } ;
21
+ use rustc_hir:: { Expr , ExprKind , LangItem } ;
18
22
use rustc_lint:: { LateContext , LateLintPass , LintContext } ;
19
23
use rustc_middle:: ty:: adjustment:: { Adjust , Adjustment } ;
20
24
use rustc_middle:: ty:: Ty ;
21
25
use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
22
26
use rustc_span:: def_id:: DefId ;
23
27
use rustc_span:: edition:: Edition :: Edition2021 ;
24
- use rustc_span:: { sym, ExpnData , ExpnKind , Span , Symbol } ;
28
+ use rustc_span:: { sym, Span , Symbol } ;
25
29
26
30
declare_clippy_lint ! {
27
31
/// ### What it does
@@ -184,111 +188,120 @@ impl FormatArgs {
184
188
185
189
impl < ' tcx > LateLintPass < ' tcx > for FormatArgs {
186
190
fn check_expr ( & mut self , cx : & LateContext < ' tcx > , expr : & ' tcx Expr < ' tcx > ) {
187
- if let Some ( format_args) = FormatArgsExpn :: parse ( cx, expr)
188
- && let expr_expn_data = expr. span . ctxt ( ) . outer_expn_data ( )
189
- && let outermost_expn_data = outermost_expn_data ( expr_expn_data)
190
- && let Some ( macro_def_id) = outermost_expn_data. macro_def_id
191
- && is_format_macro ( cx, macro_def_id)
192
- && let ExpnKind :: Macro ( _, name) = outermost_expn_data. kind
193
- {
194
- for arg in & format_args. args {
195
- check_unused_format_specifier ( cx, arg) ;
196
- if !arg. format . is_default ( ) {
197
- continue ;
198
- }
199
- if is_aliased ( & format_args, arg. param . value . hir_id ) {
200
- continue ;
191
+ let Some ( macro_call) = root_macro_call_first_node ( cx, expr) else { return } ;
192
+ if !is_format_macro ( cx, macro_call. def_id ) {
193
+ return ;
194
+ }
195
+ let name = cx. tcx . item_name ( macro_call. def_id ) ;
196
+
197
+ find_format_args ( cx, expr, macro_call. expn , |format_args| {
198
+ for piece in & format_args. template {
199
+ if let FormatArgsPiece :: Placeholder ( placeholder) = piece
200
+ && let Ok ( index) = placeholder. argument . index
201
+ && let Some ( arg) = format_args. arguments . all_args ( ) . get ( index)
202
+ {
203
+ let arg_expr = find_format_arg_expr ( expr, arg) ;
204
+
205
+ check_unused_format_specifier ( cx, placeholder, arg_expr) ;
206
+
207
+ if placeholder. format_trait != FormatTrait :: Display
208
+ || placeholder. format_options != FormatOptions :: default ( )
209
+ || is_aliased ( format_args, index)
210
+ {
211
+ continue ;
212
+ }
213
+
214
+ if let Ok ( arg_hir_expr) = arg_expr {
215
+ check_format_in_format_args ( cx, macro_call. span , name, arg_hir_expr) ;
216
+ check_to_string_in_format_args ( cx, name, arg_hir_expr) ;
217
+ }
201
218
}
202
- check_format_in_format_args ( cx, outermost_expn_data. call_site , name, arg. param . value ) ;
203
- check_to_string_in_format_args ( cx, name, arg. param . value ) ;
204
219
}
220
+
205
221
if self . msrv . meets ( msrvs:: FORMAT_ARGS_CAPTURE ) {
206
- check_uninlined_args ( cx, & format_args, outermost_expn_data . call_site , macro_def_id , self . ignore_mixed ) ;
222
+ check_uninlined_args ( cx, format_args, macro_call . span , macro_call . def_id , self . ignore_mixed ) ;
207
223
}
208
- }
224
+ } ) ;
209
225
}
210
226
211
227
extract_msrv_attr ! ( LateContext ) ;
212
228
}
213
229
214
- fn check_unused_format_specifier ( cx : & LateContext < ' _ > , arg : & FormatArg < ' _ > ) {
215
- let param_ty = cx. typeck_results ( ) . expr_ty ( arg. param . value ) . peel_refs ( ) ;
230
+ fn check_unused_format_specifier (
231
+ cx : & LateContext < ' _ > ,
232
+ placeholder : & FormatPlaceholder ,
233
+ arg_expr : Result < & Expr < ' _ > , & rustc_ast:: Expr > ,
234
+ ) {
235
+ let ty_or_ast_expr = arg_expr. map ( |expr| cx. typeck_results ( ) . expr_ty ( expr) . peel_refs ( ) ) ;
216
236
217
- if let Count :: Implied ( Some ( mut span) ) = arg. format . precision
218
- && !span. is_empty ( )
219
- {
220
- span_lint_and_then (
221
- cx,
222
- UNUSED_FORMAT_SPECS ,
223
- span,
224
- "empty precision specifier has no effect" ,
225
- |diag| {
226
- if param_ty. is_floating_point ( ) {
227
- diag. note ( "a precision specifier is not required to format floats" ) ;
228
- }
237
+ let is_format_args = match ty_or_ast_expr {
238
+ Ok ( ty) => is_type_lang_item ( cx, ty, LangItem :: FormatArguments ) ,
239
+ Err ( expr) => matches ! ( expr. peel_parens_and_refs( ) . kind, rustc_ast:: ExprKind :: FormatArgs ( _) ) ,
240
+ } ;
229
241
230
- if arg. format . is_default ( ) {
231
- // If there's no other specifiers remove the `:` too
232
- span = arg. format_span ( ) ;
233
- }
242
+ let options = & placeholder. format_options ;
234
243
235
- diag . span_suggestion_verbose ( span , "remove the `.`" , "" , Applicability :: MachineApplicable ) ;
236
- } ,
237
- ) ;
238
- }
244
+ let arg_span = match arg_expr {
245
+ Ok ( expr ) => expr . span ,
246
+ Err ( expr ) => expr . span ,
247
+ } ;
239
248
240
- if is_type_lang_item ( cx, param_ty, LangItem :: FormatArguments ) && !arg. format . is_default_for_trait ( ) {
249
+ if let Some ( placeholder_span) = placeholder. span
250
+ && is_format_args
251
+ && * options != FormatOptions :: default ( )
252
+ {
241
253
span_lint_and_then (
242
254
cx,
243
255
UNUSED_FORMAT_SPECS ,
244
- arg . span ,
256
+ placeholder_span ,
245
257
"format specifiers have no effect on `format_args!()`" ,
246
258
|diag| {
247
- let mut suggest_format = |spec, span | {
259
+ let mut suggest_format = |spec| {
248
260
let message = format ! ( "for the {spec} to apply consider using `format!()`" ) ;
249
261
250
- if let Some ( mac_call) = root_macro_call ( arg . param . value . span )
262
+ if let Some ( mac_call) = root_macro_call ( arg_span )
251
263
&& cx. tcx . is_diagnostic_item ( sym:: format_args_macro, mac_call. def_id )
252
- && arg. span . eq_ctxt ( mac_call. span )
253
264
{
254
265
diag. span_suggestion (
255
266
cx. sess ( ) . source_map ( ) . span_until_char ( mac_call. span , '!' ) ,
256
267
message,
257
268
"format" ,
258
269
Applicability :: MaybeIncorrect ,
259
270
) ;
260
- } else if let Some ( span ) = span {
261
- diag. span_help ( span , message) ;
271
+ } else {
272
+ diag. help ( message) ;
262
273
}
263
274
} ;
264
275
265
- if !arg . format . width . is_implied ( ) {
266
- suggest_format ( "width" , arg . format . width . span ( ) ) ;
276
+ if options . width . is_some ( ) {
277
+ suggest_format ( "width" ) ;
267
278
}
268
279
269
- if !arg . format . precision . is_implied ( ) {
270
- suggest_format ( "precision" , arg . format . precision . span ( ) ) ;
280
+ if options . precision . is_some ( ) {
281
+ suggest_format ( "precision" ) ;
271
282
}
272
283
273
- diag. span_suggestion_verbose (
274
- arg. format_span ( ) ,
275
- "if the current behavior is intentional, remove the format specifiers" ,
276
- "" ,
277
- Applicability :: MaybeIncorrect ,
278
- ) ;
284
+ if let Some ( format_span) = format_placeholder_format_span ( placeholder) {
285
+ diag. span_suggestion_verbose (
286
+ format_span,
287
+ "if the current behavior is intentional, remove the format specifiers" ,
288
+ "" ,
289
+ Applicability :: MaybeIncorrect ,
290
+ ) ;
291
+ }
279
292
} ,
280
293
) ;
281
294
}
282
295
}
283
296
284
297
fn check_uninlined_args (
285
298
cx : & LateContext < ' _ > ,
286
- args : & FormatArgsExpn < ' _ > ,
299
+ args : & rustc_ast :: FormatArgs ,
287
300
call_site : Span ,
288
301
def_id : DefId ,
289
302
ignore_mixed : bool ,
290
303
) {
291
- if args. format_string . span . from_expansion ( ) {
304
+ if args. span . from_expansion ( ) {
292
305
return ;
293
306
}
294
307
if call_site. edition ( ) < Edition2021 && ( is_panic ( cx, def_id) || is_assert_macro ( cx, def_id) ) {
@@ -303,7 +316,13 @@ fn check_uninlined_args(
303
316
// we cannot remove any other arguments in the format string,
304
317
// because the index numbers might be wrong after inlining.
305
318
// Example of an un-inlinable format: print!("{}{1}", foo, 2)
306
- if !args. params ( ) . all ( |p| check_one_arg ( args, & p, & mut fixes, ignore_mixed) ) || fixes. is_empty ( ) {
319
+ for ( pos, usage) in format_arg_positions ( args) {
320
+ if !check_one_arg ( args, pos, usage, & mut fixes, ignore_mixed) {
321
+ return ;
322
+ }
323
+ }
324
+
325
+ if fixes. is_empty ( ) {
307
326
return ;
308
327
}
309
328
@@ -332,38 +351,36 @@ fn check_uninlined_args(
332
351
}
333
352
334
353
fn check_one_arg (
335
- args : & FormatArgsExpn < ' _ > ,
336
- param : & FormatParam < ' _ > ,
354
+ args : & rustc_ast:: FormatArgs ,
355
+ pos : & FormatArgPosition ,
356
+ usage : FormatParamUsage ,
337
357
fixes : & mut Vec < ( Span , String ) > ,
338
358
ignore_mixed : bool ,
339
359
) -> bool {
340
- if matches ! ( param. kind, Implicit | Starred | Named ( _) | Numbered )
341
- && let ExprKind :: Path ( QPath :: Resolved ( None , path) ) = param. value . kind
342
- && let [ segment] = path. segments
360
+ let index = pos. index . unwrap ( ) ;
361
+ let arg = & args. arguments . all_args ( ) [ index] ;
362
+
363
+ if !matches ! ( arg. kind, FormatArgumentKind :: Captured ( _) )
364
+ && let rustc_ast:: ExprKind :: Path ( None , path) = & arg. expr . kind
365
+ && let [ segment] = path. segments . as_slice ( )
343
366
&& segment. args . is_none ( )
344
- && let Some ( arg_span) = args. value_with_prev_comma_span ( param. value . hir_id )
367
+ && let Some ( arg_span) = format_arg_removal_span ( args, index)
368
+ && let Some ( pos_span) = pos. span
345
369
{
346
- let replacement = match param . usage {
370
+ let replacement = match usage {
347
371
FormatParamUsage :: Argument => segment. ident . name . to_string ( ) ,
348
372
FormatParamUsage :: Width => format ! ( "{}$" , segment. ident. name) ,
349
373
FormatParamUsage :: Precision => format ! ( ".{}$" , segment. ident. name) ,
350
374
} ;
351
- fixes. push ( ( param . span , replacement) ) ;
375
+ fixes. push ( ( pos_span , replacement) ) ;
352
376
fixes. push ( ( arg_span, String :: new ( ) ) ) ;
353
377
true // successful inlining, continue checking
354
378
} else {
355
379
// Do not continue inlining (return false) in case
356
380
// * if we can't inline a numbered argument, e.g. `print!("{0} ...", foo.bar, ...)`
357
381
// * if allow_mixed_uninlined_format_args is false and this arg hasn't been inlined already
358
- param. kind != Numbered && ( !ignore_mixed || matches ! ( param. kind, NamedInline ( _) ) )
359
- }
360
- }
361
-
362
- fn outermost_expn_data ( expn_data : ExpnData ) -> ExpnData {
363
- if expn_data. call_site . from_expansion ( ) {
364
- outermost_expn_data ( expn_data. call_site . ctxt ( ) . outer_expn_data ( ) )
365
- } else {
366
- expn_data
382
+ pos. kind != FormatArgPositionKind :: Number
383
+ && ( !ignore_mixed || matches ! ( arg. kind, FormatArgumentKind :: Captured ( _) ) )
367
384
}
368
385
}
369
386
@@ -438,10 +455,31 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
438
455
}
439
456
}
440
457
441
- /// Returns true if `hir_id` is referred to by multiple format params
442
- fn is_aliased ( args : & FormatArgsExpn < ' _ > , hir_id : HirId ) -> bool {
443
- args. params ( )
444
- . filter ( |param| param. value . hir_id == hir_id)
458
+ fn format_arg_positions (
459
+ format_args : & rustc_ast:: FormatArgs ,
460
+ ) -> impl Iterator < Item = ( & FormatArgPosition , FormatParamUsage ) > {
461
+ format_args. template . iter ( ) . flat_map ( |piece| match piece {
462
+ FormatArgsPiece :: Placeholder ( placeholder) => {
463
+ let mut positions = ArrayVec :: < _ , 3 > :: new ( ) ;
464
+
465
+ positions. push ( ( & placeholder. argument , FormatParamUsage :: Argument ) ) ;
466
+ if let Some ( FormatCount :: Argument ( position) ) = & placeholder. format_options . width {
467
+ positions. push ( ( position, FormatParamUsage :: Width ) ) ;
468
+ }
469
+ if let Some ( FormatCount :: Argument ( position) ) = & placeholder. format_options . precision {
470
+ positions. push ( ( position, FormatParamUsage :: Precision ) ) ;
471
+ }
472
+
473
+ positions
474
+ } ,
475
+ FormatArgsPiece :: Literal ( _) => ArrayVec :: new ( ) ,
476
+ } )
477
+ }
478
+
479
+ /// Returns true if the format argument at `index` is referred to by multiple format params
480
+ fn is_aliased ( format_args : & rustc_ast:: FormatArgs , index : usize ) -> bool {
481
+ format_arg_positions ( format_args)
482
+ . filter ( |( position, _) | position. index == Ok ( index) )
445
483
. at_most_one ( )
446
484
. is_err ( )
447
485
}
0 commit comments