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