Skip to content

Commit 5ed93af

Browse files
committed
Use ExprUseVisitor and multipart suggestion to avoid iffy String replacement
1 parent f4c75cd commit 5ed93af

File tree

6 files changed

+334
-209
lines changed

6 files changed

+334
-209
lines changed

clippy_lints/src/methods/search_is_some.rs

Lines changed: 140 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
1+
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg, span_lint_and_then};
22
use clippy_utils::source::{snippet, snippet_with_applicability};
33
use clippy_utils::ty::is_type_diagnostic_item;
44
use clippy_utils::{is_trait_method, strip_pat_refs};
55
use if_chain::if_chain;
66
use rustc_errors::Applicability;
77
use rustc_hir as hir;
8-
use rustc_hir::PatKind;
8+
use rustc_hir::{self, HirId, HirIdMap, HirIdSet, PatKind};
9+
use rustc_infer::infer::TyCtxtInferExt;
910
use rustc_lint::LateContext;
11+
use rustc_middle::hir::place::ProjectionKind;
12+
use rustc_middle::mir::FakeReadCause;
1013
use rustc_middle::ty;
1114
use rustc_span::source_map::Span;
1215
use rustc_span::symbol::sym;
16+
use rustc_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceBase, PlaceWithHirId};
1317

1418
use super::SEARCH_IS_SOME;
1519

