Skip to content

Commit 410456d

Browse files
committed
Auto merge of rust-lang#11116 - y21:format_collect, r=Manishearth
new lint: `format_collect` A perf lint that looks for `format!`ing inside of `map`, then collecting it into a `String`. Did a quick benchmark locally and it's a bit more than 2x faster with fold. `write!` is still not optimal (presumably because the fmt stuff goes through dynamic dispatch), but it's still a lot better than creating a new string on every element. I thought about making a machine applicable suggestion, but there's a lot of suggestions that need to be made here, so I decided to just add help messages. changelog: new lint: `format_collect`
2 parents 54fa922 + c83d58f commit 410456d

File tree

7 files changed

+168
-3
lines changed

7 files changed

+168
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4865,6 +4865,7 @@ Released 2018-09-13
48654865
[`forget_copy`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_copy
48664866
[`forget_non_drop`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_non_drop
48674867
[`forget_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_ref
4868+
[`format_collect`]: https://rust-lang.github.io/rust-clippy/master/index.html#format_collect
48684869
[`format_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#format_in_format_args
48694870
[`format_push_string`]: https://rust-lang.github.io/rust-clippy/master/index.html#format_push_string
48704871
[`from_iter_instead_of_collect`]: https://rust-lang.github.io/rust-clippy/master/index.html#from_iter_instead_of_collect

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
338338
crate::methods::FILTER_NEXT_INFO,
339339
crate::methods::FLAT_MAP_IDENTITY_INFO,
340340
crate::methods::FLAT_MAP_OPTION_INFO,
341+
crate::methods::FORMAT_COLLECT_INFO,
341342
crate::methods::FROM_ITER_INSTEAD_OF_COLLECT_INFO,
342343
crate::methods::GET_FIRST_INFO,
343344
crate::methods::GET_LAST_WITH_LEN_INFO,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use super::FORMAT_COLLECT;
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::macros::{is_format_macro, root_macro_call_first_node};
4+
use clippy_utils::ty::is_type_lang_item;
5+
use rustc_hir::{Expr, ExprKind, LangItem};
6+
use rustc_lint::LateContext;
7+
use rustc_span::Span;
8+
9+
/// Same as `peel_blocks` but only actually considers blocks that are not from an expansion.
10+
/// This is needed because always calling `peel_blocks` would otherwise remove parts of the
11+
/// `format!` macro, which would cause `root_macro_call_first_node` to return `None`.
12+
fn peel_non_expn_blocks<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
13+
match expr.kind {
14+
ExprKind::Block(block, _) if !expr.span.from_expansion() => peel_non_expn_blocks(block.expr?),
15+
_ => Some(expr),
16+
}
17+
}
18+
19+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, map_arg: &Expr<'_>, map_span: Span) {
20+
if is_type_lang_item(cx, cx.typeck_results().expr_ty(expr), LangItem::String)
21+
&& let ExprKind::Closure(closure) = map_arg.kind
22+
&& let body = cx.tcx.hir().body(closure.body)
23+
&& let Some(value) = peel_non_expn_blocks(body.value)
24+
&& let Some(mac) = root_macro_call_first_node(cx, value)
25+
&& is_format_macro(cx, mac.def_id)
26+
{
27+
span_lint_and_then(cx, FORMAT_COLLECT, expr.span, "use of `format!` to build up a string from an iterator", |diag| {
28+
diag.span_help(map_span, "call `fold` instead")
29+
.span_help(value.span.source_callsite(), "... and use the `write!` macro here")
30+
.note("this can be written more efficiently by appending to a `String` directly");
31+
});
32+
}
33+
}

clippy_lints/src/methods/mod.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod filter_map_next;
2626
mod filter_next;
2727
mod flat_map_identity;
2828
mod flat_map_option;
29+
mod format_collect;
2930
mod from_iter_instead_of_collect;
3031
mod get_first;
3132
mod get_last_with_len;
@@ -3378,6 +3379,41 @@ declare_clippy_lint! {
33783379
"calling `Stdin::read_line`, then trying to parse it without first trimming"
33793380
}
33803381

3382+
declare_clippy_lint! {
3383+
/// ### What it does
3384+
/// Checks for usage of `.map(|_| format!(..)).collect::<String>()`.
3385+
///
3386+
/// ### Why is this bad?
3387+
/// This allocates a new string for every element in the iterator.
3388+
/// This can be done more efficiently by creating the `String` once and appending to it in `Iterator::fold`,
3389+
/// using either the `write!` macro which supports exactly the same syntax as the `format!` macro,
3390+
/// or concatenating with `+` in case the iterator yields `&str`/`String`.
3391+
///
3392+
/// Note also that `write!`-ing into a `String` can never fail, despite the return type of `write!` being `std::fmt::Result`,
3393+
/// so it can be safely ignored or unwrapped.
3394+
///
3395+
/// ### Example
3396+
/// ```rust
3397+
/// fn hex_encode(bytes: &[u8]) -> String {
3398+
/// bytes.iter().map(|b| format!("{b:02X}")).collect()
3399+
/// }
3400+
/// ```
3401+
/// Use instead:
3402+
/// ```rust
3403+
/// use std::fmt::Write;
3404+
/// fn hex_encode(bytes: &[u8]) -> String {
3405+
/// bytes.iter().fold(String::new(), |mut output, b| {
3406+
/// let _ = write!(output, "{b:02X}");
3407+
/// output
3408+
/// })
3409+
/// }
3410+
/// ```
3411+
#[clippy::version = "1.72.0"]
3412+
pub FORMAT_COLLECT,
3413+
perf,
3414+
"`format!`ing every element in a collection, then collecting the strings into a new `String`"
3415+
}
3416+
33813417
pub struct Methods {
33823418
avoid_breaking_exported_api: bool,
33833419
msrv: Msrv,
@@ -3512,6 +3548,7 @@ impl_lint_pass!(Methods => [
35123548
UNNECESSARY_LITERAL_UNWRAP,
35133549
DRAIN_COLLECT,
35143550
MANUAL_TRY_FOLD,
3551+
FORMAT_COLLECT,
35153552
]);
35163553

35173554
/// Extracts a method call name, args, and `Span` of the method name.
@@ -3733,8 +3770,9 @@ impl Methods {
37333770
Some((name @ ("cloned" | "copied"), recv2, [], _, _)) => {
37343771
iter_cloned_collect::check(cx, name, expr, recv2);
37353772
},
3736-
Some(("map", m_recv, [m_arg], _, _)) => {
3773+
Some(("map", m_recv, [m_arg], m_ident_span, _)) => {
37373774
map_collect_result_unit::check(cx, expr, m_recv, m_arg);
3775+
format_collect::check(cx, expr, m_arg, m_ident_span);
37383776
},
37393777
Some(("take", take_self_arg, [take_arg], _, _)) => {
37403778
if self.msrv.meets(msrvs::STR_REPEAT) {

src/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ impl ClippyCmd {
132132
let clippy_args: String = self
133133
.clippy_args
134134
.iter()
135-
.map(|arg| format!("{arg}__CLIPPY_HACKERY__"))
136-
.collect();
135+
.fold(String::new(), |s, arg| s + arg + "__CLIPPY_HACKERY__");
137136

138137
// Currently, `CLIPPY_TERMINAL_WIDTH` is used only to format "unknown field" error messages.
139138
let terminal_width = termize::dimensions().map_or(0, |(w, _)| w);

tests/ui/format_collect.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#![allow(unused, dead_code)]
2+
#![warn(clippy::format_collect)]
3+
4+
fn hex_encode(bytes: &[u8]) -> String {
5+
bytes.iter().map(|b| format!("{b:02X}")).collect()
6+
}
7+
8+
#[rustfmt::skip]
9+
fn hex_encode_deep(bytes: &[u8]) -> String {
10+
bytes.iter().map(|b| {{{{{ format!("{b:02X}") }}}}}).collect()
11+
}
12+
13+
macro_rules! fmt {
14+
($x:ident) => {
15+
format!("{x:02X}", x = $x)
16+
};
17+
}
18+
19+
fn from_macro(bytes: &[u8]) -> String {
20+
bytes.iter().map(|x| fmt!(x)).collect()
21+
}
22+
23+
fn with_block() -> String {
24+
(1..10)
25+
.map(|s| {
26+
let y = 1;
27+
format!("{s} {y}")
28+
})
29+
.collect()
30+
}
31+
fn main() {}

tests/ui/format_collect.stderr

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
error: use of `format!` to build up a string from an iterator
2+
--> $DIR/format_collect.rs:5:5
3+
|
4+
LL | bytes.iter().map(|b| format!("{b:02X}")).collect()
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
help: call `fold` instead
8+
--> $DIR/format_collect.rs:5:18
9+
|
10+
LL | bytes.iter().map(|b| format!("{b:02X}")).collect()
11+
| ^^^
12+
help: ... and use the `write!` macro here
13+
--> $DIR/format_collect.rs:5:26
14+
|
15+
LL | bytes.iter().map(|b| format!("{b:02X}")).collect()
16+
| ^^^^^^^^^^^^^^^^^^
17+
= note: this can be written more efficiently by appending to a `String` directly
18+
= note: `-D clippy::format-collect` implied by `-D warnings`
19+
20+
error: use of `format!` to build up a string from an iterator
21+
--> $DIR/format_collect.rs:10:5
22+
|
23+
LL | bytes.iter().map(|b| {{{{{ format!("{b:02X}") }}}}}).collect()
24+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
25+
|
26+
help: call `fold` instead
27+
--> $DIR/format_collect.rs:10:18
28+
|
29+
LL | bytes.iter().map(|b| {{{{{ format!("{b:02X}") }}}}}).collect()
30+
| ^^^
31+
help: ... and use the `write!` macro here
32+
--> $DIR/format_collect.rs:10:32
33+
|
34+
LL | bytes.iter().map(|b| {{{{{ format!("{b:02X}") }}}}}).collect()
35+
| ^^^^^^^^^^^^^^^^^^
36+
= note: this can be written more efficiently by appending to a `String` directly
37+
38+
error: use of `format!` to build up a string from an iterator
39+
--> $DIR/format_collect.rs:24:5
40+
|
41+
LL | / (1..10)
42+
LL | | .map(|s| {
43+
LL | | let y = 1;
44+
LL | | format!("{s} {y}")
45+
LL | | })
46+
LL | | .collect()
47+
| |__________________^
48+
|
49+
help: call `fold` instead
50+
--> $DIR/format_collect.rs:25:10
51+
|
52+
LL | .map(|s| {
53+
| ^^^
54+
help: ... and use the `write!` macro here
55+
--> $DIR/format_collect.rs:27:13
56+
|
57+
LL | format!("{s} {y}")
58+
| ^^^^^^^^^^^^^^^^^^
59+
= note: this can be written more efficiently by appending to a `String` directly
60+
61+
error: aborting due to 3 previous errors
62+

0 commit comments

Comments
 (0)