Skip to content

Commit 5ea08dd

Browse files
committed
WIP: feat(format_push_string): add a suggestion
1 parent f095fd7 commit 5ea08dd

12 files changed

+1015
-95
lines changed
Lines changed: 139 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::macros::{FormatArgsStorage, format_args_inputs_span, root_macro_call_first_node};
23
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;
37
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};
711

812
declare_clippy_lint! {
913
/// ### What it does
@@ -37,55 +41,155 @@ declare_clippy_lint! {
3741
pedantic,
3842
"`format!(..)` appended to existing `String`"
3943
}
40-
declare_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]);
44+
impl_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]);
4145

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>),
4768
}
48-
fn is_format(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
49-
let e = e.peel_blocks().peel_borrows();
5069

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+
}
60118
}
61119
}
62120

63121
impl<'tcx> LateLintPass<'tcx> for FormatPushString {
64122
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], _) => {
67125
if let Some(fn_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id)
68126
&& cx.tcx.is_diagnostic_item(sym::string_push_str, fn_def_id)
69127
{
70-
arg
128+
(recv, arg)
71129
} else {
72130
return;
73131
}
74132
},
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+
},
76136
_ => return,
77137
};
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+
},
89186
}
90187
}
91188
}
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+
}

clippy_lints/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,10 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
736736
Box::new(move |_| Box::new(cargo::Cargo::new(conf))),
737737
Box::new(|_| Box::new(empty_with_brackets::EmptyWithBrackets::default())),
738738
Box::new(|_| Box::new(unnecessary_owned_empty_strings::UnnecessaryOwnedEmptyStrings)),
739-
Box::new(|_| Box::new(format_push_string::FormatPushString)),
739+
{
740+
let format_args = format_args_storage.clone();
741+
Box::new(move |_| Box::new(format_push_string::FormatPushString::new(format_args.clone())))
742+
},
740743
Box::new(move |_| Box::new(large_include_file::LargeIncludeFile::new(conf))),
741744
Box::new(|_| Box::new(strings::TrimSplitWhitespace)),
742745
Box::new(|_| Box::new(rc_clone_in_vec_init::RcCloneInVecInit)),

tests/ui/format_push_string.fixed

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#![warn(clippy::format_push_string)]
2+
3+
fn main() {
4+
use std::fmt::Write;
5+
6+
let mut string = String::new();
7+
let _ = write!(string, "{:?}", 1234);
8+
//~^ format_push_string
9+
10+
let _ = write!(string, "{:?}", 5678);
11+
//~^ format_push_string
12+
13+
macro_rules! string {
14+
() => {
15+
String::new()
16+
};
17+
}
18+
let _ = write!(string!(), "{:?}", 5678);
19+
//~^ format_push_string
20+
}
21+
22+
// TODO: recognize the already imported `fmt::Write`, and don't add a note suggesting to import it
23+
// again
24+
mod import_write {
25+
mod push_str {
26+
mod imported_anonymously {
27+
fn main(string: &mut String) {
28+
use std::fmt::Write as _;
29+
30+
let _ = write!(string, "{:?}", 1234);
31+
//~^ format_push_string
32+
}
33+
}
34+
35+
mod imported {
36+
fn main(string: &mut String) {
37+
use std::fmt::Write;
38+
39+
let _ = write!(string, "{:?}", 1234);
40+
//~^ format_push_string
41+
}
42+
}
43+
44+
mod imported_anonymously_in_module {
45+
use std::fmt::Write as _;
46+
47+
fn main(string: &mut String) {
48+
let _ = write!(string, "{:?}", 1234);
49+
//~^ format_push_string
50+
}
51+
}
52+
53+
mod imported_in_module {
54+
use std::fmt::Write;
55+
56+
fn main(string: &mut String) {
57+
let _ = write!(string, "{:?}", 1234);
58+
//~^ format_push_string
59+
}
60+
}
61+
62+
mod imported_and_imported {
63+
fn foo(string: &mut String) {
64+
use std::fmt::Write;
65+
66+
let _ = write!(string, "{:?}", 1234);
67+
//~^ format_push_string
68+
}
69+
70+
fn bar(string: &mut String) {
71+
use std::fmt::Write;
72+
73+
let _ = write!(string, "{:?}", 1234);
74+
//~^ format_push_string
75+
}
76+
}
77+
}
78+
79+
mod add_assign {
80+
mod imported_anonymously {
81+
fn main(string: &mut String) {
82+
use std::fmt::Write as _;
83+
84+
let _ = write!(string, "{:?}", 1234);
85+
//~^ format_push_string
86+
}
87+
}
88+
89+
mod imported {
90+
fn main(string: &mut String) {
91+
use std::fmt::Write;
92+
93+
let _ = write!(string, "{:?}", 1234);
94+
//~^ format_push_string
95+
}
96+
}
97+
98+
mod imported_anonymously_in_module {
99+
use std::fmt::Write as _;
100+
101+
fn main(string: &mut String) {
102+
let _ = write!(string, "{:?}", 1234);
103+
//~^ format_push_string
104+
}
105+
}
106+
107+
mod imported_in_module {
108+
use std::fmt::Write;
109+
110+
fn main(string: &mut String) {
111+
let _ = write!(string, "{:?}", 1234);
112+
//~^ format_push_string
113+
}
114+
}
115+
116+
mod imported_and_imported {
117+
fn foo(string: &mut String) {
118+
use std::fmt::Write;
119+
120+
let _ = write!(string, "{:?}", 1234);
121+
//~^ format_push_string
122+
}
123+
124+
fn bar(string: &mut String) {
125+
use std::fmt::Write;
126+
127+
let _ = write!(string, "{:?}", 1234);
128+
//~^ format_push_string
129+
}
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)