@@ -42,30 +46,42 @@ pub(super) fn check<'tcx>(
4246
if let hir::ExprKind::Closure(_, _, body_id, ..) = search_arg.kind;
4347
let closure_body = cx.tcx.hir().body(body_id);
4448
if let Some(closure_arg) = closure_body.params.get(0);
49+
4550
then {
4651
if let hir::PatKind::Ref(..) = closure_arg.pat.kind {
47-
Some(search_snippet.replacen('&', "", 1))
48-
} else if let PatKind::Binding(annotation, _, ident, _) = strip_pat_refs(closure_arg.pat).kind {
49-
let name = &*ident.name.as_str();
50-
let old_search_snippet = search_snippet.clone();
51-
let search_snippet = search_snippet.replace(&format!("*{}", name), name);
52-
53-
if_chain! {
54-
// if there is no dereferencing used in closure body
55-
if old_search_snippet == search_snippet;
56-
if annotation == hir::BindingAnnotation::Unannotated;
57-
if let ty::Ref(_, inner_ty, _) = cx.typeck_results().node_type(closure_arg.hir_id).kind();
58-
if let ty::Ref(..) = inner_ty.kind();
59-
// put an `&` in the closure body, but skip closure params
60-
if let Some((start, end)) = old_search_snippet.split_once(&name);
61-
62-
then {
63-
let end = end.replace(name, &format!("&{}", name));
64-
Some(format!("{}{}{}", start, name, end))
65-
} else {
66-
Some(search_snippet)
52+
Some((search_snippet.replacen('&', "", 1), None))
53+
} else if let PatKind::Binding(..) = strip_pat_refs(closure_arg.pat).kind {
54+
let mut visitor = DerefDelegate {
55+
cx,
56+
set: HirIdSet::default(),
57+
deref_suggs: HirIdMap::default(),
58+
borrow_suggs: HirIdMap::default()
59+
};
60+
61+
let fn_def_id = cx.tcx.hir().local_def_id(search_arg.hir_id);
62+
cx.tcx.infer_ctxt().enter(|infcx| {
63+
ExprUseVisitor::new(
64+
&mut visitor, &infcx, fn_def_id, cx.param_env, cx.typeck_results()
65+
).consume_body(closure_body);
66+
});
67+
68+
let replacements = if visitor.set.is_empty() {
69+
None
70+
} else {
71+
let mut deref_suggs = Vec::new();
72+
let mut borrow_suggs = Vec::new();
73+
for node in visitor.set {
74+
let span = cx.tcx.hir().span(node);
75+
if let Some(sugg) = visitor.deref_suggs.get(&node) {
76+
deref_suggs.push((span, sugg.clone()));
77+
}
78+
if let Some(sugg) = visitor.borrow_suggs.get(&node) {
79+
borrow_suggs.push((span, sugg.clone()));
80+
}
6781
}
68-
}
82+
Some((deref_suggs, borrow_suggs))
83+
};
84+
Some((search_snippet.to_string(), replacements))
6985
} else {
7086
None
7187
}
@@ -74,35 +90,38 @@ pub(super) fn check<'tcx>(
7490
}
7591
};
7692
// add note if not multi-line
77-
if is_some {
78-
span_lint_and_sugg(
79-
cx,
80-
SEARCH_IS_SOME,
93+
let (closure_snippet, replacements) = any_search_snippet
94+
.as_ref()
95+
.map_or((&*search_snippet, None), |s| (&s.0, s.1.clone()));
96+
let (span, help, sugg) = if is_some {
97+
(
8198
method_span.with_hi(expr.span.hi()),
82-
&msg,
8399
"use `any()` instead",
84-
format!(
85-
"any({})",
86-
any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str)
87-
),
88-
Applicability::MachineApplicable,
89-
);
100+
format!("any({})", closure_snippet),
101+
)
90102
} else {
91103
let iter = snippet(cx, search_recv.span, "..");
92-
span_lint_and_sugg(
93-
cx,
94-
SEARCH_IS_SOME,
104+
(
95105
expr.span,
96-
&msg,
97106
"use `!_.any()` instead",
98-
format!(
99-
"!{}.any({})",
100-
iter,
101-
any_search_snippet.as_ref().map_or(&*search_snippet, String::as_str)
102-
),
103-
Applicability::MachineApplicable,
104-
);
105-
}
107+
format!("!{}.any({})", iter, closure_snippet),
108+
)
109+
};
110+
111+
span_lint_and_then(cx, SEARCH_IS_SOME, span, &msg, |db| {
112+
if let Some((deref_suggs, borrow_suggs)) = replacements {
113+
db.span_suggestion(span, help, sugg, Applicability::MaybeIncorrect);
114+
115+
if !deref_suggs.is_empty() {
116+
db.multipart_suggestion("...and remove deref", deref_suggs, Applicability::MaybeIncorrect);
117+
}
118+
if !borrow_suggs.is_empty() {
119+
db.multipart_suggestion("...and borrow variable", borrow_suggs, Applicability::MaybeIncorrect);
120+
}
121+
} else {
122+
db.span_suggestion(span, help, sugg, Applicability::MachineApplicable);
123+
}
124+
});
106125
} else {
107126
let hint = format!(
108127
"this is more succinctly expressed by calling `any()`{}",
@@ -164,3 +183,78 @@ pub(super) fn check<'tcx>(
164183
}
165184
}
166185
}
186+
187+
struct DerefDelegate<'a, 'tcx> {
188+
cx: &'a LateContext<'tcx>,
189+
set: HirIdSet,
190+
deref_suggs: HirIdMap<String>,
191+
borrow_suggs: HirIdMap<String>,
192+
}
193+
194+
impl<'tcx> Delegate<'tcx> for DerefDelegate<'_, 'tcx> {
195+
fn consume(&mut self, cmt: &PlaceWithHirId<'tcx>, _: HirId) {
196+
if let PlaceBase::Local(id) = cmt.place.base {
197+
let map = self.cx.tcx.hir();
198+
if cmt.place.projections.is_empty() {
199+
self.set.insert(cmt.hir_id);
200+
} else {
201+
let mut replacement_str = map.name(id).to_string();
202+
let last_deref = cmt
203+
.place
204+
.projections
205+
.iter()
206+
.rposition(|proj| proj.kind == ProjectionKind::Deref);
207+
208+
if let Some(pos) = last_deref {
209+
let mut projections = cmt.place.projections.clone();
210+
projections.truncate(pos);
211+
212+
for item in projections {
213+
if item.kind == ProjectionKind::Deref {
214+
replacement_str = format!("*{}", replacement_str);
215+
}
216+
}
217+
218+
self.set.insert(cmt.hir_id);
219+
self.deref_suggs.insert(cmt.hir_id, replacement_str);
220+
}
221+
}
222+
}
223+
}
224+
225+
fn borrow(&mut self, cmt: &PlaceWithHirId<'tcx>, _: HirId, _: ty::BorrowKind) {
226+
if let PlaceBase::Local(id) = cmt.place.base {
227+
let map = self.cx.tcx.hir();
228+
if cmt.place.projections.is_empty() {
229+
let replacement_str = format!("&{}", map.name(id).to_string());
230+
self.set.insert(cmt.hir_id);
231+
self.borrow_suggs.insert(cmt.hir_id, replacement_str);
232+
} else {
233+
let mut replacement_str = map.name(id).to_string();
234+
let last_deref = cmt
235+
.place
236+
.projections
237+
.iter()
238+
.rposition(|proj| proj.kind == ProjectionKind::Deref);
239+
240+
if let Some(pos) = last_deref {
241+
let mut projections = cmt.place.projections.clone();
242+
projections.truncate(pos);
243+
244+
for item in projections {
245+
if item.kind == ProjectionKind::Deref {
246+
replacement_str = format!("*{}", replacement_str);
247+
}
248+
}
249+
250+
self.set.insert(cmt.hir_id);
251+
self.deref_suggs.insert(cmt.hir_id, replacement_str);
252+
}
253+
}
254+
}
255+
}
256+
257+
fn mutate(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId) {}
258+
259+
fn fake_read(&mut self, _: rustc_typeck::expr_use_visitor::Place<'tcx>, _: FakeReadCause, _: HirId) {}
260+
}

tests/ui/search_is_some.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ fn main() {
3636
// check that we don't lint if `find()` is called with
3737
// `Pattern` that is not a string
3838
let _ = "hello world".find(|c: char| c == 'o' || c == 'l').is_some();
39+
40+
// Check `find().is_some()`, single-line case.
41+
let _ = (0..1).find(|x| **y == *x).is_some(); // one dereference less
42+
let _ = (0..1).find(|x| *x == 0).is_some();
43+
let _ = v.iter().find(|x| **x == 0).is_some();
3944
}
4045

4146
#[rustfmt::skip]
@@ -70,4 +75,44 @@ fn is_none() {
7075
// check that we don't lint if `find()` is called with
7176
// `Pattern` that is not a string
7277
let _ = "hello world".find(|c: char| c == 'o' || c == 'l').is_none();
78+
79+
// Check `find().is_none()`, single-line case.
80+
let _ = (0..1).find(|x| **y == *x).is_none(); // one dereference less
81+
let _ = (0..1).find(|x| *x == 0).is_none();
82+
let _ = v.iter().find(|x| **x == 0).is_none();
83+
}
84+
85+
#[allow(clippy::clone_on_copy, clippy::map_clone)]
86+
mod issue7392 {
87+
struct Player {
88+
hand: Vec<usize>,
89+
}
90+
fn filter() {
91+
let p = Player {
92+
hand: vec![1, 2, 3, 4, 5],
93+
};
94+
let filter_hand = vec![5];
95+
let _ = p
96+
.hand
97+
.iter()
98+
.filter(|c| filter_hand.iter().find(|cc| c == cc).is_none())
99+
.map(|c| c.clone())
100+
.collect::<Vec<_>>();
101+
}
102+
103+
struct PlayerTuple {
104+
hand: Vec<(usize, char)>,
105+
}
106+
fn filter_tuple() {
107+
let p = PlayerTuple {
108+
hand: vec![(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e')],
109+
};
110+
let filter_hand = vec![5];
111+
let _ = p
112+
.hand
113+
.iter()
114+
.filter(|(c, _)| filter_hand.iter().find(|cc| c == *cc).is_none())
115+
.map(|c| c.clone())
116+
.collect::<Vec<_>>();
117+
}
73118
}

0 commit comments

Comments
 (0)