Skip to content

Commit 7a92626

Browse files
authored
fix: filter_map_bool_then suggest wrongly when the closure cannot be decompose directly (#14370)
Closes #11617 Closes #14368 Clippy gives wrong suggestions when the filter and then cannot be put into closure directly. Since trying to transform these can be too complicated, Clippy will simply warn but don't try to fix. changelog: [`filter_map_bool_then`]: fix wrong suggestions when the closure cannot be decompose directly
2 parents 90f5f55 + 3f9ecbe commit 7a92626

File tree

3 files changed

+194
-10
lines changed

3 files changed

+194
-10
lines changed

clippy_lints/src/methods/filter_map_bool_then.rs

+66-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
use super::FILTER_MAP_BOOL_THEN;
2-
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::diagnostics::span_lint_and_then;
33
use clippy_utils::source::SpanRangeExt;
44
use clippy_utils::ty::is_copy;
5-
use clippy_utils::{is_from_proc_macro, is_trait_method, peel_blocks};
5+
use clippy_utils::{
6+
CaptureKind, can_move_expr_to_closure, contains_return, is_from_proc_macro, is_trait_method, peel_blocks,
7+
};
8+
use rustc_ast::Mutability;
9+
use rustc_data_structures::fx::FxHashSet;
610
use rustc_errors::Applicability;
7-
use rustc_hir::{Expr, ExprKind};
11+
use rustc_hir::{Expr, ExprKind, HirId, Param, Pat};
812
use rustc_lint::{LateContext, LintContext};
913
use rustc_middle::ty::Binder;
1014
use rustc_middle::ty::adjustment::Adjust;
@@ -44,17 +48,69 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, arg: &
4448
&& let Some(filter) = recv.span.get_source_text(cx)
4549
&& let Some(map) = then_body.span.get_source_text(cx)
4650
{
47-
span_lint_and_sugg(
51+
span_lint_and_then(
4852
cx,
4953
FILTER_MAP_BOOL_THEN,
5054
call_span,
5155
"usage of `bool::then` in `filter_map`",
52-
"use `filter` then `map` instead",
53-
format!(
54-
"filter(|&{param_snippet}| {derefs}{filter}).map(|{param_snippet}| {map})",
55-
derefs = "*".repeat(needed_derefs)
56-
),
57-
Applicability::MachineApplicable,
56+
|diag| {
57+
if can_filter_and_then_move_to_closure(cx, &param, recv, then_body) {
58+
diag.span_suggestion(
59+
call_span,
60+
"use `filter` then `map` instead",
61+
format!(
62+
"filter(|&{param_snippet}| {derefs}{filter}).map(|{param_snippet}| {map})",
63+
derefs = "*".repeat(needed_derefs)
64+
),
65+
Applicability::MachineApplicable,
66+
);
67+
} else {
68+
diag.help("consider using `filter` then `map` instead");
69+
}
70+
},
5871
);
5972
}
6073
}
74+
75+
/// Returns a set of all bindings found in the given pattern.
76+
fn find_bindings_from_pat(pat: &Pat<'_>) -> FxHashSet<HirId> {
77+
let mut bindings = FxHashSet::default();
78+
pat.walk(|p| {
79+
if let rustc_hir::PatKind::Binding(_, hir_id, _, _) = p.kind {
80+
bindings.insert(hir_id);
81+
}
82+
true
83+
});
84+
bindings
85+
}
86+
87+
/// Returns true if we can take a closure parameter and have it in both the `filter` function and
88+
/// the`map` function. This is not the case if:
89+
///
90+
/// - The `filter` would contain an early return,
91+
/// - `filter` and `then` contain captures, and any of those are &mut
92+
fn can_filter_and_then_move_to_closure<'tcx>(
93+
cx: &LateContext<'tcx>,
94+
param: &Param<'tcx>,
95+
filter: &'tcx Expr<'tcx>,
96+
then: &'tcx Expr<'tcx>,
97+
) -> bool {
98+
if contains_return(filter) {
99+
return false;
100+
}
101+
102+
let Some(filter_captures) = can_move_expr_to_closure(cx, filter) else {
103+
return true;
104+
};
105+
let Some(then_captures) = can_move_expr_to_closure(cx, then) else {
106+
return true;
107+
};
108+
109+
let param_bindings = find_bindings_from_pat(param.pat);
110+
filter_captures.iter().all(|(hir_id, filter_cap)| {
111+
param_bindings.contains(hir_id)
112+
|| !then_captures
113+
.get(hir_id)
114+
.is_some_and(|then_cap| matches!(*filter_cap | *then_cap, CaptureKind::Ref(Mutability::Mut)))
115+
})
116+
}
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#![allow(clippy::question_mark, unused)]
2+
#![warn(clippy::filter_map_bool_then)]
3+
//@no-rustfix
4+
5+
fn issue11617() {
6+
let mut x: Vec<usize> = vec![0; 10];
7+
let _ = (0..x.len()).zip(x.clone().iter()).filter_map(|(i, v)| {
8+
//~^ filter_map_bool_then
9+
(x[i] != *v).then(|| {
10+
x[i] = i;
11+
i
12+
})
13+
});
14+
}
15+
16+
mod issue14368 {
17+
18+
fn do_something(_: ()) -> bool {
19+
true
20+
}
21+
22+
fn option_with_early_return(x: &[Option<bool>]) {
23+
let _ = x.iter().filter_map(|&x| x?.then(|| do_something(())));
24+
//~^ filter_map_bool_then
25+
let _ = x
26+
.iter()
27+
.filter_map(|&x| if let Some(x) = x { x } else { return None }.then(|| do_something(())));
28+
//~^ filter_map_bool_then
29+
let _ = x.iter().filter_map(|&x| {
30+
//~^ filter_map_bool_then
31+
match x {
32+
Some(x) => x,
33+
None => return None,
34+
}
35+
.then(|| do_something(()))
36+
});
37+
}
38+
39+
#[derive(Copy, Clone)]
40+
enum Foo {
41+
One(bool),
42+
Two,
43+
Three(Option<i32>),
44+
}
45+
46+
fn nested_type_with_early_return(x: &[Foo]) {
47+
let _ = x.iter().filter_map(|&x| {
48+
//~^ filter_map_bool_then
49+
match x {
50+
Foo::One(x) => x,
51+
Foo::Two => return None,
52+
Foo::Three(inner) => {
53+
if inner? == 0 {
54+
return Some(false);
55+
} else {
56+
true
57+
}
58+
},
59+
}
60+
.then(|| do_something(()))
61+
});
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
error: usage of `bool::then` in `filter_map`
2+
--> tests/ui/filter_map_bool_then_unfixable.rs:7:48
3+
|
4+
LL | let _ = (0..x.len()).zip(x.clone().iter()).filter_map(|(i, v)| {
5+
| ________________________________________________^
6+
LL | |
7+
LL | | (x[i] != *v).then(|| {
8+
LL | | x[i] = i;
9+
LL | | i
10+
LL | | })
11+
LL | | });
12+
| |______^
13+
|
14+
= help: consider using `filter` then `map` instead
15+
= note: `-D clippy::filter-map-bool-then` implied by `-D warnings`
16+
= help: to override `-D warnings` add `#[allow(clippy::filter_map_bool_then)]`
17+
18+
error: usage of `bool::then` in `filter_map`
19+
--> tests/ui/filter_map_bool_then_unfixable.rs:23:26
20+
|
21+
LL | let _ = x.iter().filter_map(|&x| x?.then(|| do_something(())));
22+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
|
24+
= help: consider using `filter` then `map` instead
25+
26+
error: usage of `bool::then` in `filter_map`
27+
--> tests/ui/filter_map_bool_then_unfixable.rs:27:14
28+
|
29+
LL | .filter_map(|&x| if let Some(x) = x { x } else { return None }.then(|| do_something(())));
30+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31+
|
32+
= help: consider using `filter` then `map` instead
33+
34+
error: usage of `bool::then` in `filter_map`
35+
--> tests/ui/filter_map_bool_then_unfixable.rs:29:26
36+
|
37+
LL | let _ = x.iter().filter_map(|&x| {
38+
| __________________________^
39+
LL | |
40+
LL | | match x {
41+
LL | | Some(x) => x,
42+
... |
43+
LL | | .then(|| do_something(()))
44+
LL | | });
45+
| |__________^
46+
|
47+
= help: consider using `filter` then `map` instead
48+
49+
error: usage of `bool::then` in `filter_map`
50+
--> tests/ui/filter_map_bool_then_unfixable.rs:47:26
51+
|
52+
LL | let _ = x.iter().filter_map(|&x| {
53+
| __________________________^
54+
LL | |
55+
LL | | match x {
56+
LL | | Foo::One(x) => x,
57+
... |
58+
LL | | .then(|| do_something(()))
59+
LL | | });
60+
| |__________^
61+
|
62+
= help: consider using `filter` then `map` instead
63+
64+
error: aborting due to 5 previous errors
65+

0 commit comments

Comments
 (0)