11use  clippy_utils:: diagnostics:: { span_lint_and_sugg,  span_lint_and_then} ; 
2- use  clippy_utils:: is_diag_trait_item ; 
3- use  clippy_utils:: macros:: { is_format_macro,  FormatArgsExpn } ; 
4- use  clippy_utils:: source:: snippet_opt; 
2+ use  clippy_utils:: macros :: FormatParamKind :: { Implicit ,   Named ,   Numbered ,   Starred } ; 
3+ use  clippy_utils:: macros:: { is_format_macro,  FormatArgsExpn ,   FormatParam ,   FormatParamUsage } ; 
4+ use  clippy_utils:: source:: { expand_past_previous_comma ,   snippet_opt} ; 
55use  clippy_utils:: ty:: implements_trait; 
6+ use  clippy_utils:: { is_diag_trait_item,  meets_msrv,  msrvs} ; 
67use  if_chain:: if_chain; 
78use  itertools:: Itertools ; 
89use  rustc_errors:: Applicability ; 
9- use  rustc_hir:: { Expr ,  ExprKind ,  HirId } ; 
10+ use  rustc_hir:: { Expr ,  ExprKind ,  HirId ,   Path ,   QPath } ; 
1011use  rustc_lint:: { LateContext ,  LateLintPass } ; 
1112use  rustc_middle:: ty:: adjustment:: { Adjust ,  Adjustment } ; 
1213use  rustc_middle:: ty:: Ty ; 
13- use  rustc_session:: { declare_lint_pass,  declare_tool_lint} ; 
14+ use  rustc_semver:: RustcVersion ; 
15+ use  rustc_session:: { declare_tool_lint,  impl_lint_pass} ; 
1416use  rustc_span:: { sym,  ExpnData ,  ExpnKind ,  Span ,  Symbol } ; 
1517
1618declare_clippy_lint !  { 
@@ -64,7 +66,67 @@ declare_clippy_lint! {
6466    "`to_string` applied to a type that implements `Display` in format args" 
6567} 
6668
67- declare_lint_pass ! ( FormatArgs  => [ FORMAT_IN_FORMAT_ARGS ,  TO_STRING_IN_FORMAT_ARGS ] ) ; 
69+ declare_clippy_lint !  { 
70+     /// ### What it does 
71+ /// Detect when a variable is not inlined in a format string, 
72+ /// and suggests to inline it. 
73+ /// 
74+ /// ### Why is this bad? 
75+ /// Non-inlined code is slightly more difficult to read and understand, 
76+ /// as it requires arguments to be matched against the format string. 
77+ /// The inlined syntax, where allowed, is simpler. 
78+ /// 
79+ /// ### Example 
80+ /// ```rust 
81+ /// # let var = 42; 
82+ /// # let width = 1; 
83+ /// # let prec = 2; 
84+ /// format!("{}", var); 
85+ /// format!("{v:?}", v = var); 
86+ /// format!("{0} {0}", var); 
87+ /// format!("{0:1$}", var, width); 
88+ /// format!("{:.*}", prec, var); 
89+ /// ``` 
90+ /// Use instead: 
91+ /// ```rust 
92+ /// # let var = 42; 
93+ /// # let width = 1; 
94+ /// # let prec = 2; 
95+ /// format!("{var}"); 
96+ /// format!("{var:?}"); 
97+ /// format!("{var} {var}"); 
98+ /// format!("{var:width$}"); 
99+ /// format!("{var:.prec$}"); 
100+ /// ``` 
101+ /// 
102+ /// ### Known Problems 
103+ /// 
104+ /// There may be a false positive if the format string is expanded from certain proc macros: 
105+ /// 
106+ /// ```ignore 
107+ /// println!(indoc!("{}"), var); 
108+ /// ``` 
109+ /// 
110+ /// If a format string contains a numbered argument that cannot be inlined 
111+ /// nothing will be suggested, e.g. `println!("{0}={1}", var, 1+2)`. 
112+ [ clippy:: version = "1.65.0" ] 
113+     pub  UNINLINED_FORMAT_ARGS , 
114+     pedantic, 
115+     "using non-inlined variables in `format!` calls" 
116+ } 
117+ 
118+ impl_lint_pass ! ( FormatArgs  => [ FORMAT_IN_FORMAT_ARGS ,  UNINLINED_FORMAT_ARGS ,  TO_STRING_IN_FORMAT_ARGS ] ) ; 
119+ 
120+ pub  struct  FormatArgs  { 
121+     msrv :  Option < RustcVersion > , 
122+ } 
123+ 
124+ impl  FormatArgs  { 
125+     #[ must_use]  
126+     pub  fn  new ( msrv :  Option < RustcVersion > )  -> Self  { 
127+         Self  {  msrv } 
128+     } 
129+ } 
68130
69131impl < ' tcx >  LateLintPass < ' tcx >  for  FormatArgs  { 
70132    fn  check_expr ( & mut  self ,  cx :  & LateContext < ' tcx > ,  expr :  & ' tcx  Expr < ' tcx > )  { 
@@ -86,9 +148,73 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
86148                    check_format_in_format_args( cx,  outermost_expn_data. call_site,  name,  arg. param. value) ; 
87149                    check_to_string_in_format_args( cx,  name,  arg. param. value) ; 
88150                } 
151+                 if  meets_msrv( self . msrv,  msrvs:: FORMAT_ARGS_CAPTURE )  { 
152+                     check_uninlined_args( cx,  & format_args,  outermost_expn_data. call_site) ; 
153+                 } 
89154            } 
90155        } 
91156    } 
157+ 
158+     extract_msrv_attr ! ( LateContext ) ; 
159+ } 
160+ 
161+ fn  check_uninlined_args ( cx :  & LateContext < ' _ > ,  args :  & FormatArgsExpn < ' _ > ,  call_site :  Span )  { 
162+     if  args. format_string . span . from_expansion ( )  { 
163+         return ; 
164+     } 
165+ 
166+     let  mut  fixes = Vec :: new ( ) ; 
167+     // If any of the arguments are referenced by an index number, 
168+     // and that argument is not a simple variable and cannot be inlined, 
169+     // we cannot remove any other arguments in the format string, 
170+     // because the index numbers might be wrong after inlining. 
171+     // Example of an un-inlinable format:  print!("{}{1}", foo, 2) 
172+     if  !args. params ( ) . all ( |p| check_one_arg ( cx,  & p,  & mut  fixes) )  || fixes. is_empty ( )  { 
173+         return ; 
174+     } 
175+ 
176+     // FIXME: Properly ignore a rare case where the format string is wrapped in a macro. 
177+     // Example:  `format!(indoc!("{}"), foo);` 
178+     // If inlined, they will cause a compilation error: 
179+     //     > to avoid ambiguity, `format_args!` cannot capture variables 
180+     //     > when the format string is expanded from a macro 
181+     // @Alexendoo explanation: 
182+     //     > indoc! is a proc macro that is producing a string literal with its span 
183+     //     > set to its input it's not marked as from expansion, and since it's compatible 
184+     //     > tokenization wise clippy_utils::is_from_proc_macro wouldn't catch it either 
185+     // This might be a relatively expensive test, so do it only we are ready to replace. 
186+     // See more examples in tests/ui/uninlined_format_args.rs 
187+ 
188+     span_lint_and_then ( 
189+         cx, 
190+         UNINLINED_FORMAT_ARGS , 
191+         call_site, 
192+         "variables can be used directly in the `format!` string" , 
193+         |diag| { 
194+             diag. multipart_suggestion ( "change this to" ,  fixes,  Applicability :: MachineApplicable ) ; 
195+         } , 
196+     ) ; 
197+ } 
198+ 
199+ fn  check_one_arg ( cx :  & LateContext < ' _ > ,  param :  & FormatParam < ' _ > ,  fixes :  & mut  Vec < ( Span ,  String ) > )  -> bool  { 
200+     if  matches ! ( param. kind,  Implicit  | Starred  | Named ( _)  | Numbered ) 
201+         && let  ExprKind :: Path ( QPath :: Resolved ( None ,  path) )  = param. value . kind 
202+         && let  Path  {  span,  segments,  .. }  = path
203+         && let  [ segment]  = segments
204+     { 
205+         let  replacement = match  param. usage  { 
206+             FormatParamUsage :: Argument  => segment. ident . name . to_string ( ) , 
207+             FormatParamUsage :: Width  => format ! ( "{}$" ,  segment. ident. name) , 
208+             FormatParamUsage :: Precision  => format ! ( ".{}$" ,  segment. ident. name) , 
209+         } ; 
210+         fixes. push ( ( param. span ,  replacement) ) ; 
211+         let  arg_span = expand_past_previous_comma ( cx,  * span) ; 
212+         fixes. push ( ( arg_span,  String :: new ( ) ) ) ; 
213+         true   // successful inlining, continue checking 
214+     }  else  { 
215+         // if we can't inline a numbered argument, we can't continue 
216+         param. kind  != Numbered 
217+     } 
92218} 
93219
94220fn  outermost_expn_data ( expn_data :  ExpnData )  -> ExpnData  { 
@@ -170,7 +296,7 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
170296    } 
171297} 
172298
173- // Returns true if `hir_id` is referred to by multiple format params 
299+ ///  Returns true if `hir_id` is referred to by multiple format params 
174300fn  is_aliased ( args :  & FormatArgsExpn < ' _ > ,  hir_id :  HirId )  -> bool  { 
175301    args. params ( ) 
176302        . filter ( |param| param. value . hir_id  == hir_id) 
0 commit comments