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