Skip to content

Commit 8ccf6a6

Browse files
committed
Auto merge of rust-lang#12084 - yuxqiu:manual_retain, r=Alexendoo
fix: incorrect suggestions generated by `manual_retain` lint fixes rust-lang#10393, fixes rust-lang#11457, fixes rust-lang#12081 rust-lang#10393: In the current implementation of `manual_retain`, if the argument to the closure is matched using tuple, they are all treated as the result of a call to `map.into_iter().filter(<f>)`. However, such tuple pattern matching can also occur in many different containers that stores tuples internally. The correct approach is to apply different lint policies depending on whether the receiver of `into_iter` is a map or not. rust-lang#11457 and rust-lang#12081: In the current implementation of `manual_retain`, if the argument to the closure is `Binding`, the closure will be used directly in the `retain` method, which will result in incorrect suggestion because the first argument to the `retain` closure may be of a different type. In addition, if the argument to the closure is `Ref + Binding`, the lint will simply remove the `Ref` part and use the `Binding` part as the argument to the new closure, which will lead to bad suggestion for the same reason. The correct approach is to detect each of these cases and apply lint suggestions conservatively. changelog: [`manual_retain`] refactor and add check for various patterns
2 parents 276ce39 + 03b3a16 commit 8ccf6a6

File tree

4 files changed

+345
-64
lines changed

4 files changed

+345
-64
lines changed

clippy_lints/src/manual_retain.rs

Lines changed: 100 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use rustc_lint::{LateContext, LateLintPass};
1111
use rustc_semver::RustcVersion;
1212
use rustc_session::impl_lint_pass;
1313
use rustc_span::symbol::sym;
14+
use rustc_span::Span;
1415

