Skip to content

Commit a9bd0bd

Browse files
committed
Handle repeated str::replace calls with single char kind to str
1 parent 6e86687 commit a9bd0bd

File tree

3 files changed

+258
-91
lines changed

3 files changed

+258
-91
lines changed
Lines changed: 250 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
2-
// use clippy_utils::source::snippet_with_context;
2+
use clippy_utils::get_parent_expr;
33
use clippy_utils::visitors::for_each_expr;
44
use core::ops::ControlFlow;
55
use if_chain::if_chain;
66
use rustc_ast::ast::LitKind;
7+
use rustc_data_structures::fx::FxHashSet;
78
use rustc_errors::Applicability;
89
use rustc_hir as hir;
910
use rustc_hir::*;
1011
use rustc_lint::LateContext;
1112
use rustc_middle::ty;
1213
use rustc_span::source_map::Spanned;
13-
use std::unreachable;
14-
// use rustc_span::Span;
1514

1615
use super::method_call;
1716
use super::COLLAPSIBLE_STR_REPLACE;
@@ -25,28 +24,46 @@ pub(super) fn check<'tcx>(
2524
) {
2625
match (name, args) {
2726
("replace", [from, to]) => {
28-
// Check for `str::replace` calls with char slice for linting
27+
// The receiver of the method call must be `str` type to lint `collapsible_str_replace`
2928
let original_recv = find_original_recv(recv);
30-
let original_recv_ty = cx.typeck_results().expr_ty(original_recv).peel_refs();
29+
let original_recv_ty_kind = cx.typeck_results().expr_ty(original_recv).peel_refs().kind();
30+
let original_recv_is_str_kind = matches!(original_recv_ty_kind, ty::Str);
31+
3132
if_chain! {
32-
// Check the receiver of the method call is `str` type
33-
if matches!(original_recv_ty.kind(), ty::Str);
34-
let from_ty = cx.typeck_results().expr_ty(from).peel_refs();
35-
if let ty::Array(array_ty, _) = from_ty.kind();
33+
// Check for `str::replace` calls with char slice for linting
34+
if original_recv_is_str_kind;
35+
let from_ty_kind = cx.typeck_results().expr_ty(from).peel_refs().kind();
36+
if let ty::Array(array_ty, _) = from_ty_kind;
3637
if matches!(array_ty.kind(), ty::Char);
3738
then {
3839
check_replace_call_with_char_slice(cx, from, to);
40+
return;
41+
}
42+
}
43+
44+
if_chain! {
45+
if original_recv_is_str_kind;
46+
if let Some(parent) = get_parent_expr(cx, expr);
47+
if let Some((name, [..], _)) = method_call(parent);
48+
49+
then {
50+
match name {
51+
"replace" => return,
52+
_ => {
53+
check_consecutive_replace_calls(cx, expr);
54+
return;
55+
},
56+
}
3957
}
4058
}
4159

4260
match method_call(recv) {
4361
// Check if there's an earlier `str::replace` call
44-
Some(("replace", [prev_recv, prev_from, prev_to], prev_span)) => {
45-
println!("Consecutive replace calls");
46-
// Check that the original receiver is of `ty::Str` type
47-
// Check that all the `from` args are char literals
48-
// Check that all the `to` args are the same variable or has the same &str value
49-
// If so, then lint
62+
Some(("replace", [_, _, _], _)) => {
63+
if original_recv_is_str_kind {
64+
check_consecutive_replace_calls(cx, expr);
65+
return;
66+
}
5067
},
5168
_ => {},
5269
}
@@ -55,100 +72,246 @@ pub(super) fn check<'tcx>(
5572
}
5673
}
5774

58-
fn find_original_recv<'tcx>(recv: &'tcx hir::Expr<'tcx>) -> &'tcx hir::Expr<'tcx> {
59-
let mut original_recv = recv;
60-
61-
let _: Option<()> = for_each_expr(recv, |e| {
62-
if let Some((name, [prev_recv, args @ ..], _)) = method_call(e) {
63-
match (name, args) {
64-
("replace", [_, _]) => {
65-
original_recv = prev_recv;
66-
ControlFlow::Continue(())
67-
},
68-
_ => ControlFlow::BREAK,
69-
}
70-
} else {
71-
ControlFlow::Continue(())
72-
}
73-
});
74-
75-
original_recv
76-
}
77-
75+
/// Check a `str::replace` call that contains a char slice as `from` argument for
76+
/// `collapsible_str_replace` lint.
7877
fn check_replace_call_with_char_slice<'tcx>(
7978
cx: &LateContext<'tcx>,
8079
from_arg: &'tcx hir::Expr<'tcx>,
8180
to_arg: &'tcx hir::Expr<'tcx>,
8281
) {
83-
let mut has_no_var = true;
84-
let mut char_list: Vec<char> = Vec::new();
82+
let mut char_slice_has_no_variables = true;
83+
let mut chars: Vec<String> = Vec::new();
84+
8585
// Go through the `from_arg` to collect all char literals
8686
let _: Option<()> = for_each_expr(from_arg, |e| {
8787
if let ExprKind::Lit(Spanned {
88-
node: LitKind::Char(val),
89-
..
88+
node: LitKind::Char(_), ..
9089
}) = e.kind
9190
{
92-
char_list.push(val);
91+
chars.push(get_replace_call_char_arg_repr(e).unwrap());
9392
ControlFlow::Continue(())
9493
} else if let ExprKind::Path(..) = e.kind {
9594
// If a variable is found in the char slice, no lint for first version of this lint
96-
has_no_var = false;
95+
char_slice_has_no_variables = false;
9796
ControlFlow::BREAK
9897
} else {
9998
ControlFlow::Continue(())
10099
}
101100
});
102101

103-
if has_no_var {
104-
let to_arg_repr = match to_arg.kind {
105-
ExprKind::Lit(Spanned {
106-
node: LitKind::Str(to_arg_val, _),
107-
..
108-
}) => {
109-
let repr = to_arg_val.as_str();
110-
let double_quote = "\"";
111-
double_quote.to_owned() + repr + double_quote
112-
},
113-
ExprKind::Path(QPath::Resolved(
114-
_,
115-
Path {
116-
segments: path_segments,
117-
..
102+
if char_slice_has_no_variables {
103+
if let Some(to_arg_repr) = get_replace_call_char_arg_repr(to_arg) {
104+
let app = Applicability::MachineApplicable;
105+
span_lint_and_sugg(
106+
cx,
107+
COLLAPSIBLE_STR_REPLACE,
108+
from_arg.span,
109+
"used slice of chars in `str::replace` call",
110+
"replace with",
111+
format!("replace(|c| matches!(c, {}), {})", chars.join(" | "), to_arg_repr,),
112+
app,
113+
);
114+
}
115+
}
116+
}
117+
118+
/// Check a chain of `str::replace` calls for `collapsible_str_replace` lint.
119+
fn check_consecutive_replace_calls<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
120+
if_chain! {
121+
if let Some(from_args) = get_replace_call_from_args_if_all_char_ty(cx, expr);
122+
if let Some(to_arg) = get_replace_call_unique_to_arg_repr(expr);
123+
then {
124+
if replace_call_from_args_are_only_lit_chars(&from_args) {
125+
let from_arg_reprs: Vec<String> = from_args.iter().map(|from_arg| {
126+
get_replace_call_char_arg_repr(from_arg).unwrap()
127+
}).collect();
128+
let app = Applicability::MachineApplicable;
129+
130+
span_lint_and_sugg(
131+
cx,
132+
COLLAPSIBLE_STR_REPLACE,
133+
expr.span,
134+
"used consecutive `str::replace` call",
135+
"replace with",
136+
format!(
137+
"replace(|c| matches!(c, {}), {})",
138+
from_arg_reprs.join(" | "),
139+
to_arg,
140+
),
141+
app,
142+
);
143+
} else {
144+
// Use fallback lint
145+
let from_arg_reprs: Vec<String> = from_args.iter().map(|from_arg| {
146+
get_replace_call_char_arg_repr(from_arg).unwrap()
147+
}).collect();
148+
let app = Applicability::MachineApplicable;
149+
150+
span_lint_and_sugg(
151+
cx,
152+
COLLAPSIBLE_STR_REPLACE,
153+
expr.span,
154+
"used consecutive `str::replace` call",
155+
"replace with",
156+
format!(
157+
"replace(&[{}], {})",
158+
from_arg_reprs.join(" , "),
159+
to_arg,
160+
),
161+
app,
162+
);
163+
}
164+
}
165+
}
166+
}
167+
168+
/// Check if all the `from` arguments of a chain of consecutive calls to `str::replace`
169+
/// are all of `ExprKind::Lit` types. If any is not, return false.
170+
fn replace_call_from_args_are_only_lit_chars<'tcx>(from_args: &Vec<&'tcx hir::Expr<'tcx>>) -> bool {
171+
let mut only_lit_chars = true;
172+
173+
for from_arg in from_args.iter() {
174+
match from_arg.kind {
175+
ExprKind::Lit(..) => {},
176+
_ => only_lit_chars = false,
177+
}
178+
}
179+
180+
only_lit_chars
181+
}
182+
183+
/// Collect and return all of the `from` arguments of a chain of consecutive `str::replace` calls
184+
/// if these `from` arguments's expressions are of the `ty::Char` kind. Otherwise return `None`.
185+
fn get_replace_call_from_args_if_all_char_ty<'tcx>(
186+
cx: &LateContext<'tcx>,
187+
expr: &'tcx hir::Expr<'tcx>,
188+
) -> Option<Vec<&'tcx hir::Expr<'tcx>>> {
189+
let mut all_from_args_are_chars = true;
190+
let mut from_args = Vec::new();
191+
192+
let _: Option<()> = for_each_expr(expr, |e| {
193+
if let Some((name, [_, args @ ..], _)) = method_call(e) {
194+
match (name, args) {
195+
("replace", [from, _]) => {
196+
let from_ty_kind = cx.typeck_results().expr_ty(from).peel_refs().kind();
197+
if matches!(from_ty_kind, ty::Char) {
198+
from_args.push(from);
199+
} else {
200+
all_from_args_are_chars = false;
201+
}
202+
ControlFlow::Continue(())
118203
},
119-
)) => {
120-
// join the path_segments values by "::"
121-
let path_segment_ident_names: Vec<&str> = path_segments
122-
.iter()
123-
.map(|path_seg| path_seg.ident.name.as_str())
124-
.collect();
125-
126-
path_segment_ident_names.join("::")
204+
_ => ControlFlow::BREAK,
205+
}
206+
} else {
207+
ControlFlow::Continue(())
208+
}
209+
});
210+
211+
if all_from_args_are_chars {
212+
return Some(from_args);
213+
} else {
214+
return None;
215+
}
216+
}
217+
218+
/// Return a unique String representation of the `to` argument used in a chain of `str::replace`
219+
/// calls if each `str::replace` call's `to` argument is identical to the other `to` arguments in
220+
/// the chain. Otherwise, return `None`.
221+
fn get_replace_call_unique_to_arg_repr<'tcx>(expr: &'tcx hir::Expr<'tcx>) -> Option<String> {
222+
let mut to_args = Vec::new();
223+
224+
let _: Option<()> = for_each_expr(expr, |e| {
225+
if let Some((name, [_, args @ ..], _)) = method_call(e) {
226+
match (name, args) {
227+
("replace", [_, to]) => {
228+
to_args.push(to);
229+
ControlFlow::Continue(())
230+
},
231+
_ => ControlFlow::BREAK,
232+
}
233+
} else {
234+
ControlFlow::Continue(())
235+
}
236+
});
237+
238+
// let mut to_arg_repr_set = FxHashSet::default();
239+
let mut to_arg_reprs = Vec::new();
240+
for &to_arg in to_args.iter() {
241+
if let Some(to_arg_repr) = get_replace_call_char_arg_repr(to_arg) {
242+
to_arg_reprs.push(to_arg_repr);
243+
}
244+
}
245+
246+
let to_arg_repr_set = FxHashSet::from_iter(to_arg_reprs.iter().cloned());
247+
// Check if the set of `to` argument representations has more than one unique value
248+
if to_arg_repr_set.len() != 1 {
249+
return None;
250+
}
251+
252+
// Return the single representation value
253+
to_arg_reprs.pop()
254+
}
255+
256+
/// Get the representation of an argument of a `str::replace` call either of the literal char value
257+
/// or variable name, i.e. the resolved path segments `ident`.
258+
/// Return:
259+
/// - the str literal with double quotes, e.g. "\"l\""
260+
/// - the char literal with single quotes, e.g. "'l'"
261+
/// - the variable as a String, e.g. "l"
262+
fn get_replace_call_char_arg_repr<'tcx>(arg: &'tcx hir::Expr<'tcx>) -> Option<String> {
263+
match arg.kind {
264+
ExprKind::Lit(Spanned {
265+
node: LitKind::Str(to_arg_val, _),
266+
..
267+
}) => {
268+
let repr = to_arg_val.as_str();
269+
let double_quote = "\"";
270+
Some(double_quote.to_owned() + repr + double_quote)
271+
},
272+
ExprKind::Lit(Spanned {
273+
node: LitKind::Char(to_arg_val),
274+
..
275+
}) => {
276+
let repr = to_arg_val.to_string();
277+
let double_quote = "\'";
278+
Some(double_quote.to_owned() + &repr + double_quote)
279+
},
280+
ExprKind::Path(QPath::Resolved(
281+
_,
282+
Path {
283+
segments: path_segments,
284+
..
127285
},
128-
_ => unreachable!(),
129-
};
130-
131-
let app = Applicability::MachineApplicable;
132-
span_lint_and_sugg(
133-
cx,
134-
COLLAPSIBLE_STR_REPLACE,
135-
from_arg.span,
136-
"used slice of chars in `str::replace` call",
137-
"replace with",
138-
format!(
139-
"replace(|c| matches!(c, {}), {})",
140-
format_slice_of_chars_for_sugg(&char_list),
141-
to_arg_repr,
142-
),
143-
app,
144-
);
286+
)) => {
287+
// join the path_segments values by "::"
288+
let path_segment_ident_names: Vec<&str> = path_segments
289+
.iter()
290+
.map(|path_seg| path_seg.ident.name.as_str())
291+
.collect();
292+
Some(path_segment_ident_names.join("::"))
293+
},
294+
_ => None,
145295
}
146296
}
147297

148-
fn format_slice_of_chars_for_sugg(chars: &Vec<char>) -> String {
149-
let single_quoted_chars: Vec<String> = chars
150-
.iter()
151-
.map(|c| "'".to_owned() + &c.to_string() + &"'".to_owned())
152-
.collect();
153-
single_quoted_chars.join(" | ")
298+
/// Find the original receiver of a chain of `str::replace` method calls.
299+
fn find_original_recv<'tcx>(recv: &'tcx hir::Expr<'tcx>) -> &'tcx hir::Expr<'tcx> {
300+
let mut original_recv = recv;
301+
302+
let _: Option<()> = for_each_expr(recv, |e| {
303+
if let Some((name, [prev_recv, args @ ..], _)) = method_call(e) {
304+
match (name, args) {
305+
("replace", [_, _]) => {
306+
original_recv = prev_recv;
307+
ControlFlow::Continue(())
308+
},
309+
_ => ControlFlow::BREAK,
310+
}
311+
} else {
312+
ControlFlow::Continue(())
313+
}
314+
});
315+
316+
original_recv
154317
}

0 commit comments

Comments
 (0)