1
1
use 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} ;
5
5
use clippy_utils:: ty:: implements_trait;
6
+ use clippy_utils:: { is_diag_trait_item, meets_msrv, msrvs} ;
6
7
use if_chain:: if_chain;
7
8
use itertools:: Itertools ;
8
9
use rustc_errors:: Applicability ;
9
- use rustc_hir:: { Expr , ExprKind , HirId } ;
10
+ use rustc_hir:: { Expr , ExprKind , HirId , Path , QPath } ;
10
11
use rustc_lint:: { LateContext , LateLintPass } ;
11
12
use rustc_middle:: ty:: adjustment:: { Adjust , Adjustment } ;
12
13
use 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} ;
14
16
use rustc_span:: { sym, ExpnData , ExpnKind , Span , Symbol } ;
15
17
16
18
declare_clippy_lint ! {
@@ -64,7 +66,72 @@ declare_clippy_lint! {
64
66
"`to_string` applied to a type that implements `Display` in format args"
65
67
}
66
68
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
+ }
68
135
69
136
impl < ' tcx > LateLintPass < ' tcx > for FormatArgs {
70
137
fn check_expr ( & mut self , cx : & LateContext < ' tcx > , expr : & ' tcx Expr < ' tcx > ) {
@@ -86,9 +153,69 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
86
153
check_format_in_format_args( cx, outermost_expn_data. call_site, name, arg. param. value) ;
87
154
check_to_string_in_format_args( cx, name, arg. param. value) ;
88
155
}
156
+ if meets_msrv( self . msrv, msrvs:: FORMAT_ARGS_CAPTURE ) {
157
+ check_uninlined_args( cx, & format_args, outermost_expn_data. call_site) ;
158
+ }
89
159
}
90
160
}
91
161
}
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
+ }
92
219
}
93
220
94
221
fn outermost_expn_data ( expn_data : ExpnData ) -> ExpnData {
@@ -170,7 +297,7 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
170
297
}
171
298
}
172
299
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
174
301
fn is_aliased ( args : & FormatArgsExpn < ' _ > , hir_id : HirId ) -> bool {
175
302
args. params ( )
176
303
. filter ( |param| param. value . hir_id == hir_id)
0 commit comments