1516
const ACCEPTABLE_METHODS: [&[&str]; 5] = [
1617
&paths::BINARYHEAP_ITER,
@@ -28,6 +29,7 @@ const ACCEPTABLE_TYPES: [(rustc_span::Symbol, Option<RustcVersion>); 7] = [
2829
(sym::Vec, None),
2930
(sym::VecDeque, None),
3031
];
32+
const MAP_TYPES: [rustc_span::Symbol; 2] = [sym::BTreeMap, sym::HashMap];
3133

3234
declare_clippy_lint! {
3335
/// ### What it does
@@ -44,6 +46,7 @@ declare_clippy_lint! {
4446
/// ```no_run
4547
/// let mut vec = vec![0, 1, 2];
4648
/// vec.retain(|x| x % 2 == 0);
49+
/// vec.retain(|x| x % 2 == 0);
4750
/// ```
4851
#[clippy::version = "1.64.0"]
4952
pub MANUAL_RETAIN,
@@ -74,9 +77,9 @@ impl<'tcx> LateLintPass<'tcx> for ManualRetain {
7477
&& let Some(collect_def_id) = cx.typeck_results().type_dependent_def_id(collect_expr.hir_id)
7578
&& cx.tcx.is_diagnostic_item(sym::iterator_collect_fn, collect_def_id)
7679
{
77-
check_into_iter(cx, parent_expr, left_expr, target_expr, &self.msrv);
78-
check_iter(cx, parent_expr, left_expr, target_expr, &self.msrv);
79-
check_to_owned(cx, parent_expr, left_expr, target_expr, &self.msrv);
80+
check_into_iter(cx, left_expr, target_expr, parent_expr.span, &self.msrv);
81+
check_iter(cx, left_expr, target_expr, parent_expr.span, &self.msrv);
82+
check_to_owned(cx, left_expr, target_expr, parent_expr.span, &self.msrv);
8083
}
8184
}
8285

@@ -85,9 +88,9 @@ impl<'tcx> LateLintPass<'tcx> for ManualRetain {
8588

8689
fn check_into_iter(
8790
cx: &LateContext<'_>,
88-
parent_expr: &hir::Expr<'_>,
8991
left_expr: &hir::Expr<'_>,
9092
target_expr: &hir::Expr<'_>,
93+
parent_expr_span: Span,
9194
msrv: &Msrv,
9295
) {
9396
if let hir::ExprKind::MethodCall(_, into_iter_expr, [_], _) = &target_expr.kind
@@ -98,16 +101,39 @@ fn check_into_iter(
98101
&& Some(into_iter_def_id) == cx.tcx.lang_items().into_iter_fn()
99102
&& match_acceptable_type(cx, left_expr, msrv)
100103
&& SpanlessEq::new(cx).eq_expr(left_expr, struct_expr)
104+
&& let hir::ExprKind::MethodCall(_, _, [closure_expr], _) = target_expr.kind
105+
&& let hir::ExprKind::Closure(closure) = closure_expr.kind
106+
&& let filter_body = cx.tcx.hir().body(closure.body)
107+
&& let [filter_params] = filter_body.params
101108
{
102-
suggest(cx, parent_expr, left_expr, target_expr);
109+
if match_map_type(cx, left_expr) {
110+
if let hir::PatKind::Tuple([key_pat, value_pat], _) = filter_params.pat.kind {
111+
if let Some(sugg) = make_sugg(cx, key_pat, value_pat, left_expr, filter_body) {
112+
make_span_lint_and_sugg(cx, parent_expr_span, sugg);
113+
}
114+
}
115+
// Cannot lint other cases because `retain` requires two parameters
116+
} else {
117+
// Can always move because `retain` and `filter` have the same bound on the predicate
118+
// for other types
119+
make_span_lint_and_sugg(
120+
cx,
121+
parent_expr_span,
122+
format!(
123+
"{}.retain({})",
124+
snippet(cx, left_expr.span, ".."),
125+
snippet(cx, closure_expr.span, "..")
126+
),
127+
);
128+
}
103129
}
104130
}
105131

106132
fn check_iter(
107133
cx: &LateContext<'_>,
108-
parent_expr: &hir::Expr<'_>,
109134
left_expr: &hir::Expr<'_>,
110135
target_expr: &hir::Expr<'_>,
136+
parent_expr_span: Span,
111137
msrv: &Msrv,
112138
) {
113139
if let hir::ExprKind::MethodCall(_, filter_expr, [], _) = &target_expr.kind
@@ -122,16 +148,50 @@ fn check_iter(
122148
&& match_acceptable_def_path(cx, iter_expr_def_id)
123149
&& match_acceptable_type(cx, left_expr, msrv)
124150
&& SpanlessEq::new(cx).eq_expr(left_expr, struct_expr)
151+
&& let hir::ExprKind::MethodCall(_, _, [closure_expr], _) = filter_expr.kind
152+
&& let hir::ExprKind::Closure(closure) = closure_expr.kind
153+
&& let filter_body = cx.tcx.hir().body(closure.body)
154+
&& let [filter_params] = filter_body.params
125155
{
126-
suggest(cx, parent_expr, left_expr, filter_expr);
156+
match filter_params.pat.kind {
157+
// hir::PatKind::Binding(_, _, _, None) => {
158+
// // Be conservative now. Do nothing here.
159+
// // TODO: Ideally, we can rewrite the lambda by stripping one level of reference
160+
// },
161+
hir::PatKind::Tuple([_, _], _) => {
162+
// the `&&` reference for the `filter` method will be auto derefed to `ref`
163+
// so, we can directly use the lambda
164+
// https://doc.rust-lang.org/reference/patterns.html#binding-modes
165+
make_span_lint_and_sugg(
166+
cx,
167+
parent_expr_span,
168+
format!(
169+
"{}.retain({})",
170+
snippet(cx, left_expr.span, ".."),
171+
snippet(cx, closure_expr.span, "..")
172+
),
173+
);
174+
},
175+
hir::PatKind::Ref(pat, _) => make_span_lint_and_sugg(
176+
cx,
177+
parent_expr_span,
178+
format!(
179+
"{}.retain(|{}| {})",
180+
snippet(cx, left_expr.span, ".."),
181+
snippet(cx, pat.span, ".."),
182+
snippet(cx, filter_body.value.span, "..")
183+
),
184+
),
185+
_ => {},
186+
}
127187
}
128188
}
129189

130190
fn check_to_owned(
131191
cx: &LateContext<'_>,
132-
parent_expr: &hir::Expr<'_>,
133192
left_expr: &hir::Expr<'_>,
134193
target_expr: &hir::Expr<'_>,
194+
parent_expr_span: Span,
135195
msrv: &Msrv,
136196
) {
137197
if msrv.meets(msrvs::STRING_RETAIN)
@@ -147,43 +207,25 @@ fn check_to_owned(
147207
&& let ty = cx.typeck_results().expr_ty(str_expr).peel_refs()
148208
&& is_type_lang_item(cx, ty, hir::LangItem::String)
149209
&& SpanlessEq::new(cx).eq_expr(left_expr, str_expr)
150-
{
151-
suggest(cx, parent_expr, left_expr, filter_expr);
152-
}
153-
}
154-
155-
fn suggest(cx: &LateContext<'_>, parent_expr: &hir::Expr<'_>, left_expr: &hir::Expr<'_>, filter_expr: &hir::Expr<'_>) {
156-
if let hir::ExprKind::MethodCall(_, _, [closure], _) = filter_expr.kind
157-
&& let hir::ExprKind::Closure(&hir::Closure { body, .. }) = closure.kind
158-
&& let filter_body = cx.tcx.hir().body(body)
210+
&& let hir::ExprKind::MethodCall(_, _, [closure_expr], _) = filter_expr.kind
211+
&& let hir::ExprKind::Closure(closure) = closure_expr.kind
212+
&& let filter_body = cx.tcx.hir().body(closure.body)
159213
&& let [filter_params] = filter_body.params
160-
&& let Some(sugg) = match filter_params.pat.kind {
161-
hir::PatKind::Binding(_, _, filter_param_ident, None) => Some(format!(
162-
"{}.retain(|{filter_param_ident}| {})",
163-
snippet(cx, left_expr.span, ".."),
164-
snippet(cx, filter_body.value.span, "..")
165-
)),
166-
hir::PatKind::Tuple([key_pat, value_pat], _) => make_sugg(cx, key_pat, value_pat, left_expr, filter_body),
167-
hir::PatKind::Ref(pat, _) => match pat.kind {
168-
hir::PatKind::Binding(_, _, filter_param_ident, None) => Some(format!(
169-
"{}.retain(|{filter_param_ident}| {})",
214+
{
215+
if let hir::PatKind::Ref(pat, _) = filter_params.pat.kind {
216+
make_span_lint_and_sugg(
217+
cx,
218+
parent_expr_span,
219+
format!(
220+
"{}.retain(|{}| {})",
170221
snippet(cx, left_expr.span, ".."),
222+
snippet(cx, pat.span, ".."),
171223
snippet(cx, filter_body.value.span, "..")
172-
)),
173-
_ => None,
174-
},
175-
_ => None,
224+
),
225+
);
176226
}
177-
{
178-
span_lint_and_sugg(
179-
cx,
180-
MANUAL_RETAIN,
181-
parent_expr.span,
182-
"this expression can be written more simply using `.retain()`",
183-
"consider calling `.retain()` instead",
184-
sugg,
185-
Applicability::MachineApplicable,
186-
);
227+
// Be conservative now. Do nothing for the `Binding` case.
228+
// TODO: Ideally, we can rewrite the lambda by stripping one level of reference
187229
}
188230
}
189231

@@ -229,3 +271,20 @@ fn match_acceptable_type(cx: &LateContext<'_>, expr: &hir::Expr<'_>, msrv: &Msrv
229271
&& acceptable_msrv.map_or(true, |acceptable_msrv| msrv.meets(acceptable_msrv))
230272
})
231273
}
274+
275+
fn match_map_type(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool {
276+
let expr_ty = cx.typeck_results().expr_ty(expr).peel_refs();
277+
MAP_TYPES.iter().any(|ty| is_type_diagnostic_item(cx, expr_ty, *ty))
278+
}
279+
280+
fn make_span_lint_and_sugg(cx: &LateContext<'_>, span: Span, sugg: String) {
281+
span_lint_and_sugg(
282+
cx,
283+
MANUAL_RETAIN,
284+
span,
285+
"this expression can be written more simply using `.retain()`",
286+
"consider calling `.retain()` instead",
287+
sugg,
288+
Applicability::MachineApplicable,
289+
);
290+
}

tests/ui/manual_retain.fixed

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ fn main() {
1414
_msrv_153();
1515
_msrv_126();
1616
_msrv_118();
17+
18+
issue_10393();
19+
issue_12081();
1720
}
1821

1922
fn binary_heap_retain() {
@@ -23,6 +26,11 @@ fn binary_heap_retain() {
2326
binary_heap.retain(|x| x % 2 == 0);
2427
binary_heap.retain(|x| x % 2 == 0);
2528

29+
// Do lint, because we use pattern matching
30+
let mut tuples = BinaryHeap::from([(0, 1), (1, 2), (2, 3)]);
31+
tuples.retain(|(ref x, ref y)| *x == 0);
32+
tuples.retain(|(x, y)| *x == 0);
33+
2634
// Do not lint, because type conversion is performed
2735
binary_heap = binary_heap
2836
.into_iter()
@@ -55,6 +63,9 @@ fn btree_map_retain() {
5563
btree_map.retain(|_, &mut v| v % 2 == 0);
5664
btree_map.retain(|k, &mut v| (k % 2 == 0) && (v % 2 == 0));
5765

66+
// Do not lint, because the parameters are not matched in tuple pattern
67+
btree_map = btree_map.into_iter().filter(|t| t.0 % 2 == 0).collect();
68+
5869
// Do not lint.
5970
btree_map = btree_map
6071
.into_iter()
@@ -76,6 +87,11 @@ fn btree_set_retain() {
7687
btree_set.retain(|x| x % 2 == 0);
7788
btree_set.retain(|x| x % 2 == 0);
7889

90+
// Do lint, because we use pattern matching
91+
let mut tuples = BTreeSet::from([(0, 1), (1, 2), (2, 3)]);
92+
tuples.retain(|(ref x, ref y)| *x == 0);
93+
tuples.retain(|(x, y)| *x == 0);
94+
7995
// Do not lint, because type conversion is performed
8096
btree_set = btree_set
8197
.iter()
@@ -108,6 +124,9 @@ fn hash_map_retain() {
108124
hash_map.retain(|_, &mut v| v % 2 == 0);
109125
hash_map.retain(|k, &mut v| (k % 2 == 0) && (v % 2 == 0));
110126

127+
// Do not lint, because the parameters are not matched in tuple pattern
128+
hash_map = hash_map.into_iter().filter(|t| t.0 % 2 == 0).collect();
129+
111130
// Do not lint.
112131
hash_map = hash_map
113132
.into_iter()
@@ -128,6 +147,11 @@ fn hash_set_retain() {
128147
hash_set.retain(|x| x % 2 == 0);
129148
hash_set.retain(|x| x % 2 == 0);
130149

150+
// Do lint, because we use pattern matching
151+
let mut tuples = HashSet::from([(0, 1), (1, 2), (2, 3)]);
152+
tuples.retain(|(ref x, ref y)| *x == 0);
153+
tuples.retain(|(x, y)| *x == 0);
154+
131155
// Do not lint, because type conversion is performed
132156
hash_set = hash_set.into_iter().filter(|x| x % 2 == 0).collect::<HashSet<i8>>();
133157
hash_set = hash_set
@@ -171,6 +195,11 @@ fn vec_retain() {
171195
vec.retain(|x| x % 2 == 0);
172196
vec.retain(|x| x % 2 == 0);
173197

198+
// Do lint, because we use pattern matching
199+
let mut tuples = vec![(0, 1), (1, 2), (2, 3)];
200+
tuples.retain(|(ref x, ref y)| *x == 0);
201+
tuples.retain(|(x, y)| *x == 0);
202+
174203
// Do not lint, because type conversion is performed
175204
vec = vec.into_iter().filter(|x| x % 2 == 0).collect::<Vec<i8>>();
176205
vec = vec.iter().filter(|&x| x % 2 == 0).copied().collect::<Vec<i8>>();
@@ -246,3 +275,37 @@ fn _msrv_118() {
246275
let mut hash_map: HashMap<i8, i8> = (0..8).map(|x| (x, x * 10)).collect();
247276
hash_map = hash_map.into_iter().filter(|(k, _)| k % 2 == 0).collect();
248277
}
278+
279+
fn issue_10393() {
280+
// Do lint
281+
let mut vec = vec![(0, 1), (1, 2), (2, 3)];
282+
vec.retain(|(x, y)| *x == 0);
283+
284+
// Do lint
285+
let mut tuples = vec![(true, -2), (false, 3)];
286+
tuples.retain(|(_, n)| *n > 0);
287+
}
288+
289+
fn issue_11457() {
290+
// Do not lint, as we need to modify the closure
291+
let mut vals = vec![1, 2, 3, 4];
292+
vals = vals.iter().filter(|v| **v != 1).cloned().collect();
293+
294+
// Do not lint, as we need to modify the closure
295+
let mut s = String::from("foobar");
296+
s = s.chars().filter(|c| *c != 'o').to_owned().collect();
297+
}
298+
299+
fn issue_12081() {
300+
let mut vec = vec![0, 1, 2];
301+
302+
// Do lint
303+
vec.retain(|&x| x == 0);
304+
vec.retain(|&x| x == 0);
305+
vec.retain(|&x| x == 0);
306+
307+
// Do lint
308+
vec.retain(|x| *x == 0);
309+
vec.retain(|x| *x == 0);
310+
vec.retain(|x| *x == 0);
311+
}

0 commit comments

Comments
 (0)