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