Skip to content

Commit 5820add

Browse files
committed
Auto merge of rust-lang#9269 - nahuakang:collapsible_str_replace, r=flip1995
Lint `collapsible_str_replace` fixes rust-lang#6651 ``` changelog: [`collapsible_str_replace`]: create new lint `collapsible_str_replace` ``` If you added a new lint, here's a checklist for things that will be checked during review or continuous integration. - \[x] Followed [lint naming conventions][lint_naming] - \[x] Added passing UI tests (including committed `.stderr` file) - \[x] `cargo test` passes locally - \[ ] Executed `cargo dev update_lints` - \[x] Added lint documentation - \[x] Run `cargo dev fmt`
2 parents 0dfec01 + b070b40 commit 5820add

9 files changed

+371
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3642,6 +3642,7 @@ Released 2018-09-13
36423642
[`collapsible_else_if`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_else_if
36433643
[`collapsible_if`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
36443644
[`collapsible_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_match
3645+
[`collapsible_str_replace`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_str_replace
36453646
[`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
36463647
[`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
36473648
[`const_static_lifetime`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_static_lifetime

clippy_lints/src/lib.register_all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
154154
LintId::of(methods::CHARS_NEXT_CMP),
155155
LintId::of(methods::CLONE_DOUBLE_REF),
156156
LintId::of(methods::CLONE_ON_COPY),
157+
LintId::of(methods::COLLAPSIBLE_STR_REPLACE),
157158
LintId::of(methods::ERR_EXPECT),
158159
LintId::of(methods::EXPECT_FUN_CALL),
159160
LintId::of(methods::EXTEND_WITH_DRAIN),

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ store.register_lints(&[
287287
methods::CLONE_DOUBLE_REF,
288288
methods::CLONE_ON_COPY,
289289
methods::CLONE_ON_REF_PTR,
290+
methods::COLLAPSIBLE_STR_REPLACE,
290291
methods::ERR_EXPECT,
291292
methods::EXPECT_FUN_CALL,
292293
methods::EXPECT_USED,

clippy_lints/src/lib.register_perf.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ store.register_group(true, "clippy::perf", Some("clippy_perf"), vec![
1313
LintId::of(loops::MISSING_SPIN_LOOP),
1414
LintId::of(loops::NEEDLESS_COLLECT),
1515
LintId::of(manual_retain::MANUAL_RETAIN),
16+
LintId::of(methods::COLLAPSIBLE_STR_REPLACE),
1617
LintId::of(methods::EXPECT_FUN_CALL),
1718
LintId::of(methods::EXTEND_WITH_DRAIN),
1819
LintId::of(methods::ITER_NTH),
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::source::snippet;
3+
use clippy_utils::visitors::for_each_expr;
4+
use clippy_utils::{eq_expr_value, get_parent_expr};
5+
use core::ops::ControlFlow;
6+
use rustc_errors::Applicability;
7+
use rustc_hir as hir;
8+
use rustc_lint::LateContext;
9+
use std::collections::VecDeque;
10+
11+
use super::method_call;
12+
use super::COLLAPSIBLE_STR_REPLACE;
13+
14+
pub(super) fn check<'tcx>(
15+
cx: &LateContext<'tcx>,
16+
expr: &'tcx hir::Expr<'tcx>,
17+
from: &'tcx hir::Expr<'tcx>,
18+
to: &'tcx hir::Expr<'tcx>,
19+
) {
20+
let replace_methods = collect_replace_calls(cx, expr, to);
21+
if replace_methods.methods.len() > 1 {
22+
let from_kind = cx.typeck_results().expr_ty(from).peel_refs().kind();
23+
// If the parent node's `to` argument is the same as the `to` argument
24+
// of the last replace call in the current chain, don't lint as it was already linted
25+
if let Some(parent) = get_parent_expr(cx, expr)
26+
&& let Some(("replace", [_, current_from, current_to], _)) = method_call(parent)
27+
&& eq_expr_value(cx, to, current_to)
28+
&& from_kind == cx.typeck_results().expr_ty(current_from).peel_refs().kind()
29+
{
30+
return;
31+
}
32+
33+
check_consecutive_replace_calls(cx, expr, &replace_methods, to);
34+
}
35+
}
36+
37+
struct ReplaceMethods<'tcx> {
38+
methods: VecDeque<&'tcx hir::Expr<'tcx>>,
39+
from_args: VecDeque<&'tcx hir::Expr<'tcx>>,
40+
}
41+
42+
fn collect_replace_calls<'tcx>(
43+
cx: &LateContext<'tcx>,
44+
expr: &'tcx hir::Expr<'tcx>,
45+
to_arg: &'tcx hir::Expr<'tcx>,
46+
) -> ReplaceMethods<'tcx> {
47+
let mut methods = VecDeque::new();
48+
let mut from_args = VecDeque::new();
49+
50+
let _: Option<()> = for_each_expr(expr, |e| {
51+
if let Some(("replace", [_, from, to], _)) = method_call(e) {
52+
if eq_expr_value(cx, to_arg, to) && cx.typeck_results().expr_ty(from).peel_refs().is_char() {
53+
methods.push_front(e);
54+
from_args.push_front(from);
55+
ControlFlow::Continue(())
56+
} else {
57+
ControlFlow::BREAK
58+
}
59+
} else {
60+
ControlFlow::Continue(())
61+
}
62+
});
63+
64+
ReplaceMethods { methods, from_args }
65+
}
66+
67+
/// Check a chain of `str::replace` calls for `collapsible_str_replace` lint.
68+
fn check_consecutive_replace_calls<'tcx>(
69+
cx: &LateContext<'tcx>,
70+
expr: &'tcx hir::Expr<'tcx>,
71+
replace_methods: &ReplaceMethods<'tcx>,
72+
to_arg: &'tcx hir::Expr<'tcx>,
73+
) {
74+
let from_args = &replace_methods.from_args;
75+
let from_arg_reprs: Vec<String> = from_args
76+
.iter()
77+
.map(|from_arg| snippet(cx, from_arg.span, "..").to_string())
78+
.collect();
79+
let app = Applicability::MachineApplicable;
80+
let earliest_replace_call = replace_methods.methods.front().unwrap();
81+
if let Some((_, [..], span_lo)) = method_call(earliest_replace_call) {
82+
span_lint_and_sugg(
83+
cx,
84+
COLLAPSIBLE_STR_REPLACE,
85+
expr.span.with_lo(span_lo.lo()),
86+
"used consecutive `str::replace` call",
87+
"replace with",
88+
format!(
89+
"replace([{}], {})",
90+
from_arg_reprs.join(", "),
91+
snippet(cx, to_arg.span, ".."),
92+
),
93+
app,
94+
);
95+
}
96+
}

clippy_lints/src/methods/mod.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod chars_next_cmp_with_unwrap;
1212
mod clone_on_copy;
1313
mod clone_on_ref_ptr;
1414
mod cloned_instead_of_copied;
15+
mod collapsible_str_replace;
1516
mod err_expect;
1617
mod expect_fun_call;
1718
mod expect_used;
@@ -137,6 +138,32 @@ declare_clippy_lint! {
137138
"used `cloned` where `copied` could be used instead"
138139
}
139140

141+
declare_clippy_lint! {
142+
/// ### What it does
143+
/// Checks for consecutive calls to `str::replace` (2 or more)
144+
/// that can be collapsed into a single call.
145+
///
146+
/// ### Why is this bad?
147+
/// Consecutive `str::replace` calls scan the string multiple times
148+
/// with repetitive code.
149+
///
150+
/// ### Example
151+
/// ```rust
152+
/// let hello = "hesuo worpd"
153+
/// .replace('s', "l")
154+
/// .replace("u", "l")
155+
/// .replace('p', "l");
156+
/// ```
157+
/// Use instead:
158+
/// ```rust
159+
/// let hello = "hesuo worpd".replace(&['s', 'u', 'p'], "l");
160+
/// ```
161+
#[clippy::version = "1.64.0"]
162+
pub COLLAPSIBLE_STR_REPLACE,
163+
perf,
164+
"collapse consecutive calls to str::replace (2 or more) into a single call"
165+
}
166+
140167
declare_clippy_lint! {
141168
/// ### What it does
142169
/// Checks for usage of `_.cloned().<func>()` where call to `.cloned()` can be postponed.
@@ -3001,6 +3028,7 @@ impl_lint_pass!(Methods => [
30013028
CLONE_ON_COPY,
30023029
CLONE_ON_REF_PTR,
30033030
CLONE_DOUBLE_REF,
3031+
COLLAPSIBLE_STR_REPLACE,
30043032
ITER_OVEREAGER_CLONED,
30053033
CLONED_INSTEAD_OF_COPIED,
30063034
FLAT_MAP_OPTION,
@@ -3479,6 +3507,14 @@ impl Methods {
34793507
("repeat", [arg]) => {
34803508
repeat_once::check(cx, expr, recv, arg);
34813509
},
3510+
(name @ ("replace" | "replacen"), [arg1, arg2] | [arg1, arg2, _]) => {
3511+
no_effect_replace::check(cx, expr, arg1, arg2);
3512+
3513+
// Check for repeated `str::replace` calls to perform `collapsible_str_replace` lint
3514+
if name == "replace" && let Some(("replace", ..)) = method_call(recv) {
3515+
collapsible_str_replace::check(cx, expr, arg1, arg2);
3516+
}
3517+
},
34823518
("resize", [count_arg, default_arg]) => {
34833519
vec_resize_to_zero::check(cx, expr, count_arg, default_arg, span);
34843520
},
@@ -3556,9 +3592,6 @@ impl Methods {
35563592
unnecessary_lazy_eval::check(cx, expr, recv, u_arg, "unwrap_or");
35573593
},
35583594
},
3559-
("replace" | "replacen", [arg1, arg2] | [arg1, arg2, _]) => {
3560-
no_effect_replace::check(cx, expr, arg1, arg2);
3561-
},
35623595
("zip", [arg]) => {
35633596
if let ExprKind::MethodCall(name, [iter_recv], _) = recv.kind
35643597
&& name.ident.name == sym::iter
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// run-rustfix
2+
3+
#![warn(clippy::collapsible_str_replace)]
4+
5+
fn get_filter() -> char {
6+
'u'
7+
}
8+
9+
fn main() {
10+
let d = 'd';
11+
let p = 'p';
12+
let s = 's';
13+
let u = 'u';
14+
let l = "l";
15+
16+
let mut iter = ["l", "z"].iter();
17+
18+
// LINT CASES
19+
let _ = "hesuo worpd".replace(['s', 'u'], "l");
20+
21+
let _ = "hesuo worpd".replace(['s', 'u'], l);
22+
23+
let _ = "hesuo worpd".replace(['s', 'u', 'p'], "l");
24+
25+
let _ = "hesuo worpd"
26+
.replace(['s', 'u', 'p', 'd'], "l");
27+
28+
let _ = "hesuo world".replace([s, 'u'], "l");
29+
30+
let _ = "hesuo worpd".replace([s, 'u', 'p'], "l");
31+
32+
let _ = "hesuo worpd".replace([s, u, 'p'], "l");
33+
34+
let _ = "hesuo worpd".replace([s, u, p], "l");
35+
36+
let _ = "hesuo worlp".replace(['s', 'u'], "l").replace('p', "d");
37+
38+
let _ = "hesuo worpd".replace('s', "x").replace(['u', 'p'], "l");
39+
40+
// Note: Future iterations could lint `replace(|c| matches!(c, "su" | 'd' | 'p'), "l")`
41+
let _ = "hesudo worpd".replace("su", "l").replace(['d', 'p'], "l");
42+
43+
let _ = "hesudo worpd".replace([d, 'p'], "l").replace("su", "l");
44+
45+
let _ = "hesuo world".replace([get_filter(), 's'], "l");
46+
47+
// NO LINT CASES
48+
let _ = "hesuo world".replace('s', "l").replace('u', "p");
49+
50+
let _ = "hesuo worpd".replace('s', "l").replace('p', l);
51+
52+
let _ = "hesudo worpd".replace('d', "l").replace("su", "l").replace('p', "l");
53+
54+
// Note: Future iterations of `collapsible_str_replace` might lint this and combine to `[s, u, p]`
55+
let _ = "hesuo worpd".replace([s, u], "l").replace([u, p], "l");
56+
57+
let _ = "hesuo worpd".replace(['s', 'u'], "l").replace(['u', 'p'], "l");
58+
59+
let _ = "hesuo worpd".replace('s', "l").replace(['u', 'p'], "l");
60+
61+
let _ = "hesuo worpd".replace(['s', 'u', 'p'], "l").replace('r', "l");
62+
63+
let _ = "hesuo worpd".replace(['s', 'u', 'p'], l).replace('r', l);
64+
65+
let _ = "hesuo worpd".replace(['s', u, 'p'], "l").replace('r', "l");
66+
67+
let _ = "hesuo worpd".replace([s, u], "l").replace(p, "l");
68+
69+
// Regression test
70+
let _ = "hesuo worpd"
71+
.replace('u', iter.next().unwrap())
72+
.replace('s', iter.next().unwrap());
73+
}

tests/ui/collapsible_str_replace.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// run-rustfix
2+
3+
#![warn(clippy::collapsible_str_replace)]
4+
5+
fn get_filter() -> char {
6+
'u'
7+
}
8+
9+
fn main() {
10+
let d = 'd';
11+
let p = 'p';
12+
let s = 's';
13+
let u = 'u';
14+
let l = "l";
15+
16+
let mut iter = ["l", "z"].iter();
17+
18+
// LINT CASES
19+
let _ = "hesuo worpd".replace('s', "l").replace('u', "l");
20+
21+
let _ = "hesuo worpd".replace('s', l).replace('u', l);
22+
23+
let _ = "hesuo worpd".replace('s', "l").replace('u', "l").replace('p', "l");
24+
25+
let _ = "hesuo worpd"
26+
.replace('s', "l")
27+
.replace('u', "l")
28+
.replace('p', "l")
29+
.replace('d', "l");
30+
31+
let _ = "hesuo world".replace(s, "l").replace('u', "l");
32+
33+
let _ = "hesuo worpd".replace(s, "l").replace('u', "l").replace('p', "l");
34+
35+
let _ = "hesuo worpd".replace(s, "l").replace(u, "l").replace('p', "l");
36+
37+
let _ = "hesuo worpd".replace(s, "l").replace(u, "l").replace(p, "l");
38+
39+
let _ = "hesuo worlp".replace('s', "l").replace('u', "l").replace('p', "d");
40+
41+
let _ = "hesuo worpd".replace('s', "x").replace('u', "l").replace('p', "l");
42+
43+
// Note: Future iterations could lint `replace(|c| matches!(c, "su" | 'd' | 'p'), "l")`
44+
let _ = "hesudo worpd".replace("su", "l").replace('d', "l").replace('p', "l");
45+
46+
let _ = "hesudo worpd".replace(d, "l").replace('p', "l").replace("su", "l");
47+
48+
let _ = "hesuo world".replace(get_filter(), "l").replace('s', "l");
49+
50+
// NO LINT CASES
51+
let _ = "hesuo world".replace('s', "l").replace('u', "p");
52+
53+
let _ = "hesuo worpd".replace('s', "l").replace('p', l);
54+
55+
let _ = "hesudo worpd".replace('d', "l").replace("su", "l").replace('p', "l");
56+
57+
// Note: Future iterations of `collapsible_str_replace` might lint this and combine to `[s, u, p]`
58+
let _ = "hesuo worpd".replace([s, u], "l").replace([u, p], "l");
59+
60+
let _ = "hesuo worpd".replace(['s', 'u'], "l").replace(['u', 'p'], "l");
61+
62+
let _ = "hesuo worpd".replace('s', "l").replace(['u', 'p'], "l");
63+
64+
let _ = "hesuo worpd".replace(['s', 'u', 'p'], "l").replace('r', "l");
65+
66+
let _ = "hesuo worpd".replace(['s', 'u', 'p'], l).replace('r', l);
67+
68+
let _ = "hesuo worpd".replace(['s', u, 'p'], "l").replace('r', "l");
69+
70+
let _ = "hesuo worpd".replace([s, u], "l").replace(p, "l");
71+
72+
// Regression test
73+
let _ = "hesuo worpd"
74+
.replace('u', iter.next().unwrap())
75+
.replace('s', iter.next().unwrap());
76+
}

0 commit comments

Comments
 (0)