use arrayvec::ArrayVec; use clippy_config::msrvs::{self, Msrv}; use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; use clippy_utils::is_diag_trait_item; use clippy_utils::macros::{ find_format_arg_expr, find_format_args, format_arg_removal_span, format_placeholder_format_span, is_assert_macro, is_format_macro, is_panic, root_macro_call, root_macro_call_first_node, FormatParamUsage, MacroCall, }; use clippy_utils::source::snippet_opt; use clippy_utils::ty::{implements_trait, is_type_lang_item}; use itertools::Itertools; use rustc_ast::{ FormatArgPosition, FormatArgPositionKind, FormatArgsPiece, FormatArgumentKind, FormatCount, FormatOptions, FormatPlaceholder, FormatTrait, }; use rustc_errors::Applicability; use rustc_errors::SuggestionStyle::{CompletelyHidden, ShowCode}; use rustc_hir::{Expr, ExprKind, LangItem}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_middle::ty::adjustment::{Adjust, Adjustment}; use rustc_middle::ty::Ty; use rustc_session::impl_lint_pass; use rustc_span::edition::Edition::Edition2021; use rustc_span::{sym, Span, Symbol}; declare_clippy_lint! { /// ### What it does /// Detects `format!` within the arguments of another macro that does /// formatting such as `format!` itself, `write!` or `println!`. Suggests /// inlining the `format!` call. /// /// ### Why is this bad? /// The recommended code is both shorter and avoids a temporary allocation. /// /// ### Example /// ```no_run /// # use std::panic::Location; /// println!("error: {}", format!("something failed at {}", Location::caller())); /// ``` /// Use instead: /// ```no_run /// # use std::panic::Location; /// println!("error: something failed at {}", Location::caller()); /// ``` #[clippy::version = "1.58.0"] pub FORMAT_IN_FORMAT_ARGS, perf, "`format!` used in a macro that does formatting" } declare_clippy_lint! { /// ### What it does /// Checks for [`ToString::to_string`](https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string) /// applied to a type that implements [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html) /// in a macro that does formatting. /// /// ### Why is this bad? /// Since the type implements `Display`, the use of `to_string` is /// unnecessary. /// /// ### Example /// ```no_run /// # use std::panic::Location; /// println!("error: something failed at {}", Location::caller().to_string()); /// ``` /// Use instead: /// ```no_run /// # use std::panic::Location; /// println!("error: something failed at {}", Location::caller()); /// ``` #[clippy::version = "1.58.0"] pub TO_STRING_IN_FORMAT_ARGS, perf, "`to_string` applied to a type that implements `Display` in format args" } declare_clippy_lint! { /// ### What it does /// Detect when a variable is not inlined in a format string, /// and suggests to inline it. /// /// ### Why is this bad? /// Non-inlined code is slightly more difficult to read and understand, /// as it requires arguments to be matched against the format string. /// The inlined syntax, where allowed, is simpler. /// /// ### Example /// ```no_run /// # let var = 42; /// # let width = 1; /// # let prec = 2; /// format!("{}", var); /// format!("{v:?}", v = var); /// format!("{0} {0}", var); /// format!("{0:1$}", var, width); /// format!("{:.*}", prec, var); /// ``` /// Use instead: /// ```no_run /// # let var = 42; /// # let width = 1; /// # let prec = 2; /// format!("{var}"); /// format!("{var:?}"); /// format!("{var} {var}"); /// format!("{var:width$}"); /// format!("{var:.prec$}"); /// ``` /// /// If allow-mixed-uninlined-format-args is set to false in clippy.toml, /// the following code will also trigger the lint: /// ```no_run /// # let var = 42; /// format!("{} {}", var, 1+2); /// ``` /// Use instead: /// ```no_run /// # let var = 42; /// format!("{var} {}", 1+2); /// ``` /// /// ### Known Problems /// /// If a format string contains a numbered argument that cannot be inlined /// nothing will be suggested, e.g. `println!("{0}={1}", var, 1+2)`. #[clippy::version = "1.66.0"] pub UNINLINED_FORMAT_ARGS, pedantic, "using non-inlined variables in `format!` calls" } declare_clippy_lint! { /// ### What it does /// Detects [formatting parameters] that have no effect on the output of /// `format!()`, `println!()` or similar macros. /// /// ### Why is this bad? /// Shorter format specifiers are easier to read, it may also indicate that /// an expected formatting operation such as adding padding isn't happening. /// /// ### Example /// ```no_run /// println!("{:.}", 1.0); /// /// println!("not padded: {:5}", format_args!("...")); /// ``` /// Use instead: /// ```no_run /// println!("{}", 1.0); /// /// println!("not padded: {}", format_args!("...")); /// // OR /// println!("padded: {:5}", format!("...")); /// ``` /// /// [formatting parameters]: https://doc.rust-lang.org/std/fmt/index.html#formatting-parameters #[clippy::version = "1.66.0"] pub UNUSED_FORMAT_SPECS, complexity, "use of a format specifier that has no effect" } impl_lint_pass!(FormatArgs => [ FORMAT_IN_FORMAT_ARGS, TO_STRING_IN_FORMAT_ARGS, UNINLINED_FORMAT_ARGS, UNUSED_FORMAT_SPECS, ]); pub struct FormatArgs { msrv: Msrv, ignore_mixed: bool, } impl FormatArgs { #[must_use] pub fn new(msrv: Msrv, allow_mixed_uninlined_format_args: bool) -> Self { Self { msrv, ignore_mixed: allow_mixed_uninlined_format_args, } } } impl<'tcx> LateLintPass<'tcx> for FormatArgs { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { if let Some(macro_call) = root_macro_call_first_node(cx, expr) && is_format_macro(cx, macro_call.def_id) && let Some(format_args) = find_format_args(cx, expr, macro_call.expn) { let linter = FormatArgsExpr { cx, expr, macro_call: ¯o_call, format_args: &format_args, ignore_mixed: self.ignore_mixed, }; linter.check_templates(); if self.msrv.meets(msrvs::FORMAT_ARGS_CAPTURE) { linter.check_uninlined_args(); } } } extract_msrv_attr!(LateContext); } struct FormatArgsExpr<'a, 'tcx> { cx: &'a LateContext<'tcx>, expr: &'tcx Expr<'tcx>, macro_call: &'a MacroCall, format_args: &'a rustc_ast::FormatArgs, ignore_mixed: bool, } impl<'a, 'tcx> FormatArgsExpr<'a, 'tcx> { fn check_templates(&self) { for piece in &self.format_args.template { if let FormatArgsPiece::Placeholder(placeholder) = piece && let Ok(index) = placeholder.argument.index && let Some(arg) = self.format_args.arguments.all_args().get(index) { let arg_expr = find_format_arg_expr(self.expr, arg); self.check_unused_format_specifier(placeholder, arg_expr); if let Ok(arg_expr) = arg_expr && placeholder.format_trait == FormatTrait::Display && placeholder.format_options == FormatOptions::default() && !self.is_aliased(index) { let name = self.cx.tcx.item_name(self.macro_call.def_id); self.check_format_in_format_args(name, arg_expr); self.check_to_string_in_format_args(name, arg_expr); } } } } fn check_unused_format_specifier( &self, placeholder: &FormatPlaceholder, arg_expr: Result<&Expr<'_>, &rustc_ast::Expr>, ) { let ty_or_ast_expr = arg_expr.map(|expr| self.cx.typeck_results().expr_ty(expr).peel_refs()); let is_format_args = match ty_or_ast_expr { Ok(ty) => is_type_lang_item(self.cx, ty, LangItem::FormatArguments), Err(expr) => matches!(expr.peel_parens_and_refs().kind, rustc_ast::ExprKind::FormatArgs(_)), }; let options = &placeholder.format_options; let arg_span = match arg_expr { Ok(expr) => expr.span, Err(expr) => expr.span, }; if let Some(placeholder_span) = placeholder.span && is_format_args && *options != FormatOptions::default() { span_lint_and_then( self.cx, UNUSED_FORMAT_SPECS, placeholder_span, "format specifiers have no effect on `format_args!()`", |diag| { let mut suggest_format = |spec| { let message = format!("for the {spec} to apply consider using `format!()`"); if let Some(mac_call) = root_macro_call(arg_span) && self.cx.tcx.is_diagnostic_item(sym::format_args_macro, mac_call.def_id) { diag.span_suggestion( self.cx.sess().source_map().span_until_char(mac_call.span, '!'), message, "format", Applicability::MaybeIncorrect, ); } else { diag.help(message); } }; if options.width.is_some() { suggest_format("width"); } if options.precision.is_some() { suggest_format("precision"); } if let Some(format_span) = format_placeholder_format_span(placeholder) { diag.span_suggestion_verbose( format_span, "if the current behavior is intentional, remove the format specifiers", "", Applicability::MaybeIncorrect, ); } }, ); } } fn check_uninlined_args(&self) { if self.format_args.span.from_expansion() { return; } if self.macro_call.span.edition() < Edition2021 && (is_panic(self.cx, self.macro_call.def_id) || is_assert_macro(self.cx, self.macro_call.def_id)) { // panic!, assert!, and debug_assert! before 2021 edition considers a single string argument as // non-format return; } let mut fixes = Vec::new(); // If any of the arguments are referenced by an index number, // and that argument is not a simple variable and cannot be inlined, // we cannot remove any other arguments in the format string, // because the index numbers might be wrong after inlining. // Example of an un-inlinable format: print!("{}{1}", foo, 2) for (pos, usage) in self.format_arg_positions() { if !self.check_one_arg(pos, usage, &mut fixes) { return; } } if fixes.is_empty() { return; } // multiline span display suggestion is sometimes broken: https://github.com/rust-lang/rust/pull/102729#discussion_r988704308 // in those cases, make the code suggestion hidden let multiline_fix = fixes .iter() .any(|(span, _)| self.cx.sess().source_map().is_multiline(*span)); // Suggest removing each argument only once, for example in `format!("{0} {0}", arg)`. fixes.sort_unstable_by_key(|(span, _)| *span); fixes.dedup_by_key(|(span, _)| *span); span_lint_and_then( self.cx, UNINLINED_FORMAT_ARGS, self.macro_call.span, "variables can be used directly in the `format!` string", |diag| { diag.multipart_suggestion_with_style( "change this to", fixes, Applicability::MachineApplicable, if multiline_fix { CompletelyHidden } else { ShowCode }, ); }, ); } fn check_one_arg(&self, pos: &FormatArgPosition, usage: FormatParamUsage, fixes: &mut Vec<(Span, String)>) -> bool { let index = pos.index.unwrap(); let arg = &self.format_args.arguments.all_args()[index]; if !matches!(arg.kind, FormatArgumentKind::Captured(_)) && let rustc_ast::ExprKind::Path(None, path) = &arg.expr.kind && let [segment] = path.segments.as_slice() && segment.args.is_none() && let Some(arg_span) = format_arg_removal_span(self.format_args, index) && let Some(pos_span) = pos.span { let replacement = match usage { FormatParamUsage::Argument => segment.ident.name.to_string(), FormatParamUsage::Width => format!("{}$", segment.ident.name), FormatParamUsage::Precision => format!(".{}$", segment.ident.name), }; fixes.push((pos_span, replacement)); fixes.push((arg_span, String::new())); true // successful inlining, continue checking } else { // Do not continue inlining (return false) in case // * if we can't inline a numbered argument, e.g. `print!("{0} ...", foo.bar, ...)` // * if allow_mixed_uninlined_format_args is false and this arg hasn't been inlined already pos.kind != FormatArgPositionKind::Number && (!self.ignore_mixed || matches!(arg.kind, FormatArgumentKind::Captured(_))) } } fn check_format_in_format_args(&self, name: Symbol, arg: &Expr<'_>) { let expn_data = arg.span.ctxt().outer_expn_data(); if expn_data.call_site.from_expansion() { return; } let Some(mac_id) = expn_data.macro_def_id else { return }; if !self.cx.tcx.is_diagnostic_item(sym::format_macro, mac_id) { return; } span_lint_and_then( self.cx, FORMAT_IN_FORMAT_ARGS, self.macro_call.span, &format!("`format!` in `{name}!` args"), |diag| { diag.help(format!( "combine the `format!(..)` arguments with the outer `{name}!(..)` call" )); diag.help("or consider changing `format!` to `format_args!`"); }, ); } fn check_to_string_in_format_args(&self, name: Symbol, value: &Expr<'_>) { let cx = self.cx; if !value.span.from_expansion() && let ExprKind::MethodCall(_, receiver, [], to_string_span) = value.kind && let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(value.hir_id) && is_diag_trait_item(cx, method_def_id, sym::ToString) && let receiver_ty = cx.typeck_results().expr_ty(receiver) && let Some(display_trait_id) = cx.tcx.get_diagnostic_item(sym::Display) && let (n_needed_derefs, target) = count_needed_derefs(receiver_ty, cx.typeck_results().expr_adjustments(receiver).iter()) && implements_trait(cx, target, display_trait_id, &[]) && let Some(sized_trait_id) = cx.tcx.lang_items().sized_trait() && let Some(receiver_snippet) = snippet_opt(cx, receiver.span) { let needs_ref = !implements_trait(cx, receiver_ty, sized_trait_id, &[]); if n_needed_derefs == 0 && !needs_ref { span_lint_and_sugg( cx, TO_STRING_IN_FORMAT_ARGS, to_string_span.with_lo(receiver.span.hi()), &format!("`to_string` applied to a type that implements `Display` in `{name}!` args"), "remove this", String::new(), Applicability::MachineApplicable, ); } else { span_lint_and_sugg( cx, TO_STRING_IN_FORMAT_ARGS, value.span, &format!("`to_string` applied to a type that implements `Display` in `{name}!` args"), "use this", format!( "{}{:*>n_needed_derefs$}{receiver_snippet}", if needs_ref { "&" } else { "" }, "" ), Applicability::MachineApplicable, ); } } } fn format_arg_positions(&self) -> impl Iterator { self.format_args.template.iter().flat_map(|piece| match piece { FormatArgsPiece::Placeholder(placeholder) => { let mut positions = ArrayVec::<_, 3>::new(); positions.push((&placeholder.argument, FormatParamUsage::Argument)); if let Some(FormatCount::Argument(position)) = &placeholder.format_options.width { positions.push((position, FormatParamUsage::Width)); } if let Some(FormatCount::Argument(position)) = &placeholder.format_options.precision { positions.push((position, FormatParamUsage::Precision)); } positions }, FormatArgsPiece::Literal(_) => ArrayVec::new(), }) } /// Returns true if the format argument at `index` is referred to by multiple format params fn is_aliased(&self, index: usize) -> bool { self.format_arg_positions() .filter(|(position, _)| position.index == Ok(index)) .at_most_one() .is_err() } } fn count_needed_derefs<'tcx, I>(mut ty: Ty<'tcx>, mut iter: I) -> (usize, Ty<'tcx>) where I: Iterator>, { let mut n_total = 0; let mut n_needed = 0; loop { if let Some(Adjustment { kind: Adjust::Deref(overloaded_deref), target, }) = iter.next() { n_total += 1; if overloaded_deref.is_some() { n_needed = n_total; } ty = *target; } else { return (n_needed, ty); } } }