Skip to content

Commit 893677f

Browse files
feat(format_push_string): give a (possibly incomplete) suggestion (#16093)
Resolves #8824 Supersedes #15828 -- I wasn't able to get `TyCtxt::in_scope_traits` to work, so here's a more basic implementation that just adds a note about the possible need to import `std::fmt::Write`. changelog: [`format_push_string`]: add a suggestion
2 parents d36794f + 34551be commit 893677f

13 files changed

+1030
-102
lines changed
Lines changed: 139 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::higher;
2+
use clippy_utils::macros::{FormatArgsStorage, format_args_inputs_span, root_macro_call_first_node};
33
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;
47
use rustc_hir::{AssignOpKind, Expr, ExprKind, LangItem, MatchSource};
5-
use rustc_lint::{LateContext, LateLintPass};
6-
use rustc_session::declare_lint_pass;
7-
use rustc_span::sym;
8+
use rustc_lint::{LateContext, LateLintPass, LintContext};
9+
use rustc_session::impl_lint_pass;
10+
use rustc_span::{Span, sym};
811

912
declare_clippy_lint! {
1013
/// ### What it does
@@ -38,62 +41,156 @@ declare_clippy_lint! {
3841
pedantic,
3942
"`format!(..)` appended to existing `String`"
4043
}
41-
declare_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]);
44+
impl_lint_pass!(FormatPushString => [FORMAT_PUSH_STRING]);
4245

43-
fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
44-
cx.typeck_results()
45-
.expr_ty(e)
46-
.peel_refs()
47-
.is_lang_item(cx, LangItem::String)
46+
pub(crate) struct FormatPushString {
47+
format_args: FormatArgsStorage,
4848
}
49-
fn is_format(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
50-
let e = e.peel_blocks().peel_borrows();
5149

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.get_diagnostic_name(macro_def_id) == Some(sym::format_macro)
56-
} else if let Some(higher::If { then, r#else, .. }) = higher::If::hir(e) {
57-
is_format(cx, then) || r#else.is_some_and(|e| is_format(cx, e))
58-
} else {
59-
match higher::IfLetOrMatch::parse(cx, e) {
60-
Some(higher::IfLetOrMatch::Match(_, arms, MatchSource::Normal)) => {
61-
arms.iter().any(|arm| is_format(cx, arm.body))
62-
},
63-
Some(higher::IfLetOrMatch::IfLet(_, _, then, r#else, _)) => {
64-
is_format(cx, then) || r#else.is_some_and(|e| is_format(cx, e))
65-
},
66-
_ => false,
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>),
68+
}
69+
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)
67117
}
68118
}
69119
}
70120

71121
impl<'tcx> LateLintPass<'tcx> for FormatPushString {
72122
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
73-
let arg = match expr.kind {
74-
ExprKind::MethodCall(_, _, [arg], _) => {
123+
let (recv, arg) = match expr.kind {
124+
ExprKind::MethodCall(_, recv, [arg], _) => {
75125
if let Some(fn_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id)
76126
&& cx.tcx.is_diagnostic_item(sym::string_push_str, fn_def_id)
77127
{
78-
arg
128+
(recv, arg)
79129
} else {
80130
return;
81131
}
82132
},
83-
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+
},
84136
_ => return,
85137
};
86-
if is_format(cx, arg) {
87-
#[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")]
88-
span_lint_and_then(
89-
cx,
90-
FORMAT_PUSH_STRING,
91-
expr.span,
92-
"`format!(..)` appended to existing `String`",
93-
|diag| {
94-
diag.help("consider using `write!` to avoid the extra allocation");
95-
},
96-
);
138+
let Some(std_or_core) = std_or_core(cx) else {
139+
// not even `core` is available, so can't suggest `write!`
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: omit the note if the `Write` trait is imported at point
161+
// Tip: `TyCtxt::in_scope_traits` isn't it -- it returns a non-empty list only when called on
162+
// the `HirId` of a `ExprKind::MethodCall` that is a call of a _trait_ method.
163+
diag.note(format!("you may need to import the `{std_or_core}::fmt::Write` trait"));
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: omit the note if the `Write` trait is imported at point
179+
// Tip: `TyCtxt::in_scope_traits` isn't it -- it returns a non-empty list only when called
180+
// on the `HirId` of a `ExprKind::MethodCall` that is a call of
181+
// a _trait_ method.
182+
diag.note(format!("you may need to import the `{std_or_core}::fmt::Write` trait"));
183+
},
184+
);
185+
}
186+
},
97187
}
98188
}
99189
}
190+
191+
fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
192+
cx.typeck_results()
193+
.expr_ty(e)
194+
.peel_refs()
195+
.is_lang_item(cx, LangItem::String)
196+
}

clippy_lints/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,10 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
739739
Box::new(move |_| Box::new(cargo::Cargo::new(conf))),
740740
Box::new(|_| Box::new(empty_with_brackets::EmptyWithBrackets::default())),
741741
Box::new(|_| Box::new(unnecessary_owned_empty_strings::UnnecessaryOwnedEmptyStrings)),
742-
Box::new(|_| Box::new(format_push_string::FormatPushString)),
742+
{
743+
let format_args = format_args_storage.clone();
744+
Box::new(move |_| Box::new(format_push_string::FormatPushString::new(format_args.clone())))
745+
},
743746
Box::new(move |_| Box::new(large_include_file::LargeIncludeFile::new(conf))),
744747
Box::new(|_| Box::new(strings::TrimSplitWhitespace)),
745748
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)