Skip to content

Commit b070b40

Browse files
committed
Simplify lint logic and address code review comments
1 parent fb30b64 commit b070b40

File tree

5 files changed

+201
-330
lines changed

5 files changed

+201
-330
lines changed
Lines changed: 61 additions & 240 deletions
Original file line numberDiff line numberDiff line change
@@ -1,275 +1,96 @@
1-
// run-rustfix
2-
31
use clippy_utils::diagnostics::span_lint_and_sugg;
4-
use clippy_utils::get_parent_expr;
2+
use clippy_utils::source::snippet;
53
use clippy_utils::visitors::for_each_expr;
4+
use clippy_utils::{eq_expr_value, get_parent_expr};
65
use core::ops::ControlFlow;
7-
use if_chain::if_chain;
8-
use rustc_ast::ast::LitKind;
9-
use rustc_data_structures::fx::FxHashSet;
106
use rustc_errors::Applicability;
117
use rustc_hir as hir;
12-
use rustc_hir::{ExprKind, Path, QPath};
138
use rustc_lint::LateContext;
14-
use rustc_middle::ty;
15-
use rustc_span::source_map::Spanned;
16-
use rustc_span::Span;
9+
use std::collections::VecDeque;
1710

1811
use super::method_call;
1912
use super::COLLAPSIBLE_STR_REPLACE;
2013

2114
pub(super) fn check<'tcx>(
2215
cx: &LateContext<'tcx>,
2316
expr: &'tcx hir::Expr<'tcx>,
24-
name: &str,
25-
recv: &'tcx hir::Expr<'tcx>,
17+
from: &'tcx hir::Expr<'tcx>,
18+
to: &'tcx hir::Expr<'tcx>,
2619
) {
27-
if name == "replace" {
28-
// The receiver of the method call must be `str` type to lint `collapsible_str_replace`
29-
let original_recv = find_original_recv(recv);
30-
let original_recv_ty_kind = cx.typeck_results().expr_ty(original_recv).peel_refs().kind();
31-
let original_recv_is_str_kind = matches!(original_recv_ty_kind, ty::Str);
32-
33-
if_chain! {
34-
if original_recv_is_str_kind;
35-
if let Some(parent) = get_parent_expr(cx, expr);
36-
if let Some((name, ..)) = method_call(parent);
37-
if name == "replace";
38-
39-
then {
40-
// If the parent node is a `str::replace` call, we've already handled the lint, don't lint again
41-
return;
42-
}
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;
4331
}
4432

45-
if let Some(("replace", ..)) = method_call(recv) {
46-
// Check if there's an earlier `str::replace` call
47-
if original_recv_is_str_kind {
48-
check_consecutive_replace_calls(cx, expr);
49-
}
50-
}
33+
check_consecutive_replace_calls(cx, expr, &replace_methods, to);
5134
}
5235
}
5336

54-
/// Check a chain of `str::replace` calls for `collapsible_str_replace` lint.
55-
fn check_consecutive_replace_calls<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
56-
if_chain! {
57-
if let Some(from_args) = get_replace_call_from_args_if_all_char_ty(cx, expr);
58-
if let Some(to_arg) = get_replace_call_unique_to_arg_repr(expr);
59-
then {
60-
let earliest_replace_call_span = get_earliest_replace_call_span(expr);
61-
62-
if replace_call_from_args_are_only_lit_chars(&from_args) {
63-
let from_arg_reprs: Vec<String> = from_args.iter().map(|from_arg| {
64-
get_replace_call_char_arg_repr(from_arg).unwrap()
65-
}).collect();
66-
let app = Applicability::MachineApplicable;
67-
68-
span_lint_and_sugg(
69-
cx,
70-
COLLAPSIBLE_STR_REPLACE,
71-
expr.span.with_lo(earliest_replace_call_span.lo()),
72-
"used consecutive `str::replace` call",
73-
"replace with",
74-
format!(
75-
"replace(|c| matches!(c, {}), {})",
76-
from_arg_reprs.join(" | "),
77-
to_arg,
78-
),
79-
app,
80-
);
81-
} else {
82-
// Use fallback lint
83-
let from_arg_reprs: Vec<String> = from_args.iter().map(|from_arg| {
84-
get_replace_call_char_arg_repr(from_arg).unwrap()
85-
}).collect();
86-
let app = Applicability::MachineApplicable;
87-
88-
span_lint_and_sugg(
89-
cx,
90-
COLLAPSIBLE_STR_REPLACE,
91-
expr.span.with_lo(earliest_replace_call_span.lo()),
92-
"used consecutive `str::replace` call",
93-
"replace with",
94-
format!(
95-
"replace(&[{}], {})",
96-
from_arg_reprs.join(" , "),
97-
to_arg,
98-
),
99-
app,
100-
);
101-
}
102-
}
103-
}
37+
struct ReplaceMethods<'tcx> {
38+
methods: VecDeque<&'tcx hir::Expr<'tcx>>,
39+
from_args: VecDeque<&'tcx hir::Expr<'tcx>>,
10440
}
10541

