-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathformat_push_string.rs
More file actions
197 lines (184 loc) · 7.48 KB
/
format_push_string.rs
File metadata and controls
197 lines (184 loc) · 7.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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)
}