|
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}; |
| 3 | +use clippy_utils::source::{snippet_indent, snippet_with_applicability, snippet_with_context}; |
| 4 | +use clippy_utils::std_or_core; |
2 | 5 | use clippy_utils::ty::is_type_lang_item;
|
| 6 | +use rustc_data_structures::fx::FxHashSet; |
| 7 | +use rustc_errors::Applicability; |
| 8 | +use rustc_hir::def_id::{DefId, LocalModDefId}; |
3 | 9 | 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; |
| 10 | +use rustc_lint::{LateContext, LateLintPass, LintContext}; |
| 11 | +use rustc_middle::ty::TyCtxt; |
| 12 | +use rustc_session::impl_lint_pass; |
| 13 | +use rustc_span::{Span, sym}; |
7 | 14 |
|
8 | 15 | declare_clippy_lint! {
|
9 | 16 | /// ### What it does
|
@@ -37,52 +44,185 @@ declare_clippy_lint! {
|
37 | 44 | pedantic,
|
38 | 45 | "`format!(..)` appended to existing `String`"
|
39 | 46 | }
|
40 |
| -declare_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]); |
| 47 | +impl_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]); |
41 | 48 |
|
42 |
| -fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { |
43 |
| - is_type_lang_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), LangItem::String) |
| 49 | +pub(crate) struct FormatPushString { |
| 50 | + format_args: FormatArgsStorage, |
| 51 | + write_trait: Option<DefId>, |
| 52 | + mods_with_import_added: FxHashSet<LocalModDefId>, |
44 | 53 | }
|
45 |
| -fn is_format(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { |
46 |
| - let e = e.peel_blocks().peel_borrows(); |
47 | 54 |
|
48 |
| - match e.kind { |
49 |
| - _ if e.span.from_expansion() |
50 |
| - && let Some(macro_def_id) = e.span.ctxt().outer_expn_data().macro_def_id => |
51 |
| - { |
52 |
| - cx.tcx.is_diagnostic_item(sym::format_macro, macro_def_id) |
53 |
| - }, |
54 |
| - ExprKind::Match(_, arms, MatchSource::Normal) => arms.iter().any(|arm| is_format(cx, arm.body)), |
55 |
| - ExprKind::If(_, then, els) => is_format(cx, then) || els.is_some_and(|e| is_format(cx, e)), |
56 |
| - _ => false, |
| 55 | +enum FormatSearchResults { |
| 56 | + /// The expression is itself a `format!()` invocation -- we can make a suggestion to replace it |
| 57 | + Direct(Span), |
| 58 | + /// The expression contains zero or more `format!()`s, e.g.: |
| 59 | + /// ```ignore |
| 60 | + /// if true { |
| 61 | + /// format!("hello") |
| 62 | + /// } else { |
| 63 | + /// format!("world") |
| 64 | + /// } |
| 65 | + /// ``` |
| 66 | + /// or |
| 67 | + /// ```ignore |
| 68 | + /// match true { |
| 69 | + /// true => format!("hello"), |
| 70 | + /// false => format!("world"), |
| 71 | + /// } |
| 72 | + Nested(Vec<Span>), |
| 73 | +} |
| 74 | + |
| 75 | +impl FormatPushString { |
| 76 | + pub(crate) fn new(tcx: TyCtxt<'_>, format_args: FormatArgsStorage) -> Self { |
| 77 | + Self { |
| 78 | + format_args, |
| 79 | + write_trait: tcx.get_diagnostic_item(sym::FmtWrite), |
| 80 | + mods_with_import_added: FxHashSet::default(), |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + fn find_formats<'tcx>(&self, cx: &LateContext<'_>, e: &'tcx Expr<'tcx>) -> FormatSearchResults { |
| 85 | + let expr_as_format = |e| { |
| 86 | + if let Some(macro_call) = root_macro_call_first_node(cx, e) |
| 87 | + && cx.tcx.is_diagnostic_item(sym::format_macro, macro_call.def_id) |
| 88 | + && let Some(format_args) = self.format_args.get(cx, e, macro_call.expn) |
| 89 | + { |
| 90 | + Some(format_args_inputs_span(format_args)) |
| 91 | + } else { |
| 92 | + None |
| 93 | + } |
| 94 | + }; |
| 95 | + |
| 96 | + let e = e.peel_blocks().peel_borrows(); |
| 97 | + if let Some(fmt) = expr_as_format(e) { |
| 98 | + FormatSearchResults::Direct(fmt) |
| 99 | + } else { |
| 100 | + fn inner<'tcx>( |
| 101 | + e: &'tcx Expr<'tcx>, |
| 102 | + expr_as_format: &impl Fn(&'tcx Expr<'tcx>) -> Option<Span>, |
| 103 | + out: &mut Vec<Span>, |
| 104 | + ) { |
| 105 | + let e = e.peel_blocks().peel_borrows(); |
| 106 | + |
| 107 | + match e.kind { |
| 108 | + _ if expr_as_format(e).is_some() => out.push(e.span), |
| 109 | + ExprKind::Match(_, arms, MatchSource::Normal) => { |
| 110 | + for arm in arms { |
| 111 | + inner(arm.body, expr_as_format, out); |
| 112 | + } |
| 113 | + }, |
| 114 | + ExprKind::If(_, then, els) => { |
| 115 | + inner(then, expr_as_format, out); |
| 116 | + if let Some(els) = els { |
| 117 | + inner(els, expr_as_format, out); |
| 118 | + } |
| 119 | + }, |
| 120 | + _ => {}, |
| 121 | + } |
| 122 | + } |
| 123 | + let mut spans = vec![]; |
| 124 | + inner(e, &expr_as_format, &mut spans); |
| 125 | + FormatSearchResults::Nested(spans) |
| 126 | + } |
57 | 127 | }
|
58 | 128 | }
|
59 | 129 |
|
60 | 130 | impl<'tcx> LateLintPass<'tcx> for FormatPushString {
|
61 | 131 | fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
|
62 |
| - let arg = match expr.kind { |
63 |
| - ExprKind::MethodCall(_, _, [arg], _) => { |
| 132 | + let (recv, arg) = match expr.kind { |
| 133 | + ExprKind::MethodCall(_, recv, [arg], _) => { |
64 | 134 | if let Some(fn_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id)
|
65 | 135 | && cx.tcx.is_diagnostic_item(sym::string_push_str, fn_def_id)
|
66 | 136 | {
|
67 |
| - arg |
| 137 | + (recv, arg) |
68 | 138 | } else {
|
69 | 139 | return;
|
70 | 140 | }
|
71 | 141 | },
|
72 |
| - ExprKind::AssignOp(op, left, arg) if op.node == AssignOpKind::AddAssign && is_string(cx, left) => arg, |
| 142 | + ExprKind::AssignOp(op, recv, arg) if op.node == AssignOpKind::AddAssign && is_string(cx, recv) => { |
| 143 | + (recv, arg) |
| 144 | + }, |
73 | 145 | _ => return,
|
74 | 146 | };
|
75 |
| - if is_format(cx, arg) { |
76 |
| - #[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")] |
77 |
| - span_lint_and_then( |
78 |
| - cx, |
79 |
| - FORMAT_PUSH_STRING, |
80 |
| - expr.span, |
81 |
| - "`format!(..)` appended to existing `String`", |
82 |
| - |diag| { |
83 |
| - diag.help("consider using `write!` to avoid the extra allocation"); |
84 |
| - }, |
85 |
| - ); |
| 147 | + match self.find_formats(cx, arg) { |
| 148 | + FormatSearchResults::Direct(format_args) => { |
| 149 | + span_lint_and_then( |
| 150 | + cx, |
| 151 | + FORMAT_PUSH_STRING, |
| 152 | + expr.span, |
| 153 | + "`format!(..)` appended to existing `String`", |
| 154 | + |diag| { |
| 155 | + let mut app = Applicability::MachineApplicable; |
| 156 | + let msg = "consider using `write!` to avoid the extra allocation"; |
| 157 | + |
| 158 | + let main_sugg = ( |
| 159 | + expr.span, |
| 160 | + format!( |
| 161 | + "let _ = write!({recv}, {format_args})", |
| 162 | + recv = snippet_with_context(cx.sess(), recv.span, expr.span.ctxt(), "_", &mut app).0, |
| 163 | + format_args = snippet_with_applicability(cx.sess(), format_args, "..", &mut app), |
| 164 | + ), |
| 165 | + ); |
| 166 | + |
| 167 | + let is_write_in_scope = if let Some(did) = self.write_trait |
| 168 | + && let Some(trait_candidates) = cx.tcx.in_scope_traits(expr.hir_id) |
| 169 | + && trait_candidates |
| 170 | + .iter() |
| 171 | + .any(|trait_candidate| trait_candidate.def_id == did) |
| 172 | + { |
| 173 | + true |
| 174 | + } else { |
| 175 | + false |
| 176 | + }; |
| 177 | + |
| 178 | + if is_write_in_scope { |
| 179 | + diag.span_suggestion_verbose(main_sugg.0, msg, main_sugg.1, app); |
| 180 | + } else { |
| 181 | + let body_id = cx.tcx.hir_enclosing_body_owner(expr.hir_id); |
| 182 | + let scope = cx.tcx.parent_module_from_def_id(body_id); |
| 183 | + let (module, _, _) = cx.tcx.hir_get_module(scope); |
| 184 | + |
| 185 | + if self.mods_with_import_added.insert(scope) { |
| 186 | + // The trait is not in scope -- we'll need to import it |
| 187 | + let import_span = module.spans.inject_use_span; |
| 188 | + let import_sugg = ( |
| 189 | + import_span, |
| 190 | + format!( |
| 191 | + "use {std}::fmt::Write as _;\n{indent_of_imports}", |
| 192 | + std = std_or_core(cx).unwrap(), |
| 193 | + indent_of_imports = |
| 194 | + snippet_indent(cx.sess(), import_span).as_deref().unwrap_or(""), |
| 195 | + ), |
| 196 | + ); |
| 197 | + let suggs = vec![main_sugg, import_sugg]; |
| 198 | + |
| 199 | + diag.multipart_suggestion_verbose(msg, suggs, app); |
| 200 | + } else { |
| 201 | + // Another suggestion will have taken care of importing the trait |
| 202 | + diag.span_suggestion_verbose(main_sugg.0, msg, main_sugg.1, app); |
| 203 | + } |
| 204 | + } |
| 205 | + }, |
| 206 | + ); |
| 207 | + }, |
| 208 | + FormatSearchResults::Nested(spans) => { |
| 209 | + if !spans.is_empty() { |
| 210 | + span_lint_and_then( |
| 211 | + cx, |
| 212 | + FORMAT_PUSH_STRING, |
| 213 | + expr.span, |
| 214 | + "`format!(..)` appended to existing `String`", |
| 215 | + |diag| { |
| 216 | + diag.help("consider using `write!` to avoid the extra allocation"); |
| 217 | + diag.span_labels(spans, "`format!` used here"); |
| 218 | + }, |
| 219 | + ); |
| 220 | + } |
| 221 | + }, |
86 | 222 | }
|
87 | 223 | }
|
88 | 224 | }
|
| 225 | + |
| 226 | +fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool { |
| 227 | + is_type_lang_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), LangItem::String) |
| 228 | +} |
0 commit comments