106-
/// Check if all the `from` arguments of a chain of consecutive calls to `str::replace`
107-
/// are all of `ExprKind::Lit` types. If any is not, return false.
108-
fn replace_call_from_args_are_only_lit_chars<'tcx>(from_args: &[&'tcx hir::Expr<'tcx>]) -> bool {
109-
let mut only_lit_chars = true;
110-
111-
for from_arg in from_args.iter() {
112-
match from_arg.kind {
113-
ExprKind::Lit(..) => {},
114-
_ => only_lit_chars = false,
115-
}
116-
}
117-
118-
only_lit_chars
119-
}
120-
121-
/// Collect and return all of the `from` arguments of a chain of consecutive `str::replace` calls
122-
/// if these `from` arguments's expressions are of the `ty::Char` kind. Otherwise return `None`.
123-
fn get_replace_call_from_args_if_all_char_ty<'tcx>(
42+
fn collect_replace_calls<'tcx>(
12443
cx: &LateContext<'tcx>,
12544
expr: &'tcx hir::Expr<'tcx>,
126-
) -> Option<Vec<&'tcx hir::Expr<'tcx>>> {
127-
let mut all_from_args_are_chars = true;
128-
let mut from_args = Vec::new();
45+
to_arg: &'tcx hir::Expr<'tcx>,
46+
) -> ReplaceMethods<'tcx> {
47+
let mut methods = VecDeque::new();
48+
let mut from_args = VecDeque::new();
12949

13050
let _: Option<()> = for_each_expr(expr, |e| {
131-
if let Some((name, [_, args @ ..], _)) = method_call(e) {
132-
match (name, args) {
133-
("replace", [from, _]) => {
134-
let from_ty_kind = cx.typeck_results().expr_ty(from).peel_refs().kind();
135-
if matches!(from_ty_kind, ty::Char) {
136-
from_args.push(from);
137-
} else {
138-
all_from_args_are_chars = false;
139-
}
140-
ControlFlow::Continue(())
141-
},
142-
_ => ControlFlow::BREAK,
143-
}
144-
} else {
145-
ControlFlow::Continue(())
146-
}
147-
});
148-
149-
if all_from_args_are_chars {
150-
return Some(from_args);
151-
}
152-
153-
None
154-
}
155-
156-
/// Return a unique String representation of the `to` argument used in a chain of `str::replace`
157-
/// calls if each `str::replace` call's `to` argument is identical to the other `to` arguments in
158-
/// the chain. Otherwise, return `None`.
159-
fn get_replace_call_unique_to_arg_repr<'tcx>(expr: &'tcx hir::Expr<'tcx>) -> Option<String> {
160-
let mut to_args = Vec::new();
161-
162-
let _: Option<()> = for_each_expr(expr, |e| {
163-
if let Some((name, [_, args @ ..], _)) = method_call(e) {
164-
match (name, args) {
165-
("replace", [_, to]) => {
166-
to_args.push(to);
167-
ControlFlow::Continue(())
168-
},
169-
_ => ControlFlow::BREAK,
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
17058
}
17159
} else {
17260
ControlFlow::Continue(())
17361
}
17462
});
17563

176-
// let mut to_arg_repr_set = FxHashSet::default();
177-
let mut to_arg_reprs = Vec::new();
178-
for &to_arg in &to_args {
179-
if let Some(to_arg_repr) = get_replace_call_char_arg_repr(to_arg) {
180-
to_arg_reprs.push(to_arg_repr);
181-
}
182-
}
183-
184-
let to_arg_repr_set = to_arg_reprs.iter().cloned().collect::<FxHashSet<_>>();
185-
// Check if the set of `to` argument representations has more than one unique value
186-
if to_arg_repr_set.len() != 1 {
187-
return None;
188-
}
189-
190-
// Return the single representation value
191-
to_arg_reprs.pop()
64+
ReplaceMethods { methods, from_args }
19265
}
19366

