use clippy_utils::diagnostics::span_lint_and_help; use clippy_utils::ty::is_type_lang_item; use clippy_utils::{higher, match_def_path, paths}; use rustc_hir::{BinOpKind, Expr, ExprKind, LangItem, MatchSource}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; use rustc_span::sym; declare_clippy_lint! { /// ### What it does /// Detects cases where the result of a `format!` call is /// appended to an existing `String`. /// /// ### Why is this bad? /// Introduces an extra, avoidable heap allocation. /// /// ### Known problems /// `format!` returns a `String` but `write!` returns a `Result`. /// Thus you are forced to ignore the `Err` variant to achieve the same API. /// /// While using `write!` in the suggested way should never fail, this isn't necessarily clear to the programmer. /// /// ### Example /// ```no_run /// let mut s = String::new(); /// s += &format!("0x{:X}", 1024); /// s.push_str(&format!("0x{:X}", 1024)); /// ``` /// Use instead: /// ```no_run /// use std::fmt::Write as _; // import without risk of name clashing /// /// let mut s = String::new(); /// let _ = write!(s, "0x{:X}", 1024); /// ``` #[clippy::version = "1.62.0"] pub FORMAT_PUSH_STRING, restriction, "`format!(..)` appended to existing `String`" } declare_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]); fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { is_type_lang_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), LangItem::String) } fn is_format(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { let e = e.peel_blocks().peel_borrows(); if e.span.from_expansion() && let Some(macro_def_id) = e.span.ctxt().outer_expn_data().macro_def_id { cx.tcx.get_diagnostic_name(macro_def_id) == Some(sym::format_macro) } else if let Some(higher::If { then, r#else, .. }) = higher::If::hir(e) { is_format(cx, then) || r#else.is_some_and(|e| is_format(cx, e)) } else { match higher::IfLetOrMatch::parse(cx, e) { Some(higher::IfLetOrMatch::Match(_, arms, MatchSource::Normal)) => { arms.iter().any(|arm| is_format(cx, arm.body)) }, Some(higher::IfLetOrMatch::IfLet(_, _, then, r#else, _)) => { is_format(cx, then) || r#else.is_some_and(|e| is_format(cx, e)) }, _ => false, } } } impl<'tcx> LateLintPass<'tcx> for FormatPushString { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { let arg = match expr.kind { ExprKind::MethodCall(_, _, [arg], _) => { if let Some(fn_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) && match_def_path(cx, fn_def_id, &paths::PUSH_STR) { arg } else { return; } }, ExprKind::AssignOp(op, left, arg) if op.node == BinOpKind::Add && is_string(cx, left) => arg, _ => return, }; if is_format(cx, arg) { span_lint_and_help( cx, FORMAT_PUSH_STRING, expr.span, "`format!(..)` appended to existing `String`", None, "consider using `write!` to avoid the extra allocation", ); } } }