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,72 @@ 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); // implied variables
85+ /// format!("{0}", var); // positional variables
86+ /// format!("{v}", v=var); // named variables
87+ /// format!("{0} {0}", var); // aliased variables
88+ /// format!("{0:1$}", var, width); // width support
89+ /// format!("{0:.1$}", var, prec); // precision support
90+ /// format!("{:.*}", prec, var); // asterisk support
91+ /// ```
92+ /// Use instead:
93+ /// ```rust
94+ /// # let var = 42;
95+ /// # let width = 1;
96+ /// # let prec = 2;
97+ /// format!("{var}"); // implied, positional, and named variables
98+ /// format!("{var} {var}"); // aliased variables
99+ /// format!("{var:width$}"); // width support
100+ /// format!("{var:.prec$}"); // precision and asterisk support
101+ /// ```
102+ ///
103+ /// ### Known Problems
104+ ///
105+ /// * There may be a false positive if the format string is wrapped in a macro call:
106+ /// ```rust
107+ /// # let var = 42;
108+ /// macro_rules! no_param_str { () => { "{}" }; }
109+ /// macro_rules! pass_through { ($expr:expr) => { $expr }; }
110+ /// println!(no_param_str!(), var);
111+ /// println!(pass_through!("{}"), var);
112+ /// ```
113+ ///
114+ /// * Format string uses an indexed argument that cannot be inlined.
115+ /// Supporting this case requires re-indexing of the format string.
116+ /// Until implemented, `print!("{0}={1}", var, 1+2)` should be changed to `print!("{var}={0}", 1+2)` by hand.
117+ #[ clippy:: version = "1.65.0" ]
118+ pub UNINLINED_FORMAT_ARGS ,
119+ pedantic,
120+ "using non-inlined variables in `format!` calls"
121+ }
122+
123+ impl_lint_pass ! ( FormatArgs => [ FORMAT_IN_FORMAT_ARGS , UNINLINED_FORMAT_ARGS , TO_STRING_IN_FORMAT_ARGS ] ) ;
124+
125+ pub struct FormatArgs {
126+ msrv : Option < RustcVersion > ,
127+ }
128+
129+ impl FormatArgs {
130+ #[ must_use]
131+ pub fn new ( msrv : Option < RustcVersion > ) -> Self {
132+ Self { msrv }
133+ }
134+ }
68135
69136impl < ' tcx > LateLintPass < ' tcx > for FormatArgs {
70137 fn check_expr ( & mut self , cx : & LateContext < ' tcx > , expr : & ' tcx Expr < ' tcx > ) {
@@ -86,9 +153,69 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
86153 check_format_in_format_args( cx, outermost_expn_data. call_site, name, arg. param. value) ;
87154 check_to_string_in_format_args( cx, name, arg. param. value) ;
88155 }
156+ if meets_msrv( self . msrv, msrvs:: FORMAT_ARGS_CAPTURE ) {
157+ check_uninlined_args( cx, & format_args, outermost_expn_data. call_site) ;
158+ }
89159 }
90160 }
91161 }
162+
163+ extract_msrv_attr ! ( LateContext ) ;
164+ }
165+
166+ fn check_uninlined_args ( cx : & LateContext < ' _ > , args : & FormatArgsExpn < ' _ > , call_site : Span ) {
167+ let mut fixes = Vec :: new ( ) ;
168+ // If any of the arguments are referenced by an index number,
169+ // and that argument is not a simple variable and cannot be inlined,
170+ // we cannot remove any other arguments in the format string,
171+ // because the index numbers might be wrong after inlining.
172+ // Example of an un-inlinable format: print!("{}{1}", foo, 2)
173+ if !args. params ( ) . all ( |p| check_one_arg ( cx, & p, & mut fixes) ) || fixes. is_empty ( ) {
174+ return ;
175+ }
176+
177+ // FIXME: Properly ignore a rare case where the format string is wrapped in a macro.
178+ // Example: `format!(indoc!("{}"), foo);`
179+ // If inlined, they will cause a compilation error:
180+ // > to avoid ambiguity, `format_args!` cannot capture variables
181+ // > when the format string is expanded from a macro
182+ // @Alexendoo explanation:
183+ // > indoc! is a proc macro that is producing a string literal with its span
184+ // > set to its input it's not marked as from expansion, and since it's compatible
185+ // > tokenization wise clippy_utils::is_from_proc_macro wouldn't catch it either
186+ // This might be a relatively expensive test, so do it only we are ready to replace.
187+ // See more examples in tests/ui/uninlined_format_args.rs
188+
189+ span_lint_and_then (
190+ cx,
191+ UNINLINED_FORMAT_ARGS ,
192+ call_site,
193+ "variables can be used directly in the `format!` string" ,
194+ |diag| {
195+ diag. multipart_suggestion ( "change this to" , fixes, Applicability :: MachineApplicable ) ;
196+ } ,
197+ ) ;
198+ }
199+
200+ fn check_one_arg ( cx : & LateContext < ' _ > , param : & FormatParam < ' _ > , fixes : & mut Vec < ( Span , String ) > ) -> bool {
201+ if matches ! ( param. kind, Implicit | Starred | Named ( _) | Numbered )
202+ && let ExprKind :: Path ( QPath :: Resolved ( None , path) ) = param. value . kind
203+ && let Path { span, segments, .. } = path
204+ && let [ segment] = segments
205+ {
206+ let replacement = match param. usage {
207+ FormatParamUsage :: Argument => segment. ident . name . to_string ( ) ,
208+ FormatParamUsage :: Width => format ! ( "{}$" , segment. ident. name) ,
209+ FormatParamUsage :: Precision => format ! ( ".{}$" , segment. ident. name) ,
210+ } ;
211+ fixes. push ( ( param. span , replacement) ) ;
212+ let arg_span = expand_past_previous_comma ( cx, * span) ;
213+ fixes. push ( ( arg_span, String :: new ( ) ) ) ;
214+ true // successful inlining, continue checking
215+ } else {
216+ // if we can't inline a numbered argument, we can't continue
217+ param. kind != Numbered
218+ }
92219}
93220
94221fn outermost_expn_data ( expn_data : ExpnData ) -> ExpnData {
@@ -170,7 +297,7 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
170297 }
171298}
172299
173- // Returns true if `hir_id` is referred to by multiple format params
300+ /// Returns true if `hir_id` is referred to by multiple format params
174301fn is_aliased ( args : & FormatArgsExpn < ' _ > , hir_id : HirId ) -> bool {
175302 args. params ( )
176303 . filter ( |param| param. value . hir_id == hir_id)
0 commit comments