1+ use arrayvec:: ArrayVec ;
12use clippy_utils:: diagnostics:: { span_lint_and_sugg, span_lint_and_then} ;
23use clippy_utils:: is_diag_trait_item;
3- use clippy_utils:: macros:: FormatParamKind :: { Implicit , Named , NamedInline , Numbered , Starred } ;
44use 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 ,
77} ;
88use clippy_utils:: msrvs:: { self , Msrv } ;
99use clippy_utils:: source:: snippet_opt;
1010use clippy_utils:: ty:: { implements_trait, is_type_lang_item} ;
1111use if_chain:: if_chain;
1212use itertools:: Itertools ;
13+ use rustc_ast:: {
14+ FormatArgPosition , FormatArgPositionKind , FormatArgsPiece , FormatArgumentKind , FormatCount , FormatOptions ,
15+ FormatPlaceholder , FormatTrait ,
16+ } ;
1317use rustc_errors:: {
1418 Applicability ,
1519 SuggestionStyle :: { CompletelyHidden , ShowCode } ,
1620} ;
17- use rustc_hir:: { Expr , ExprKind , HirId , LangItem , QPath } ;
21+ use rustc_hir:: { Expr , ExprKind , LangItem } ;
1822use rustc_lint:: { LateContext , LateLintPass , LintContext } ;
1923use rustc_middle:: ty:: adjustment:: { Adjust , Adjustment } ;
2024use rustc_middle:: ty:: Ty ;
2125use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
2226use rustc_span:: def_id:: DefId ;
2327use rustc_span:: edition:: Edition :: Edition2021 ;
24- use rustc_span:: { sym, ExpnData , ExpnKind , Span , Symbol } ;
28+ use rustc_span:: { sym, Span , Symbol } ;
2529
2630declare_clippy_lint ! {
2731 /// ### What it does
@@ -184,111 +188,120 @@ impl FormatArgs {
184188
185189impl < ' tcx > LateLintPass < ' tcx > for FormatArgs {
186190 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+ }
201218 }
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 ) ;
204219 }
220+
205221 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 ) ;
207223 }
208- }
224+ } ) ;
209225 }
210226
211227 extract_msrv_attr ! ( LateContext ) ;
212228}
213229
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 ( ) ) ;
216236
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+ } ;
229241
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 ;
234243
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+ } ;
239248
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+ {
241253 span_lint_and_then (
242254 cx,
243255 UNUSED_FORMAT_SPECS ,
244- arg . span ,
256+ placeholder_span ,
245257 "format specifiers have no effect on `format_args!()`" ,
246258 |diag| {
247- let mut suggest_format = |spec, span | {
259+ let mut suggest_format = |spec| {
248260 let message = format ! ( "for the {spec} to apply consider using `format!()`" ) ;
249261
250- if let Some ( mac_call) = root_macro_call ( arg . param . value . span )
262+ if let Some ( mac_call) = root_macro_call ( arg_span )
251263 && cx. tcx . is_diagnostic_item ( sym:: format_args_macro, mac_call. def_id )
252- && arg. span . eq_ctxt ( mac_call. span )
253264 {
254265 diag. span_suggestion (
255266 cx. sess ( ) . source_map ( ) . span_until_char ( mac_call. span , '!' ) ,
256267 message,
257268 "format" ,
258269 Applicability :: MaybeIncorrect ,
259270 ) ;
260- } else if let Some ( span ) = span {
261- diag. span_help ( span , message) ;
271+ } else {
272+ diag. help ( message) ;
262273 }
263274 } ;
264275
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" ) ;
267278 }
268279
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" ) ;
271282 }
272283
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+ }
279292 } ,
280293 ) ;
281294 }
282295}
283296
284297fn check_uninlined_args (
285298 cx : & LateContext < ' _ > ,
286- args : & FormatArgsExpn < ' _ > ,
299+ args : & rustc_ast :: FormatArgs ,
287300 call_site : Span ,
288301 def_id : DefId ,
289302 ignore_mixed : bool ,
290303) {
291- if args. format_string . span . from_expansion ( ) {
304+ if args. span . from_expansion ( ) {
292305 return ;
293306 }
294307 if call_site. edition ( ) < Edition2021 && ( is_panic ( cx, def_id) || is_assert_macro ( cx, def_id) ) {
@@ -303,7 +316,13 @@ fn check_uninlined_args(
303316 // we cannot remove any other arguments in the format string,
304317 // because the index numbers might be wrong after inlining.
305318 // 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 ( ) {
307326 return ;
308327 }
309328
@@ -332,38 +351,36 @@ fn check_uninlined_args(
332351}
333352
334353fn check_one_arg (
335- args : & FormatArgsExpn < ' _ > ,
336- param : & FormatParam < ' _ > ,
354+ args : & rustc_ast:: FormatArgs ,
355+ pos : & FormatArgPosition ,
356+ usage : FormatParamUsage ,
337357 fixes : & mut Vec < ( Span , String ) > ,
338358 ignore_mixed : bool ,
339359) -> 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 ( )
343366 && 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
345369 {
346- let replacement = match param . usage {
370+ let replacement = match usage {
347371 FormatParamUsage :: Argument => segment. ident . name . to_string ( ) ,
348372 FormatParamUsage :: Width => format ! ( "{}$" , segment. ident. name) ,
349373 FormatParamUsage :: Precision => format ! ( ".{}$" , segment. ident. name) ,
350374 } ;
351- fixes. push ( ( param . span , replacement) ) ;
375+ fixes. push ( ( pos_span , replacement) ) ;
352376 fixes. push ( ( arg_span, String :: new ( ) ) ) ;
353377 true // successful inlining, continue checking
354378 } else {
355379 // Do not continue inlining (return false) in case
356380 // * if we can't inline a numbered argument, e.g. `print!("{0} ...", foo.bar, ...)`
357381 // * 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 ( _) ) )
367384 }
368385}
369386
@@ -438,10 +455,31 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
438455 }
439456}
440457
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) )
445483 . at_most_one ( )
446484 . is_err ( )
447485}
0 commit comments