|
1 | 1 | use clippy_utils::diagnostics::span_lint_and_then; |
| 2 | +use clippy_utils::macros::{FormatArgsStorage, format_args_inputs_span, root_macro_call_first_node}; |
2 | 3 | use clippy_utils::res::MaybeDef; |
| 4 | +use clippy_utils::source::{snippet_with_applicability, snippet_with_context}; |
| 5 | +use clippy_utils::std_or_core; |
| 6 | +use rustc_errors::Applicability; |
3 | 7 | use rustc_hir::{AssignOpKind, Expr, ExprKind, LangItem, MatchSource}; |
4 | | -use rustc_lint::{LateContext, LateLintPass}; |
5 | | -use rustc_session::declare_lint_pass; |
6 | | -use rustc_span::sym; |
| 8 | +use rustc_lint::{LateContext, LateLintPass, LintContext}; |
| 9 | +use rustc_session::impl_lint_pass; |
| 10 | +use rustc_span::{Span, sym}; |
7 | 11 |
|
8 | 12 | declare_clippy_lint! { |
9 | 13 | /// ### What it does |
@@ -37,55 +41,155 @@ declare_clippy_lint! { |
37 | 41 | pedantic, |
38 | 42 | "`format!(..)` appended to existing `String`" |
39 | 43 | } |
40 | | -declare_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]); |
| 44 | +impl_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]); |
41 | 45 |
|
42 | | -fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { |
43 | | - cx.typeck_results() |
44 | | - .expr_ty(e) |
45 | | - .peel_refs() |
46 | | - .is_lang_item(cx, LangItem::String) |
| 46 | +pub(crate) struct FormatPushString { |
| 47 | + format_args: FormatArgsStorage, |
| 48 | +} |
| 49 | + |
| 50 | +enum FormatSearchResults { |
| 51 | + /// The expression is itself a `format!()` invocation -- we can make a suggestion to replace it |
| 52 | + Direct(Span), |
| 53 | + /// The expression contains zero or more `format!()`s, e.g.: |
| 54 | + /// ```ignore |
| 55 | + /// if true { |
| 56 | + /// format!("hello") |
| 57 | + /// } else { |
| 58 | + /// format!("world") |
| 59 | + /// } |
| 60 | + /// ``` |
| 61 | + /// or |
| 62 | + /// ```ignore |
| 63 | + /// match true { |
| 64 | + /// true => format!("hello"), |
| 65 | + /// false => format!("world"), |
| 66 | + /// } |
| 67 | + Nested(Vec<Span>), |
47 | 68 | } |
48 | | -fn is_format(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { |
49 | | - let e = e.peel_blocks().peel_borrows(); |
50 | 69 |
|
51 | | - match e.kind { |
52 | | - _ if e.span.from_expansion() |
53 | | - && let Some(macro_def_id) = e.span.ctxt().outer_expn_data().macro_def_id => |
54 | | - { |
55 | | - cx.tcx.is_diagnostic_item(sym::format_macro, macro_def_id) |
56 | | - }, |
57 | | - ExprKind::Match(_, arms, MatchSource::Normal) => arms.iter().any(|arm| is_format(cx, arm.body)), |
58 | | - ExprKind::If(_, then, els) => is_format(cx, then) || els.is_some_and(|e| is_format(cx, e)), |
59 | | - _ => false, |
| 70 | +impl FormatPushString { |
| 71 | + pub(crate) fn new(format_args: FormatArgsStorage) -> Self { |
| 72 | + Self { format_args } |
| 73 | + } |
| 74 | + |
| 75 | + fn find_formats<'tcx>(&self, cx: &LateContext<'_>, e: &'tcx Expr<'tcx>) -> FormatSearchResults { |
| 76 | + let expr_as_format = |e| { |
| 77 | + if let Some(macro_call) = root_macro_call_first_node(cx, e) |
| 78 | + && cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id) |
| 79 | + && let Some(format_args) = self.format_args.get(cx, e, macro_call.expn) |
| 80 | + { |
| 81 | + Some(format_args_inputs_span(format_args)) |
| 82 | + } else { |
| 83 | + None |
| 84 | + } |
| 85 | + }; |
| 86 | + |
| 87 | + let e = e.peel_blocks().peel_borrows(); |
| 88 | + if let Some(fmt) = expr_as_format(e) { |
| 89 | + FormatSearchResults::Direct(fmt) |
| 90 | + } else { |
| 91 | + fn inner<'tcx>( |
| 92 | + e: &'tcx Expr<'tcx>, |
| 93 | + expr_as_format: &impl Fn(&'tcx Expr<'tcx>) -> Option<Span>, |
| 94 | + out: &mut Vec<Span>, |
| 95 | + ) { |
| 96 | + let e = e.peel_blocks().peel_borrows(); |
| 97 | + |
| 98 | + match e.kind { |
| 99 | + _ if expr_as_format(e).is_some() => out.push(e.span), |
| 100 | + ExprKind::Match(_, arms, MatchSource::Normal) => { |
| 101 | + for arm in arms { |
| 102 | + inner(arm.body, expr_as_format, out); |
| 103 | + } |
| 104 | + }, |
| 105 | + ExprKind::If(_, then, els) => { |
| 106 | + inner(then, expr_as_format, out); |
| 107 | + if let Some(els) = els { |
| 108 | + inner(els, expr_as_format, out); |
| 109 | + } |
| 110 | + }, |
| 111 | + _ => {}, |
| 112 | + } |
| 113 | + } |
| 114 | + let mut spans = vec![]; |
| 115 | + inner(e, &expr_as_format, &mut spans); |
| 116 | + FormatSearchResults::Nested(spans) |
| 117 | + } |
60 | 118 | } |
61 | 119 | } |
62 | 120 |
|
63 | 121 | impl<'tcx> LateLintPass<'tcx> for FormatPushString { |
64 | 122 | fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { |
65 | | - let arg = match expr.kind { |
66 | | - ExprKind::MethodCall(_, _, [arg], _) => { |
| 123 | + let (recv, arg) = match expr.kind { |
| 124 | + ExprKind::MethodCall(_, recv, [arg], _) => { |
67 | 125 | if let Some(fn_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) |
68 | 126 | && cx.tcx.is_diagnostic_item(sym::string_push_str, fn_def_id) |
69 | 127 | { |
70 | | - arg |
| 128 | + (recv, arg) |
71 | 129 | } else { |
72 | 130 | return; |
73 | 131 | } |
74 | 132 | }, |
75 | | - ExprKind::AssignOp(op, left, arg) if op.node == AssignOpKind::AddAssign && is_string(cx, left) => arg, |
| 133 | + ExprKind::AssignOp(op, recv, arg) if op.node == AssignOpKind::AddAssign && is_string(cx, recv) => { |
| 134 | + (recv, arg) |
| 135 | + }, |
76 | 136 | _ => return, |
77 | 137 | }; |
78 | | - if is_format(cx, arg) { |
79 | | - #[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")] |
80 | | - span_lint_and_then( |
81 | | - cx, |
82 | | - FORMAT_PUSH_STRING, |
83 | | - expr.span, |
84 | | - "`format!(..)` appended to existing `String`", |
85 | | - |diag| { |
86 | | - diag.help("consider using `write!` to avoid the extra allocation"); |
87 | | - }, |
88 | | - ); |
| 138 | + let Some(std_or_core) = std_or_core(cx) else { |
| 139 | + // This can't really happen, as a no-core crate wouldn't have access to `String` in the first place |
| 140 | + return; |
| 141 | + }; |
| 142 | + match self.find_formats(cx, arg) { |
| 143 | + FormatSearchResults::Direct(format_args) => { |
| 144 | + span_lint_and_then( |
| 145 | + cx, |
| 146 | + FORMAT_PUSH_STRING, |
| 147 | + expr.span, |
| 148 | + "`format!(..)` appended to existing `String`", |
| 149 | + |diag| { |
| 150 | + let mut app = Applicability::MaybeIncorrect; |
| 151 | + let msg = "consider using `write!` to avoid the extra allocation"; |
| 152 | + |
| 153 | + let sugg = format!( |
| 154 | + "let _ = write!({recv}, {format_args})", |
| 155 | + recv = snippet_with_context(cx.sess(), recv.span, expr.span.ctxt(), "_", &mut app).0, |
| 156 | + format_args = snippet_with_applicability(cx.sess(), format_args, "..", &mut app), |
| 157 | + ); |
| 158 | + diag.span_suggestion_verbose(expr.span, msg, sugg, app); |
| 159 | + |
| 160 | + // TODO: Ideally we'd use `TyCtxt::in_scope_traits` to detect whether the trait is imported, and |
| 161 | + // either not emit this note if it is, or emit an automated suggestion to import it if it isn't. |
| 162 | + // But the method doesn't seem to work, see https://rust-lang.zulipchat.com/#narrow/channel/257328-clippy/topic/How+to+suggest+importing+a+path/with/544260181 |
| 163 | + diag.note(format!("you may need to import `{std_or_core}::fmt::Write`")); |
| 164 | + }, |
| 165 | + ); |
| 166 | + }, |
| 167 | + FormatSearchResults::Nested(spans) => { |
| 168 | + if !spans.is_empty() { |
| 169 | + span_lint_and_then( |
| 170 | + cx, |
| 171 | + FORMAT_PUSH_STRING, |
| 172 | + expr.span, |
| 173 | + "`format!(..)` appended to existing `String`", |
| 174 | + |diag| { |
| 175 | + diag.help("consider using `write!` to avoid the extra allocation"); |
| 176 | + diag.span_labels(spans, "`format!` used here"); |
| 177 | + |
| 178 | + // TODO: Ideally we'd use `TyCtxt::in_scope_traits` to detect whether the trait is imported, |
| 179 | + // and either not emit this note if it is, or emit an automated suggestion to import it if |
| 180 | + // it isn't. But the method doesn't seem to work, see https://rust-lang.zulipchat.com/#narrow/channel/257328-clippy/topic/How+to+suggest+importing+a+path/with/544260181 |
| 181 | + diag.note(format!("you may need to import `{std_or_core}::fmt::Write`")); |
| 182 | + }, |
| 183 | + ); |
| 184 | + } |
| 185 | + }, |
89 | 186 | } |
90 | 187 | } |
91 | 188 | } |
| 189 | + |
| 190 | +fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { |
| 191 | + cx.typeck_results() |
| 192 | + .expr_ty(e) |
| 193 | + .peel_refs() |
| 194 | + .is_lang_item(cx, LangItem::String) |
| 195 | +} |
0 commit comments