@@ -145,28 +145,8 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
145
145
check_format_in_format_args( cx, outermost_expn_data. call_site, name, arg. param. value) ;
146
146
check_to_string_in_format_args( cx, name, arg. param. value) ;
147
147
}
148
-
149
- if !meets_msrv( self . msrv, msrvs:: FORMAT_ARGS_CAPTURE ) {
150
- return ;
151
- }
152
- let mut inline_spans = Vec :: new( ) ;
153
- // If any of the arguments are referenced by an index number,
154
- // and that argument is not a simple variable and cannot be inlined,
155
- // we cannot remove any other arguments in the format string,
156
- // because the index numbers might be wrong after inlining.
157
- // Example of an un-inlinable format: print!("{}{1}", foo, 2)
158
- let do_inlining = format_args. params( ) . all( |p| check_inline( cx, & p, & mut inline_spans) ) ;
159
- if do_inlining && !inline_spans. is_empty( )
160
- {
161
- span_lint_and_then(
162
- cx,
163
- UNINLINED_FORMAT_ARGS ,
164
- outermost_expn_data. call_site,
165
- "variables can be used directly in the `format!` string" ,
166
- |diag| {
167
- diag. multipart_suggestion( "change this to" , inline_spans, Applicability :: MachineApplicable ) ;
168
- } ,
169
- ) ;
148
+ if meets_msrv( self . msrv, msrvs:: FORMAT_ARGS_CAPTURE ) {
149
+ check_uninlined_args( cx, & format_args, outermost_expn_data. call_site) ;
170
150
}
171
151
}
172
152
}
@@ -175,7 +155,53 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
175
155
extract_msrv_attr ! ( LateContext ) ;
176
156
}
177
157
178
- fn check_inline ( cx : & LateContext < ' _ > , param : & FormatParam < ' _ > , inline_spans : & mut Vec < ( Span , String ) > ) -> bool {
158
+ fn check_uninlined_args ( cx : & LateContext < ' _ > , args : & FormatArgsExpn < ' _ > , call_site : Span ) {
159
+ let mut fixes = Vec :: new ( ) ;
160
+ // If any of the arguments are referenced by an index number,
161
+ // and that argument is not a simple variable and cannot be inlined,
162
+ // we cannot remove any other arguments in the format string,
163
+ // because the index numbers might be wrong after inlining.
164
+ // Example of an un-inlinable format: print!("{}{1}", foo, 2)
165
+ if !args. params ( ) . all ( |p| check_one_arg ( cx, & p, & mut fixes) ) || fixes. is_empty ( ) {
166
+ return ;
167
+ }
168
+
169
+ // Handle a very rare case: `format!(indoc!("{}"), foo)` must not be inlined.
170
+ // If inlined, it will cause a compilation error:
171
+ // > to avoid ambiguity, `format_args!` cannot capture variables
172
+ // > when the format string is expanded from a macro
173
+ // @Alexendoo explanation:
174
+ // > indoc! is a proc macro that is producing a string literal with its span
175
+ // > set to its input it's not marked as from expansion, and since it's compatible
176
+ // > tokenization wise clippy_utils::is_from_proc_macro wouldn't catch it either
177
+ // Hacky solution (TBD if it should be improved):
178
+ // iterate over all tokens between the format string and the first argument,
179
+ // and ensure we do not see any closing brackets/braces/parens.
180
+ // This might be a relatively expensive test, so do it only we are ready to replace.
181
+
182
+ /*
183
+ let fmt_str_span = args.format_string.span;
184
+ println!("1st: {}", snippet_opt(cx, fmt_str_span).unwrap());
185
+ let first_arg_span = walk_chain(fmt_str_span, fmt_str_span.ctxt());
186
+ println!("2nd: {}", snippet_opt(cx, first_arg_span).unwrap());
187
+ let between = fmt_str_span.with_lo(fmt_str_span.hi()).with_hi(first_arg_span.lo());
188
+ println!("3rd: {}", snippet_opt(cx, between).unwrap());
189
+ if let Some(between_snippet) = snippet_opt(cx, between) {
190
+ if !tokenize(&between_snippet).any(|t| matches!(t.kind, CloseParen | CloseBrace | CloseBracket))
191
+ */
192
+
193
+ span_lint_and_then (
194
+ cx,
195
+ UNINLINED_FORMAT_ARGS ,
196
+ call_site,
197
+ "variables can be used directly in the `format!` string" ,
198
+ |diag| {
199
+ diag. multipart_suggestion ( "change this to" , fixes, Applicability :: MachineApplicable ) ;
200
+ } ,
201
+ ) ;
202
+ }
203
+
204
+ fn check_one_arg ( cx : & LateContext < ' _ > , param : & FormatParam < ' _ > , fixes : & mut Vec < ( Span , String ) > ) -> bool {
179
205
if matches ! ( param. kind, Implicit | Starred | Named ( _) | Numbered )
180
206
&& let ExprKind :: Path ( QPath :: Resolved ( None , path) ) = param. value . kind
181
207
&& let Path { span, segments, .. } = path
@@ -186,9 +212,9 @@ fn check_inline(cx: &LateContext<'_>, param: &FormatParam<'_>, inline_spans: &mu
186
212
FormatParamUsage :: Width => format ! ( "{}$" , segment. ident. name) ,
187
213
FormatParamUsage :: Precision => format ! ( ".{}$" , segment. ident. name) ,
188
214
} ;
189
- inline_spans . push ( ( param. span , replacement) ) ;
215
+ fixes . push ( ( param. span , replacement) ) ;
190
216
let arg_span = expand_past_previous_comma ( cx, * span) ;
191
- inline_spans . push ( ( arg_span, String :: new ( ) ) ) ;
217
+ fixes . push ( ( arg_span, String :: new ( ) ) ) ;
192
218
true // successful inlining, continue checking
193
219
} else {
194
220
// if we can't inline a numbered argument, we can't continue
0 commit comments