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,121 @@ 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+ // 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+ } ;
229242
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 ;
234244
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+ } ;
239249
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+ {
241254 span_lint_and_then (
242255 cx,
243256 UNUSED_FORMAT_SPECS ,
244- arg . span ,
257+ placeholder_span ,
245258 "format specifiers have no effect on `format_args!()`" ,
246259 |diag| {
247- let mut suggest_format = |spec, span | {
260+ let mut suggest_format = |spec| {
248261 let message = format ! ( "for the {spec} to apply consider using `format!()`" ) ;
249262
250- if let Some ( mac_call) = root_macro_call ( arg . param . value . span )
263+ if let Some ( mac_call) = root_macro_call ( arg_span )
251264 && cx. tcx . is_diagnostic_item ( sym:: format_args_macro, mac_call. def_id )
252- && arg. span . eq_ctxt ( mac_call. span )
253265 {
254266 diag. span_suggestion (
255267 cx. sess ( ) . source_map ( ) . span_until_char ( mac_call. span , '!' ) ,
256268 message,
257269 "format" ,
258270 Applicability :: MaybeIncorrect ,
259271 ) ;
260- } else if let Some ( span ) = span {
261- diag. span_help ( span , message) ;
272+ } else {
273+ diag. help ( message) ;
262274 }
263275 } ;
264276
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" ) ;
267279 }
268280
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" ) ;
271283 }
272284
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+ }
279293 } ,
280294 ) ;
281295 }
282296}
283297
284298fn check_uninlined_args (
285299 cx : & LateContext < ' _ > ,
286- args : & FormatArgsExpn < ' _ > ,
300+ args : & rustc_ast :: FormatArgs ,
287301 call_site : Span ,
288302 def_id : DefId ,
289303 ignore_mixed : bool ,
290304) {
291- if args. format_string . span . from_expansion ( ) {
305+ if args. span . from_expansion ( ) {
292306 return ;
293307 }
294308 if call_site. edition ( ) < Edition2021 && ( is_panic ( cx, def_id) || is_assert_macro ( cx, def_id) ) {
@@ -303,7 +317,13 @@ fn check_uninlined_args(
303317 // we cannot remove any other arguments in the format string,
304318 // because the index numbers might be wrong after inlining.
305319 // 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 ( ) {
307327 return ;
308328 }
309329
@@ -332,38 +352,36 @@ fn check_uninlined_args(
332352}
333353
334354fn check_one_arg (
335- args : & FormatArgsExpn < ' _ > ,
336- param : & FormatParam < ' _ > ,
355+ args : & rustc_ast:: FormatArgs ,
356+ pos : & FormatArgPosition ,
357+ usage : FormatParamUsage ,
337358 fixes : & mut Vec < ( Span , String ) > ,
338359 ignore_mixed : bool ,
339360) -> 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 ( )
343367 && 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
345370 {
346- let replacement = match param . usage {
371+ let replacement = match usage {
347372 FormatParamUsage :: Argument => segment. ident . name . to_string ( ) ,
348373 FormatParamUsage :: Width => format ! ( "{}$" , segment. ident. name) ,
349374 FormatParamUsage :: Precision => format ! ( ".{}$" , segment. ident. name) ,
350375 } ;
351- fixes. push ( ( param . span , replacement) ) ;
376+ fixes. push ( ( pos_span , replacement) ) ;
352377 fixes. push ( ( arg_span, String :: new ( ) ) ) ;
353378 true // successful inlining, continue checking
354379 } else {
355380 // Do not continue inlining (return false) in case
356381 // * if we can't inline a numbered argument, e.g. `print!("{0} ...", foo.bar, ...)`
357382 // * 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 ( _) ) )
367385 }
368386}
369387
@@ -438,10 +456,31 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
438456 }
439457}
440458
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) )
445484 . at_most_one ( )
446485 . is_err ( )
447486}
0 commit comments