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,121 @@ 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
+ // TODO: Change to .peel_parens_and_refs() after https://github.com/rust-lang/rust/pull/106824
240
+ Err ( expr) => matches ! ( expr. peel_parens( ) . kind, rustc_ast:: ExprKind :: FormatArgs ( _) ) ,
241
+ } ;
229
242
230
- if arg. format . is_default ( ) {
231
- // If there's no other specifiers remove the `:` too
232
- span = arg. format_span ( ) ;
233
- }
243
+ let options = & placeholder. format_options ;
234
244
235
- diag . span_suggestion_verbose ( span , "remove the `.`" , "" , Applicability :: MachineApplicable ) ;
236
- } ,
237
- ) ;
238
- }
245
+ let arg_span = match arg_expr {
246
+ Ok ( expr ) => expr . span ,
247
+ Err ( expr ) => expr . span ,
248
+ } ;
239
249
240
- if is_type_lang_item ( cx, param_ty, LangItem :: FormatArguments ) && !arg. format . is_default_for_trait ( ) {
250
+ if let Some ( placeholder_span) = placeholder. span
251
+ && is_format_args
252
+ && * options != FormatOptions :: default ( )
253
+ {
241
254
span_lint_and_then (
242
255
cx,
243
256
UNUSED_FORMAT_SPECS ,
244
- arg . span ,
257
+ placeholder_span ,
245
258
"format specifiers have no effect on `format_args!()`" ,
246
259
|diag| {
247
- let mut suggest_format = |spec, span | {
260
+ let mut suggest_format = |spec| {
248
261
let message = format ! ( "for the {spec} to apply consider using `format!()`" ) ;
249
262
250
- if let Some ( mac_call) = root_macro_call ( arg . param . value . span )
263
+ if let Some ( mac_call) = root_macro_call ( arg_span )
251
264
&& cx. tcx . is_diagnostic_item ( sym:: format_args_macro, mac_call. def_id )
252
- && arg. span . eq_ctxt ( mac_call. span )
253
265
{
254
266
diag. span_suggestion (
255
267
cx. sess ( ) . source_map ( ) . span_until_char ( mac_call. span , '!' ) ,
256
268
message,
257
269
"format" ,
258
270
Applicability :: MaybeIncorrect ,
259
271
) ;
260
- } else if let Some ( span ) = span {
261
- diag. span_help ( span , message) ;
272
+ } else {
273
+ diag. help ( message) ;
262
274
}
263
275
} ;
264
276
265
- if !arg . format . width . is_implied ( ) {
266
- suggest_format ( "width" , arg . format . width . span ( ) ) ;
277
+ if options . width . is_some ( ) {
278
+ suggest_format ( "width" ) ;
267
279
}
268
280
269
- if !arg . format . precision . is_implied ( ) {
270
- suggest_format ( "precision" , arg . format . precision . span ( ) ) ;
281
+ if options . precision . is_some ( ) {
282
+ suggest_format ( "precision" ) ;
271
283
}
272
284
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
- ) ;
285
+ if let Some ( format_span) = format_placeholder_format_span ( placeholder) {
286
+ diag. span_suggestion_verbose (
287
+ format_span,
288
+ "if the current behavior is intentional, remove the format specifiers" ,
289
+ "" ,
290
+ Applicability :: MaybeIncorrect ,
291
+ ) ;
292
+ }
279
293
} ,
280
294
) ;
281
295
}
282
296
}
283
297
284
298
fn check_uninlined_args (
285
299
cx : & LateContext < ' _ > ,
286
- args : & FormatArgsExpn < ' _ > ,
300
+ args : & rustc_ast :: FormatArgs ,
287
301
call_site : Span ,
288
302
def_id : DefId ,
289
303
ignore_mixed : bool ,
290
304
) {
291
- if args. format_string . span . from_expansion ( ) {
305
+ if args. span . from_expansion ( ) {
292
306
return ;
293
307
}
294
308
if call_site. edition ( ) < Edition2021 && ( is_panic ( cx, def_id) || is_assert_macro ( cx, def_id) ) {
@@ -303,7 +317,13 @@ fn check_uninlined_args(
303
317
// we cannot remove any other arguments in the format string,
304
318
// because the index numbers might be wrong after inlining.
305
319
// 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 ( ) {
320
+ for ( pos, usage) in format_arg_positions ( args) {
321
+ if !check_one_arg ( args, pos, usage, & mut fixes, ignore_mixed) {
322
+ return ;
323
+ }
324
+ }
325
+
326
+ if fixes. is_empty ( ) {
307
327
return ;
308
328
}
309
329
@@ -332,38 +352,36 @@ fn check_uninlined_args(
332
352
}
333
353
334
354
fn check_one_arg (
335
- args : & FormatArgsExpn < ' _ > ,
336
- param : & FormatParam < ' _ > ,
355
+ args : & rustc_ast:: FormatArgs ,
356
+ pos : & FormatArgPosition ,
357
+ usage : FormatParamUsage ,
337
358
fixes : & mut Vec < ( Span , String ) > ,
338
359
ignore_mixed : bool ,
339
360
) -> 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
361
+ let index = pos. index . unwrap ( ) ;
362
+ let arg = & args. arguments . all_args ( ) [ index] ;
363
+
364
+ if !matches ! ( arg. kind, FormatArgumentKind :: Captured ( _) )
365
+ && let rustc_ast:: ExprKind :: Path ( None , path) = & arg. expr . kind
366
+ && let [ segment] = path. segments . as_slice ( )
343
367
&& segment. args . is_none ( )
344
- && let Some ( arg_span) = args. value_with_prev_comma_span ( param. value . hir_id )
368
+ && let Some ( arg_span) = format_arg_removal_span ( args, index)
369
+ && let Some ( pos_span) = pos. span
345
370
{
346
- let replacement = match param . usage {
371
+ let replacement = match usage {
347
372
FormatParamUsage :: Argument => segment. ident . name . to_string ( ) ,
348
373
FormatParamUsage :: Width => format ! ( "{}$" , segment. ident. name) ,
349
374
FormatParamUsage :: Precision => format ! ( ".{}$" , segment. ident. name) ,
350
375
} ;
351
- fixes. push ( ( param . span , replacement) ) ;
376
+ fixes. push ( ( pos_span , replacement) ) ;
352
377
fixes. push ( ( arg_span, String :: new ( ) ) ) ;
353
378
true // successful inlining, continue checking
354
379
} else {
355
380
// Do not continue inlining (return false) in case
356
381
// * if we can't inline a numbered argument, e.g. `print!("{0} ...", foo.bar, ...)`
357
382
// * 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
383
+ pos. kind != FormatArgPositionKind :: Number
384
+ && ( !ignore_mixed || matches ! ( arg. kind, FormatArgumentKind :: Captured ( _) ) )
367
385
}
368
386
}
369
387
@@ -438,10 +456,31 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
438
456
}
439
457
}
440
458
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)
459
+ fn format_arg_positions (
460
+ format_args : & rustc_ast:: FormatArgs ,
461
+ ) -> impl Iterator < Item = ( & FormatArgPosition , FormatParamUsage ) > {
462
+ format_args. template . iter ( ) . flat_map ( |piece| match piece {
463
+ FormatArgsPiece :: Placeholder ( placeholder) => {
464
+ let mut positions = ArrayVec :: < _ , 3 > :: new ( ) ;
465
+
466
+ positions. push ( ( & placeholder. argument , FormatParamUsage :: Argument ) ) ;
467
+ if let Some ( FormatCount :: Argument ( position) ) = & placeholder. format_options . width {
468
+ positions. push ( ( position, FormatParamUsage :: Width ) ) ;
469
+ }
470
+ if let Some ( FormatCount :: Argument ( position) ) = & placeholder. format_options . precision {
471
+ positions. push ( ( position, FormatParamUsage :: Precision ) ) ;
472
+ }
473
+
474
+ positions
475
+ } ,
476
+ FormatArgsPiece :: Literal ( _) => ArrayVec :: new ( ) ,
477
+ } )
478
+ }
479
+
480
+ /// Returns true if the format argument at `index` is referred to by multiple format params
481
+ fn is_aliased ( format_args : & rustc_ast:: FormatArgs , index : usize ) -> bool {
482
+ format_arg_positions ( format_args)
483
+ . filter ( |( position, _) | position. index == Ok ( index) )
445
484
. at_most_one ( )
446
485
. is_err ( )
447
486
}
0 commit comments