]> git.proxmox.com Git - rustc.git/blob - src/tools/clippy/clippy_lints/src/format_args.rs
New upstream version 1.72.1+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / format_args.rs
1 use arrayvec::ArrayVec;
2 use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
3 use clippy_utils::is_diag_trait_item;
4 use clippy_utils::macros::{
5 find_format_arg_expr, find_format_args, format_arg_removal_span, format_placeholder_format_span, is_assert_macro,
6 is_format_macro, is_panic, root_macro_call, root_macro_call_first_node, FormatParamUsage,
7 };
8 use clippy_utils::msrvs::{self, Msrv};
9 use clippy_utils::source::snippet_opt;
10 use clippy_utils::ty::{implements_trait, is_type_lang_item};
11 use if_chain::if_chain;
12 use itertools::Itertools;
13 use rustc_ast::{
14 FormatArgPosition, FormatArgPositionKind, FormatArgsPiece, FormatArgumentKind, FormatCount, FormatOptions,
15 FormatPlaceholder, FormatTrait,
16 };
17 use rustc_errors::{
18 Applicability,
19 SuggestionStyle::{CompletelyHidden, ShowCode},
20 };
21 use rustc_hir::{Expr, ExprKind, LangItem};
22 use rustc_lint::{LateContext, LateLintPass, LintContext};
23 use rustc_middle::ty::adjustment::{Adjust, Adjustment};
24 use rustc_middle::ty::Ty;
25 use rustc_session::{declare_tool_lint, impl_lint_pass};
26 use rustc_span::def_id::DefId;
27 use rustc_span::edition::Edition::Edition2021;
28 use rustc_span::{sym, Span, Symbol};
29
30 declare_clippy_lint! {
31 /// ### What it does
32 /// Detects `format!` within the arguments of another macro that does
33 /// formatting such as `format!` itself, `write!` or `println!`. Suggests
34 /// inlining the `format!` call.
35 ///
36 /// ### Why is this bad?
37 /// The recommended code is both shorter and avoids a temporary allocation.
38 ///
39 /// ### Example
40 /// ```rust
41 /// # use std::panic::Location;
42 /// println!("error: {}", format!("something failed at {}", Location::caller()));
43 /// ```
44 /// Use instead:
45 /// ```rust
46 /// # use std::panic::Location;
47 /// println!("error: something failed at {}", Location::caller());
48 /// ```
49 #[clippy::version = "1.58.0"]
50 pub FORMAT_IN_FORMAT_ARGS,
51 perf,
52 "`format!` used in a macro that does formatting"
53 }
54
55 declare_clippy_lint! {
56 /// ### What it does
57 /// Checks for [`ToString::to_string`](https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string)
58 /// applied to a type that implements [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html)
59 /// in a macro that does formatting.
60 ///
61 /// ### Why is this bad?
62 /// Since the type implements `Display`, the use of `to_string` is
63 /// unnecessary.
64 ///
65 /// ### Example
66 /// ```rust
67 /// # use std::panic::Location;
68 /// println!("error: something failed at {}", Location::caller().to_string());
69 /// ```
70 /// Use instead:
71 /// ```rust
72 /// # use std::panic::Location;
73 /// println!("error: something failed at {}", Location::caller());
74 /// ```
75 #[clippy::version = "1.58.0"]
76 pub TO_STRING_IN_FORMAT_ARGS,
77 perf,
78 "`to_string` applied to a type that implements `Display` in format args"
79 }
80
81 declare_clippy_lint! {
82 /// ### What it does
83 /// Detect when a variable is not inlined in a format string,
84 /// and suggests to inline it.
85 ///
86 /// ### Why is this bad?
87 /// Non-inlined code is slightly more difficult to read and understand,
88 /// as it requires arguments to be matched against the format string.
89 /// The inlined syntax, where allowed, is simpler.
90 ///
91 /// ### Example
92 /// ```rust
93 /// # let var = 42;
94 /// # let width = 1;
95 /// # let prec = 2;
96 /// format!("{}", var);
97 /// format!("{v:?}", v = var);
98 /// format!("{0} {0}", var);
99 /// format!("{0:1$}", var, width);
100 /// format!("{:.*}", prec, var);
101 /// ```
102 /// Use instead:
103 /// ```rust
104 /// # let var = 42;
105 /// # let width = 1;
106 /// # let prec = 2;
107 /// format!("{var}");
108 /// format!("{var:?}");
109 /// format!("{var} {var}");
110 /// format!("{var:width$}");
111 /// format!("{var:.prec$}");
112 /// ```
113 ///
114 /// If allow-mixed-uninlined-format-args is set to false in clippy.toml,
115 /// the following code will also trigger the lint:
116 /// ```rust
117 /// # let var = 42;
118 /// format!("{} {}", var, 1+2);
119 /// ```
120 /// Use instead:
121 /// ```rust
122 /// # let var = 42;
123 /// format!("{var} {}", 1+2);
124 /// ```
125 ///
126 /// ### Known Problems
127 ///
128 /// If a format string contains a numbered argument that cannot be inlined
129 /// nothing will be suggested, e.g. `println!("{0}={1}", var, 1+2)`.
130 #[clippy::version = "1.66.0"]
131 pub UNINLINED_FORMAT_ARGS,
132 pedantic,
133 "using non-inlined variables in `format!` calls"
134 }
135
136 declare_clippy_lint! {
137 /// ### What it does
138 /// Detects [formatting parameters] that have no effect on the output of
139 /// `format!()`, `println!()` or similar macros.
140 ///
141 /// ### Why is this bad?
142 /// Shorter format specifiers are easier to read, it may also indicate that
143 /// an expected formatting operation such as adding padding isn't happening.
144 ///
145 /// ### Example
146 /// ```rust
147 /// println!("{:.}", 1.0);
148 ///
149 /// println!("not padded: {:5}", format_args!("..."));
150 /// ```
151 /// Use instead:
152 /// ```rust
153 /// println!("{}", 1.0);
154 ///
155 /// println!("not padded: {}", format_args!("..."));
156 /// // OR
157 /// println!("padded: {:5}", format!("..."));
158 /// ```
159 ///
160 /// [formatting parameters]: https://doc.rust-lang.org/std/fmt/index.html#formatting-parameters
161 #[clippy::version = "1.66.0"]
162 pub UNUSED_FORMAT_SPECS,
163 complexity,
164 "use of a format specifier that has no effect"
165 }
166
167 impl_lint_pass!(FormatArgs => [
168 FORMAT_IN_FORMAT_ARGS,
169 TO_STRING_IN_FORMAT_ARGS,
170 UNINLINED_FORMAT_ARGS,
171 UNUSED_FORMAT_SPECS,
172 ]);
173
174 pub struct FormatArgs {
175 msrv: Msrv,
176 ignore_mixed: bool,
177 }
178
179 impl FormatArgs {
180 #[must_use]
181 pub fn new(msrv: Msrv, allow_mixed_uninlined_format_args: bool) -> Self {
182 Self {
183 msrv,
184 ignore_mixed: allow_mixed_uninlined_format_args,
185 }
186 }
187 }
188
189 impl<'tcx> LateLintPass<'tcx> for FormatArgs {
190 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
191 let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return };
192 if !is_format_macro(cx, macro_call.def_id) {
193 return;
194 }
195 let name = cx.tcx.item_name(macro_call.def_id);
196
197 find_format_args(cx, expr, macro_call.expn, |format_args| {
198 for piece in &format_args.template {
199 if let FormatArgsPiece::Placeholder(placeholder) = piece
200 && let Ok(index) = placeholder.argument.index
201 && let Some(arg) = format_args.arguments.all_args().get(index)
202 {
203 let arg_expr = find_format_arg_expr(expr, arg);
204
205 check_unused_format_specifier(cx, placeholder, arg_expr);
206
207 if placeholder.format_trait != FormatTrait::Display
208 || placeholder.format_options != FormatOptions::default()
209 || is_aliased(format_args, index)
210 {
211 continue;
212 }
213
214 if let Ok(arg_hir_expr) = arg_expr {
215 check_format_in_format_args(cx, macro_call.span, name, arg_hir_expr);
216 check_to_string_in_format_args(cx, name, arg_hir_expr);
217 }
218 }
219 }
220
221 if self.msrv.meets(msrvs::FORMAT_ARGS_CAPTURE) {
222 check_uninlined_args(cx, format_args, macro_call.span, macro_call.def_id, self.ignore_mixed);
223 }
224 });
225 }
226
227 extract_msrv_attr!(LateContext);
228 }
229
230 fn check_unused_format_specifier(
231 cx: &LateContext<'_>,
232 placeholder: &FormatPlaceholder,
233 arg_expr: Result<&Expr<'_>, &rustc_ast::Expr>,
234 ) {
235 let ty_or_ast_expr = arg_expr.map(|expr| cx.typeck_results().expr_ty(expr).peel_refs());
236
237 let is_format_args = match ty_or_ast_expr {
238 Ok(ty) => is_type_lang_item(cx, ty, LangItem::FormatArguments),
239 Err(expr) => matches!(expr.peel_parens_and_refs().kind, rustc_ast::ExprKind::FormatArgs(_)),
240 };
241
242 let options = &placeholder.format_options;
243
244 let arg_span = match arg_expr {
245 Ok(expr) => expr.span,
246 Err(expr) => expr.span,
247 };
248
249 if let Some(placeholder_span) = placeholder.span
250 && is_format_args
251 && *options != FormatOptions::default()
252 {
253 span_lint_and_then(
254 cx,
255 UNUSED_FORMAT_SPECS,
256 placeholder_span,
257 "format specifiers have no effect on `format_args!()`",
258 |diag| {
259 let mut suggest_format = |spec| {
260 let message = format!("for the {spec} to apply consider using `format!()`");
261
262 if let Some(mac_call) = root_macro_call(arg_span)
263 && cx.tcx.is_diagnostic_item(sym::format_args_macro, mac_call.def_id)
264 {
265 diag.span_suggestion(
266 cx.sess().source_map().span_until_char(mac_call.span, '!'),
267 message,
268 "format",
269 Applicability::MaybeIncorrect,
270 );
271 } else {
272 diag.help(message);
273 }
274 };
275
276 if options.width.is_some() {
277 suggest_format("width");
278 }
279
280 if options.precision.is_some() {
281 suggest_format("precision");
282 }
283
284 if let Some(format_span) = format_placeholder_format_span(placeholder) {
285 diag.span_suggestion_verbose(
286 format_span,
287 "if the current behavior is intentional, remove the format specifiers",
288 "",
289 Applicability::MaybeIncorrect,
290 );
291 }
292 },
293 );
294 }
295 }
296
297 fn check_uninlined_args(
298 cx: &LateContext<'_>,
299 args: &rustc_ast::FormatArgs,
300 call_site: Span,
301 def_id: DefId,
302 ignore_mixed: bool,
303 ) {
304 if args.span.from_expansion() {
305 return;
306 }
307 if call_site.edition() < Edition2021 && (is_panic(cx, def_id) || is_assert_macro(cx, def_id)) {
308 // panic!, assert!, and debug_assert! before 2021 edition considers a single string argument as
309 // non-format
310 return;
311 }
312
313 let mut fixes = Vec::new();
314 // If any of the arguments are referenced by an index number,
315 // and that argument is not a simple variable and cannot be inlined,
316 // we cannot remove any other arguments in the format string,
317 // because the index numbers might be wrong after inlining.
318 // Example of an un-inlinable format: print!("{}{1}", foo, 2)
319 for (pos, usage) in format_arg_positions(args) {
320 if !check_one_arg(args, pos, usage, &mut fixes, ignore_mixed) {
321 return;
322 }
323 }
324
325 if fixes.is_empty() {
326 return;
327 }
328
329 // multiline span display suggestion is sometimes broken: https://github.com/rust-lang/rust/pull/102729#discussion_r988704308
330 // in those cases, make the code suggestion hidden
331 let multiline_fix = fixes.iter().any(|(span, _)| cx.sess().source_map().is_multiline(*span));
332
333 // Suggest removing each argument only once, for example in `format!("{0} {0}", arg)`.
334 fixes.sort_unstable_by_key(|(span, _)| *span);
335 fixes.dedup_by_key(|(span, _)| *span);
336
337 span_lint_and_then(
338 cx,
339 UNINLINED_FORMAT_ARGS,
340 call_site,
341 "variables can be used directly in the `format!` string",
342 |diag| {
343 diag.multipart_suggestion_with_style(
344 "change this to",
345 fixes,
346 Applicability::MachineApplicable,
347 if multiline_fix { CompletelyHidden } else { ShowCode },
348 );
349 },
350 );
351 }
352
353 fn check_one_arg(
354 args: &rustc_ast::FormatArgs,
355 pos: &FormatArgPosition,
356 usage: FormatParamUsage,
357 fixes: &mut Vec<(Span, String)>,
358 ignore_mixed: bool,
359 ) -> bool {
360 let index = pos.index.unwrap();
361 let arg = &args.arguments.all_args()[index];
362
363 if !matches!(arg.kind, FormatArgumentKind::Captured(_))
364 && let rustc_ast::ExprKind::Path(None, path) = &arg.expr.kind
365 && let [segment] = path.segments.as_slice()
366 && segment.args.is_none()
367 && let Some(arg_span) = format_arg_removal_span(args, index)
368 && let Some(pos_span) = pos.span
369 {
370 let replacement = match usage {
371 FormatParamUsage::Argument => segment.ident.name.to_string(),
372 FormatParamUsage::Width => format!("{}$", segment.ident.name),
373 FormatParamUsage::Precision => format!(".{}$", segment.ident.name),
374 };
375 fixes.push((pos_span, replacement));
376 fixes.push((arg_span, String::new()));
377 true // successful inlining, continue checking
378 } else {
379 // Do not continue inlining (return false) in case
380 // * if we can't inline a numbered argument, e.g. `print!("{0} ...", foo.bar, ...)`
381 // * if allow_mixed_uninlined_format_args is false and this arg hasn't been inlined already
382 pos.kind != FormatArgPositionKind::Number
383 && (!ignore_mixed || matches!(arg.kind, FormatArgumentKind::Captured(_)))
384 }
385 }
386
387 fn check_format_in_format_args(cx: &LateContext<'_>, call_site: Span, name: Symbol, arg: &Expr<'_>) {
388 let expn_data = arg.span.ctxt().outer_expn_data();
389 if expn_data.call_site.from_expansion() {
390 return;
391 }
392 let Some(mac_id) = expn_data.macro_def_id else { return };
393 if !cx.tcx.is_diagnostic_item(sym::format_macro, mac_id) {
394 return;
395 }
396 span_lint_and_then(
397 cx,
398 FORMAT_IN_FORMAT_ARGS,
399 call_site,
400 &format!("`format!` in `{name}!` args"),
401 |diag| {
402 diag.help(format!(
403 "combine the `format!(..)` arguments with the outer `{name}!(..)` call"
404 ));
405 diag.help("or consider changing `format!` to `format_args!`");
406 },
407 );
408 }
409
410 fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Expr<'_>) {
411 if_chain! {
412 if !value.span.from_expansion();
413 if let ExprKind::MethodCall(_, receiver, [], to_string_span) = value.kind;
414 if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(value.hir_id);
415 if is_diag_trait_item(cx, method_def_id, sym::ToString);
416 let receiver_ty = cx.typeck_results().expr_ty(receiver);
417 if let Some(display_trait_id) = cx.tcx.get_diagnostic_item(sym::Display);
418 let (n_needed_derefs, target) =
419 count_needed_derefs(receiver_ty, cx.typeck_results().expr_adjustments(receiver).iter());
420 if implements_trait(cx, target, display_trait_id, &[]);
421 if let Some(sized_trait_id) = cx.tcx.lang_items().sized_trait();
422 if let Some(receiver_snippet) = snippet_opt(cx, receiver.span);
423 then {
424 let needs_ref = !implements_trait(cx, receiver_ty, sized_trait_id, &[]);
425 if n_needed_derefs == 0 && !needs_ref {
426 span_lint_and_sugg(
427 cx,
428 TO_STRING_IN_FORMAT_ARGS,
429 to_string_span.with_lo(receiver.span.hi()),
430 &format!(
431 "`to_string` applied to a type that implements `Display` in `{name}!` args"
432 ),
433 "remove this",
434 String::new(),
435 Applicability::MachineApplicable,
436 );
437 } else {
438 span_lint_and_sugg(
439 cx,
440 TO_STRING_IN_FORMAT_ARGS,
441 value.span,
442 &format!(
443 "`to_string` applied to a type that implements `Display` in `{name}!` args"
444 ),
445 "use this",
446 format!(
447 "{}{:*>n_needed_derefs$}{receiver_snippet}",
448 if needs_ref { "&" } else { "" },
449 ""
450 ),
451 Applicability::MachineApplicable,
452 );
453 }
454 }
455 }
456 }
457
458 fn format_arg_positions(
459 format_args: &rustc_ast::FormatArgs,
460 ) -> impl Iterator<Item = (&FormatArgPosition, FormatParamUsage)> {
461 format_args.template.iter().flat_map(|piece| match piece {
462 FormatArgsPiece::Placeholder(placeholder) => {
463 let mut positions = ArrayVec::<_, 3>::new();
464
465 positions.push((&placeholder.argument, FormatParamUsage::Argument));
466 if let Some(FormatCount::Argument(position)) = &placeholder.format_options.width {
467 positions.push((position, FormatParamUsage::Width));
468 }
469 if let Some(FormatCount::Argument(position)) = &placeholder.format_options.precision {
470 positions.push((position, FormatParamUsage::Precision));
471 }
472
473 positions
474 },
475 FormatArgsPiece::Literal(_) => ArrayVec::new(),
476 })
477 }
478
479 /// Returns true if the format argument at `index` is referred to by multiple format params
480 fn is_aliased(format_args: &rustc_ast::FormatArgs, index: usize) -> bool {
481 format_arg_positions(format_args)
482 .filter(|(position, _)| position.index == Ok(index))
483 .at_most_one()
484 .is_err()
485 }
486
487 fn count_needed_derefs<'tcx, I>(mut ty: Ty<'tcx>, mut iter: I) -> (usize, Ty<'tcx>)
488 where
489 I: Iterator<Item = &'tcx Adjustment<'tcx>>,
490 {
491 let mut n_total = 0;
492 let mut n_needed = 0;
493 loop {
494 if let Some(Adjustment {
495 kind: Adjust::Deref(overloaded_deref),
496 target,
497 }) = iter.next()
498 {
499 n_total += 1;
500 if overloaded_deref.is_some() {
501 n_needed = n_total;
502 }
503 ty = *target;
504 } else {
505 return (n_needed, ty);
506 }
507 }
508 }