]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/format_args.rs
Rollup merge of #90741 - mbartlett21:patch-4, r=dtolnay
[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::higher::{FormatArgsArg, FormatArgsExpn, FormatExpn};
3 use clippy_utils::source::snippet_opt;
4 use clippy_utils::ty::implements_trait;
5 use clippy_utils::{is_diag_trait_item, match_def_path, paths};
6 use if_chain::if_chain;
7 use rustc_errors::Applicability;
8 use rustc_hir::{Expr, ExprKind};
9 use rustc_lint::{LateContext, LateLintPass};
10 use rustc_middle::ty::adjustment::{Adjust, Adjustment};
11 use rustc_middle::ty::Ty;
12 use rustc_session::{declare_lint_pass, declare_tool_lint};
13 use rustc_span::{sym, ExpnData, ExpnKind, Span, Symbol};
14
15 declare_clippy_lint! {
16     /// ### What it does
17     /// Detects `format!` within the arguments of another macro that does
18     /// formatting such as `format!` itself, `write!` or `println!`. Suggests
19     /// inlining the `format!` call.
20     ///
21     /// ### Why is this bad?
22     /// The recommended code is both shorter and avoids a temporary allocation.
23     ///
24     /// ### Example
25     /// ```rust
26     /// # use std::panic::Location;
27     /// println!("error: {}", format!("something failed at {}", Location::caller()));
28     /// ```
29     /// Use instead:
30     /// ```rust
31     /// # use std::panic::Location;
32     /// println!("error: something failed at {}", Location::caller());
33     /// ```
34     #[clippy::version = "1.58.0"]
35     pub FORMAT_IN_FORMAT_ARGS,
36     perf,
37     "`format!` used in a macro that does formatting"
38 }
39
40 declare_clippy_lint! {
41     /// ### What it does
42     /// Checks for [`ToString::to_string`](https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string)
43     /// applied to a type that implements [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html)
44     /// in a macro that does formatting.
45     ///
46     /// ### Why is this bad?
47     /// Since the type implements `Display`, the use of `to_string` is
48     /// unnecessary.
49     ///
50     /// ### Example
51     /// ```rust
52     /// # use std::panic::Location;
53     /// println!("error: something failed at {}", Location::caller().to_string());
54     /// ```
55     /// Use instead:
56     /// ```rust
57     /// # use std::panic::Location;
58     /// println!("error: something failed at {}", Location::caller());
59     /// ```
60     #[clippy::version = "1.58.0"]
61     pub TO_STRING_IN_FORMAT_ARGS,
62     perf,
63     "`to_string` applied to a type that implements `Display` in format args"
64 }
65
66 declare_lint_pass!(FormatArgs => [FORMAT_IN_FORMAT_ARGS, TO_STRING_IN_FORMAT_ARGS]);
67
68 const FORMAT_MACRO_PATHS: &[&[&str]] = &[
69     &paths::FORMAT_ARGS_MACRO,
70     &paths::ASSERT_EQ_MACRO,
71     &paths::ASSERT_MACRO,
72     &paths::ASSERT_NE_MACRO,
73     &paths::EPRINT_MACRO,
74     &paths::EPRINTLN_MACRO,
75     &paths::PRINT_MACRO,
76     &paths::PRINTLN_MACRO,
77     &paths::WRITE_MACRO,
78     &paths::WRITELN_MACRO,
79 ];
80
81 const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[sym::format_macro, sym::std_panic_macro];
82
83 impl<'tcx> LateLintPass<'tcx> for FormatArgs {
84     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
85         if_chain! {
86             if let Some(format_args) = FormatArgsExpn::parse(expr);
87             let expr_expn_data = expr.span.ctxt().outer_expn_data();
88             let outermost_expn_data = outermost_expn_data(expr_expn_data);
89             if let Some(macro_def_id) = outermost_expn_data.macro_def_id;
90             if FORMAT_MACRO_PATHS
91                 .iter()
92                 .any(|path| match_def_path(cx, macro_def_id, path))
93                 || FORMAT_MACRO_DIAG_ITEMS
94                     .iter()
95                     .any(|diag_item| cx.tcx.is_diagnostic_item(*diag_item, macro_def_id));
96             if let ExpnKind::Macro(_, name) = outermost_expn_data.kind;
97             if let Some(args) = format_args.args();
98             then {
99                 for (i, arg) in args.iter().enumerate() {
100                     if !arg.is_display() {
101                         continue;
102                     }
103                     if arg.has_string_formatting() {
104                         continue;
105                     }
106                     if is_aliased(&args, i) {
107                         continue;
108                     }
109                     check_format_in_format_args(cx, outermost_expn_data.call_site, name, arg);
110                     check_to_string_in_format_args(cx, name, arg);
111                 }
112             }
113         }
114     }
115 }
116
117 fn outermost_expn_data(expn_data: ExpnData) -> ExpnData {
118     if expn_data.call_site.from_expansion() {
119         outermost_expn_data(expn_data.call_site.ctxt().outer_expn_data())
120     } else {
121         expn_data
122     }
123 }
124
125 fn check_format_in_format_args(cx: &LateContext<'_>, call_site: Span, name: Symbol, arg: &FormatArgsArg<'_>) {
126     if_chain! {
127         if FormatExpn::parse(arg.value).is_some();
128         if !arg.value.span.ctxt().outer_expn_data().call_site.from_expansion();
129         then {
130             span_lint_and_then(
131                 cx,
132                 FORMAT_IN_FORMAT_ARGS,
133                 call_site,
134                 &format!("`format!` in `{}!` args", name),
135                 |diag| {
136                     diag.help(&format!(
137                         "combine the `format!(..)` arguments with the outer `{}!(..)` call",
138                         name
139                     ));
140                     diag.help("or consider changing `format!` to `format_args!`");
141                 },
142             );
143         }
144     }
145 }
146
147 fn check_to_string_in_format_args<'tcx>(cx: &LateContext<'tcx>, name: Symbol, arg: &FormatArgsArg<'tcx>) {
148     let value = arg.value;
149     if_chain! {
150         if !value.span.from_expansion();
151         if let ExprKind::MethodCall(_, _, [receiver], _) = value.kind;
152         if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(value.hir_id);
153         if is_diag_trait_item(cx, method_def_id, sym::ToString);
154         let receiver_ty = cx.typeck_results().expr_ty(receiver);
155         if let Some(display_trait_id) = cx.tcx.get_diagnostic_item(sym::Display);
156         if let Some(receiver_snippet) = snippet_opt(cx, receiver.span);
157         then {
158             let (n_needed_derefs, target) = count_needed_derefs(
159                 receiver_ty,
160                 cx.typeck_results().expr_adjustments(receiver).iter(),
161             );
162             if implements_trait(cx, target, display_trait_id, &[]) {
163                 if n_needed_derefs == 0 {
164                     span_lint_and_sugg(
165                         cx,
166                         TO_STRING_IN_FORMAT_ARGS,
167                         value.span.with_lo(receiver.span.hi()),
168                         &format!("`to_string` applied to a type that implements `Display` in `{}!` args", name),
169                         "remove this",
170                         String::new(),
171                         Applicability::MachineApplicable,
172                     );
173                 } else {
174                     span_lint_and_sugg(
175                         cx,
176                         TO_STRING_IN_FORMAT_ARGS,
177                         value.span,
178                         &format!("`to_string` applied to a type that implements `Display` in `{}!` args", name),
179                         "use this",
180                         format!("{:*>width$}{}", "", receiver_snippet, width = n_needed_derefs),
181                         Applicability::MachineApplicable,
182                     );
183                 }
184             }
185         }
186     }
187 }
188
189 // Returns true if `args[i]` "refers to" or "is referred to by" another argument.
190 fn is_aliased(args: &[FormatArgsArg<'_>], i: usize) -> bool {
191     let value = args[i].value;
192     args.iter()
193         .enumerate()
194         .any(|(j, arg)| i != j && std::ptr::eq(value, arg.value))
195 }
196
197 fn count_needed_derefs<'tcx, I>(mut ty: Ty<'tcx>, mut iter: I) -> (usize, Ty<'tcx>)
198 where
199     I: Iterator<Item = &'tcx Adjustment<'tcx>>,
200 {
201     let mut n_total = 0;
202     let mut n_needed = 0;
203     loop {
204         if let Some(Adjustment {
205             kind: Adjust::Deref(overloaded_deref),
206             target,
207         }) = iter.next()
208         {
209             n_total += 1;
210             if overloaded_deref.is_some() {
211                 n_needed = n_total;
212             }
213             ty = target;
214         } else {
215             return (n_needed, ty);
216         }
217     }
218 }