194-
/// Get the representation of an argument of a `str::replace` call either of the literal char value
195-
/// or variable name, i.e. the resolved path segments `ident`.
196-
/// Return:
197-
/// - the str literal with double quotes, e.g. "\"l\""
198-
/// - the char literal with single quotes, e.g. "'l'"
199-
/// - the variable as a String, e.g. "l"
200-
fn get_replace_call_char_arg_repr<'tcx>(arg: &'tcx hir::Expr<'tcx>) -> Option<String> {
201-
match arg.kind {
202-
ExprKind::Lit(Spanned {
203-
node: LitKind::Str(to_arg_val, _),
204-
..
205-
}) => {
206-
let repr = to_arg_val.as_str();
207-
let double_quote = "\"";
208-
Some(double_quote.to_owned() + repr + double_quote)
209-
},
210-
ExprKind::Lit(Spanned {
211-
node: LitKind::Char(to_arg_val),
212-
..
213-
}) => {
214-
let repr = to_arg_val.to_string();
215-
let double_quote = "\'";
216-
Some(double_quote.to_owned() + &repr + double_quote)
217-
},
218-
ExprKind::Path(QPath::Resolved(
219-
_,
220-
Path {
221-
segments: path_segments,
222-
..
223-
},
224-
)) => {
225-
// join the path_segments values by "::"
226-
let path_segment_ident_names: Vec<&str> = path_segments
227-
.iter()
228-
.map(|path_seg| path_seg.ident.name.as_str())
229-
.collect();
230-
Some(path_segment_ident_names.join("::"))
231-
},
232-
_ => None,
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+
);
23395
}
23496
}
235-
236-
fn get_earliest_replace_call_span<'tcx>(expr: &'tcx hir::Expr<'tcx>) -> Span {
237-
let mut earliest_replace_call_span = expr.span;
238-
239-
let _: Option<()> = for_each_expr(expr, |e| {
240-
if let Some((name, [_, args @ ..], span)) = method_call(e) {
241-
match (name, args) {
242-
("replace", [_, _]) => {
243-
earliest_replace_call_span = span;
244-
ControlFlow::Continue(())
245-
},
246-
_ => ControlFlow::BREAK,
247-
}
248-
} else {
249-
ControlFlow::Continue(())
250-
}
251-
});
252-
253-
earliest_replace_call_span
254-
}
255-
256-
/// Find the original receiver of a chain of `str::replace` method calls.
257-
fn find_original_recv<'tcx>(recv: &'tcx hir::Expr<'tcx>) -> &'tcx hir::Expr<'tcx> {
258-
let mut original_recv = recv;
259-
260-
let _: Option<()> = for_each_expr(recv, |e| {
261-
if let Some((name, [prev_recv, args @ ..], _)) = method_call(e) {
262-
match (name, args) {
263-
("replace", [_, _]) => {
264-
original_recv = prev_recv;
265-
ControlFlow::Continue(())
266-
},
267-
_ => ControlFlow::BREAK,
268-
}
269-
} else {
270-
ControlFlow::Continue(())
271-
}
272-
});
273-
274-
original_recv
275-
}

clippy_lints/src/methods/mod.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ declare_clippy_lint! {
156156
/// ```
157157
/// Use instead:
158158
/// ```rust
159-
/// let hello = "hesuo worpd".replace(|c| matches!(c, 's' | 'u' | 'p'), "l");
159+
/// let hello = "hesuo worpd".replace(&['s', 'u', 'p'], "l");
160160
/// ```
161161
#[clippy::version = "1.64.0"]
162162
pub COLLAPSIBLE_STR_REPLACE,
@@ -3507,6 +3507,14 @@ impl Methods {
35073507
("repeat", [arg]) => {
35083508
repeat_once::check(cx, expr, recv, arg);
35093509
},
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+
},
35103518
("resize", [count_arg, default_arg]) => {
35113519
vec_resize_to_zero::check(cx, expr, count_arg, default_arg, span);
35123520
},
@@ -3519,10 +3527,6 @@ impl Methods {
35193527
("sort_unstable_by", [arg]) => {
35203528
unnecessary_sort_by::check(cx, expr, recv, arg, true);
35213529
},
3522-
("replace" | "replacen", [arg1, arg2] | [arg1, arg2, _]) => {
3523-
no_effect_replace::check(cx, expr, arg1, arg2);
3524-
collapsible_str_replace::check(cx, expr, name, recv);
3525-
},
35263530
("splitn" | "rsplitn", [count_arg, pat_arg]) => {
35273531
if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg) {
35283532
suspicious_splitn::check(cx, name, expr, recv, count);

0 commit comments

Comments
 (0)