mirror of
https://github.com/rust-lang/rust.git
synced 2026-04-30 23:03:06 +03:00
197 lines
7.5 KiB
Rust
197 lines
7.5 KiB
Rust
use clippy_utils::diagnostics::span_lint_and_then;
|
|
use clippy_utils::macros::{FormatArgsStorage, format_args_inputs_span, root_macro_call_first_node};
|
|
use clippy_utils::res::MaybeDef;
|
|
use clippy_utils::source::{snippet_with_applicability, snippet_with_context};
|
|
use clippy_utils::{std_or_core, sym};
|
|
use rustc_errors::Applicability;
|
|
use rustc_hir::{AssignOpKind, Expr, ExprKind, LangItem, MatchSource};
|
|
use rustc_lint::{LateContext, LateLintPass, LintContext};
|
|
use rustc_session::impl_lint_pass;
|
|
use rustc_span::Span;
|
|
|
|
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,
|
|
pedantic,
|
|
"`format!(..)` appended to existing `String`"
|
|
}
|
|
impl_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]);
|
|
|
|
pub(crate) struct FormatPushString {
|
|
format_args: FormatArgsStorage,
|
|
}
|
|
|
|
enum FormatSearchResults {
|
|
/// The expression is itself a `format!()` invocation -- we can make a suggestion to replace it
|
|
Direct(Span),
|
|
/// The expression contains zero or more `format!()`s, e.g.:
|
|
/// ```ignore
|
|
/// if true {
|
|
/// format!("hello")
|
|
/// } else {
|
|
/// format!("world")
|
|
/// }
|
|
/// ```
|
|
/// or
|
|
/// ```ignore
|
|
/// match true {
|
|
/// true => format!("hello"),
|
|
/// false => format!("world"),
|
|
/// }
|
|
Nested(Vec<Span>),
|
|
}
|
|
|
|
impl FormatPushString {
|
|
pub(crate) fn new(format_args: FormatArgsStorage) -> Self {
|
|
Self { format_args }
|
|
}
|
|
|
|
fn find_formats<'tcx>(&self, cx: &LateContext<'_>, e: &'tcx Expr<'tcx>) -> FormatSearchResults {
|
|
let expr_as_format = |e| {
|
|
if let Some(macro_call) = root_macro_call_first_node(cx, e)
|
|
&& cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id)
|
|
&& let Some(format_args) = self.format_args.get(cx, e, macro_call.expn)
|
|
{
|
|
Some(format_args_inputs_span(format_args))
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
let e = e.peel_blocks().peel_borrows();
|
|
if let Some(fmt) = expr_as_format(e) {
|
|
FormatSearchResults::Direct(fmt)
|
|
} else {
|
|
fn inner<'tcx>(
|
|
e: &'tcx Expr<'tcx>,
|
|
expr_as_format: &impl Fn(&'tcx Expr<'tcx>) -> Option<Span>,
|
|
out: &mut Vec<Span>,
|
|
) {
|
|
let e = e.peel_blocks().peel_borrows();
|
|
|
|
match e.kind {
|
|
_ if expr_as_format(e).is_some() => out.push(e.span),
|
|
ExprKind::Match(_, arms, MatchSource::Normal) => {
|
|
for arm in arms {
|
|
inner(arm.body, expr_as_format, out);
|
|
}
|
|
},
|
|
ExprKind::If(_, then, els) => {
|
|
inner(then, expr_as_format, out);
|
|
if let Some(els) = els {
|
|
inner(els, expr_as_format, out);
|
|
}
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
let mut spans = vec![];
|
|
inner(e, &expr_as_format, &mut spans);
|
|
FormatSearchResults::Nested(spans)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'tcx> LateLintPass<'tcx> for FormatPushString {
|
|
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
|
|
let (recv, arg) = match expr.kind {
|
|
ExprKind::MethodCall(_, recv, [arg], _) => {
|
|
if let Some(fn_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id)
|
|
&& cx.tcx.is_diagnostic_item(sym::string_push_str, fn_def_id)
|
|
{
|
|
(recv, arg)
|
|
} else {
|
|
return;
|
|
}
|
|
},
|
|
ExprKind::AssignOp(op, recv, arg) if op.node == AssignOpKind::AddAssign && is_string(cx, recv) => {
|
|
(recv, arg)
|
|
},
|
|
_ => return,
|
|
};
|
|
let Some(std_or_core) = std_or_core(cx) else {
|
|
// not even `core` is available, so can't suggest `write!`
|
|
return;
|
|
};
|
|
match self.find_formats(cx, arg) {
|
|
FormatSearchResults::Direct(format_args) => {
|
|
span_lint_and_then(
|
|
cx,
|
|
FORMAT_PUSH_STRING,
|
|
expr.span,
|
|
"`format!(..)` appended to existing `String`",
|
|
|diag| {
|
|
let mut app = Applicability::MaybeIncorrect;
|
|
let msg = "consider using `write!` to avoid the extra allocation";
|
|
|
|
let sugg = format!(
|
|
"let _ = write!({recv}, {format_args})",
|
|
recv = snippet_with_context(cx.sess(), recv.span, expr.span.ctxt(), "_", &mut app).0,
|
|
format_args = snippet_with_applicability(cx.sess(), format_args, "..", &mut app),
|
|
);
|
|
diag.span_suggestion_verbose(expr.span, msg, sugg, app);
|
|
|
|
// TODO: omit the note if the `Write` trait is imported at point
|
|
// Tip: `TyCtxt::in_scope_traits` isn't it -- it returns a non-empty list only when called on
|
|
// the `HirId` of a `ExprKind::MethodCall` that is a call of a _trait_ method.
|
|
diag.note(format!("you may need to import the `{std_or_core}::fmt::Write` trait"));
|
|
},
|
|
);
|
|
},
|
|
FormatSearchResults::Nested(spans) => {
|
|
if !spans.is_empty() {
|
|
span_lint_and_then(
|
|
cx,
|
|
FORMAT_PUSH_STRING,
|
|
expr.span,
|
|
"`format!(..)` appended to existing `String`",
|
|
|diag| {
|
|
diag.help("consider using `write!` to avoid the extra allocation");
|
|
diag.span_labels(spans, "`format!` used here");
|
|
|
|
// TODO: omit the note if the `Write` trait is imported at point
|
|
// Tip: `TyCtxt::in_scope_traits` isn't it -- it returns a non-empty list only when called
|
|
// on the `HirId` of a `ExprKind::MethodCall` that is a call of
|
|
// a _trait_ method.
|
|
diag.note(format!("you may need to import the `{std_or_core}::fmt::Write` trait"));
|
|
},
|
|
);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
|
|
cx.typeck_results()
|
|
.expr_ty(e)
|
|
.peel_refs()
|
|
.is_lang_item(cx, LangItem::String)
|
|
}
|