Skip to content

Commit 0b63e95

Browse files
committed
Auto merge of #10949 - y21:issue8010, r=Alexendoo
[`manual_filter_map`]: lint on `matches` and pattern matching Fixes #8010 Previously this lint only worked specifically for a very limited set of methods on the filter call (`.filter(|opt| opt.is_some())` and `.filter(|res| res.is_ok())`). This PR extends it to also recognize `matches!` in the `filter` and pattern matching with `if let` or `match` in the `map`. Example: ```rs enum Enum { A(i32), B, } let _ = [Enum::A(123), Enum::B].into_iter() .filter(|x| matches!(x, Enum::A(_))) .map(|x| if let Enum::A(s) = x { s } else { unreachable!() }); ``` Now suggests: ```diff - .filter(|x| matches!(x, Enum::A(_))).map(if let Enum::A(s) = x { s } else { unreachable!() }) + .filter_map(|x| match x { Enum::A(s) => Some(s), _ => None }) ``` Adding this required a somewhat large change in code because it originally seemed to be specifically written with only method calls in the filter in mind, and `matches!` has different behavior in the map, so this new setup should make it possible to support more "generic" cases that need different handling for the filter and map calls. changelog: [`manual_filter_map`]: lint on `matches` and pattern matching (and some internal refactoring)
2 parents 7a34143 + 648d1ae commit 0b63e95

File tree

6 files changed

+440
-59
lines changed

6 files changed

+440
-59
lines changed

clippy_lints/src/methods/filter_map.rs

+262-58
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use clippy_utils::diagnostics::span_lint_and_sugg;
1+
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
2+
use clippy_utils::macros::{is_panic, root_macro_call};
23
use clippy_utils::source::{indent_of, reindent_multiline, snippet};
34
use clippy_utils::ty::is_type_diagnostic_item;
4-
use clippy_utils::{is_trait_method, path_to_local_id, peel_blocks, SpanlessEq};
5+
use clippy_utils::{higher, is_trait_method, path_to_local_id, peel_blocks, SpanlessEq};
6+
use hir::{Body, HirId, MatchSource, Pat};
57
use if_chain::if_chain;
68
use rustc_errors::Applicability;
79
use rustc_hir as hir;
@@ -10,7 +12,7 @@ use rustc_hir::{Closure, Expr, ExprKind, PatKind, PathSegment, QPath, UnOp};
1012
use rustc_lint::LateContext;
1113
use rustc_middle::ty::adjustment::Adjust;
1214
use rustc_span::source_map::Span;
13-
use rustc_span::symbol::{sym, Symbol};
15+
use rustc_span::symbol::{sym, Ident, Symbol};
1416
use std::borrow::Cow;
1517

1618
use super::{MANUAL_FILTER_MAP, MANUAL_FIND_MAP, OPTION_FILTER_MAP};
@@ -48,6 +50,214 @@ fn is_option_filter_map(cx: &LateContext<'_>, filter_arg: &hir::Expr<'_>, map_ar
4850
is_method(cx, map_arg, sym::unwrap) && is_method(cx, filter_arg, sym!(is_some))
4951
}
5052

53+
#[derive(Debug, Copy, Clone)]
54+
enum OffendingFilterExpr<'tcx> {
55+
/// `.filter(|opt| opt.is_some())`
56+
IsSome {
57+
/// The receiver expression
58+
receiver: &'tcx Expr<'tcx>,
59+
/// If `Some`, then this contains the span of an expression that possibly contains side
60+
/// effects: `.filter(|opt| side_effect(opt).is_some())`
61+
/// ^^^^^^^^^^^^^^^^
62+
///
63+
/// We will use this later for warning the user that the suggested fix may change
64+
/// the behavior.
65+
side_effect_expr_span: Option<Span>,
66+
},
67+
/// `.filter(|res| res.is_ok())`
68+
IsOk {
69+
/// The receiver expression
70+
receiver: &'tcx Expr<'tcx>,
71+
/// See `IsSome`
72+
side_effect_expr_span: Option<Span>,
73+
},
74+
/// `.filter(|enum| matches!(enum, Enum::A(_)))`
75+
Matches {
76+
/// The DefId of the variant being matched
77+
variant_def_id: hir::def_id::DefId,
78+
},
79+
}
80+
81+
#[derive(Debug)]
82+
enum CalledMethod {
83+
OptionIsSome,
84+
ResultIsOk,
85+
}
86+
87+
/// The result of checking a `map` call, returned by `OffendingFilterExpr::check_map_call`
88+
#[derive(Debug)]
89+
enum CheckResult<'tcx> {
90+
Method {
91+
map_arg: &'tcx Expr<'tcx>,
92+
/// The method that was called inside of `filter`
93+
method: CalledMethod,
94+
/// See `OffendingFilterExpr::IsSome`
95+
side_effect_expr_span: Option<Span>,
96+
},
97+
PatternMatching {
98+
/// The span of the variant being matched
99+
/// if let Some(s) = enum
100+
/// ^^^^^^^
101+
variant_span: Span,
102+
/// if let Some(s) = enum
103+
/// ^
104+
variant_ident: Ident,
105+
},
106+
}
107+
108+
impl<'tcx> OffendingFilterExpr<'tcx> {
109+
pub fn check_map_call(
110+
&mut self,
111+
cx: &LateContext<'tcx>,
112+
map_body: &'tcx Body<'tcx>,
113+
map_param_id: HirId,
114+
filter_param_id: HirId,
115+
is_filter_param_ref: bool,
116+
) -> Option<CheckResult<'tcx>> {
117+
match *self {
118+
OffendingFilterExpr::IsSome {
119+
receiver,
120+
side_effect_expr_span,
121+
}
122+
| OffendingFilterExpr::IsOk {
123+
receiver,
124+
side_effect_expr_span,
125+
} => {
126+
// check if closure ends with expect() or unwrap()
127+
if let ExprKind::MethodCall(seg, map_arg, ..) = map_body.value.kind
128+
&& matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or)
129+
// .map(|y| f(y).copied().unwrap())
130+
// ~~~~
131+
&& let map_arg_peeled = match map_arg.kind {
132+
ExprKind::MethodCall(method, original_arg, [], _) if acceptable_methods(method) => {
133+
original_arg
134+
},
135+
_ => map_arg,
136+
}
137+
// .map(|y| y[.acceptable_method()].unwrap())
138+
&& let simple_equal = (path_to_local_id(receiver, filter_param_id)
139+
&& path_to_local_id(map_arg_peeled, map_param_id))
140+
&& let eq_fallback = (|a: &Expr<'_>, b: &Expr<'_>| {
141+
// in `filter(|x| ..)`, replace `*x` with `x`
142+
let a_path = if_chain! {
143+
if !is_filter_param_ref;
144+
if let ExprKind::Unary(UnOp::Deref, expr_path) = a.kind;
145+
then { expr_path } else { a }
146+
};
147+
// let the filter closure arg and the map closure arg be equal
148+
path_to_local_id(a_path, filter_param_id)
149+
&& path_to_local_id(b, map_param_id)
150+
&& cx.typeck_results().expr_ty_adjusted(a) == cx.typeck_results().expr_ty_adjusted(b)
151+
})
152+
&& (simple_equal
153+
|| SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(receiver, map_arg_peeled))
154+
{
155+
Some(CheckResult::Method {
156+
map_arg,
157+
side_effect_expr_span,
158+
method: match self {
159+
OffendingFilterExpr::IsSome { .. } => CalledMethod::OptionIsSome,
160+
OffendingFilterExpr::IsOk { .. } => CalledMethod::ResultIsOk,
161+
OffendingFilterExpr::Matches { .. } => unreachable!("only IsSome and IsOk can get here"),
162+
}
163+
})
164+
} else {
165+
None
166+
}
167+
},
168+
OffendingFilterExpr::Matches { variant_def_id } => {
169+
let expr_uses_local = |pat: &Pat<'_>, expr: &Expr<'_>| {
170+
if let PatKind::TupleStruct(QPath::Resolved(_, path), [subpat], _) = pat.kind
171+
&& let PatKind::Binding(_, local_id, ident, _) = subpat.kind
172+
&& path_to_local_id(expr.peel_blocks(), local_id)
173+
&& let Some(local_variant_def_id) = path.res.opt_def_id()
174+
&& local_variant_def_id == variant_def_id
175+
{
176+
Some((ident, pat.span))
177+
} else {
178+
None
179+
}
180+
};
181+
182+
// look for:
183+
// `if let Variant (v) = enum { v } else { unreachable!() }`
184+
// ^^^^^^^ ^ ^^^^ ^^^^^^^^^^^^^^^^^^
185+
// variant_span variant_ident scrutinee else_ (blocks peeled later)
186+
// OR
187+
// `match enum { Variant (v) => v, _ => unreachable!() }`
188+
// ^^^^ ^^^^^^^ ^ ^^^^^^^^^^^^^^
189+
// scrutinee variant_span variant_ident else_
190+
let (scrutinee, else_, variant_ident, variant_span) =
191+
match higher::IfLetOrMatch::parse(cx, map_body.value) {
192+
// For `if let` we want to check that the variant matching arm references the local created by its pattern
193+
Some(higher::IfLetOrMatch::IfLet(sc, pat, then, Some(else_)))
194+
if let Some((ident, span)) = expr_uses_local(pat, then) =>
195+
{
196+
(sc, else_, ident, span)
197+
},
198+
// For `match` we want to check that the "else" arm is the wildcard (`_`) pattern
199+
// and that the variant matching arm references the local created by its pattern
200+
Some(higher::IfLetOrMatch::Match(sc, [arm, wild_arm], MatchSource::Normal))
201+
if let PatKind::Wild = wild_arm.pat.kind
202+
&& let Some((ident, span)) = expr_uses_local(arm.pat, arm.body.peel_blocks()) =>
203+
{
204+
(sc, wild_arm.body, ident, span)
205+
},
206+
_ => return None,
207+
};
208+
209+
if path_to_local_id(scrutinee, map_param_id)
210+
// else branch should be a `panic!` or `unreachable!` macro call
211+
&& let Some(mac) = root_macro_call(else_.peel_blocks().span)
212+
&& (is_panic(cx, mac.def_id) || cx.tcx.opt_item_name(mac.def_id) == Some(sym::unreachable))
213+
{
214+
Some(CheckResult::PatternMatching { variant_span, variant_ident })
215+
} else {
216+
None
217+
}
218+
},
219+
}
220+
}
221+
222+
fn hir(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, filter_param_id: HirId) -> Option<Self> {
223+
if let ExprKind::MethodCall(path, receiver, [], _) = expr.kind
224+
&& let Some(recv_ty) = cx.typeck_results().expr_ty(receiver).peel_refs().ty_adt_def()
225+
{
226+
// we still want to lint if the expression possibly contains side effects,
227+
// *but* it can't be machine-applicable then, because that can change the behavior of the program:
228+
// .filter(|x| effect(x).is_some()).map(|x| effect(x).unwrap())
229+
// vs.
230+
// .filter_map(|x| effect(x))
231+
//
232+
// the latter only calls `effect` once
233+
let side_effect_expr_span = receiver.can_have_side_effects().then_some(receiver.span);
234+
235+
if cx.tcx.is_diagnostic_item(sym::Option, recv_ty.did())
236+
&& path.ident.name == sym!(is_some)
237+
{
238+
Some(Self::IsSome { receiver, side_effect_expr_span })
239+
} else if cx.tcx.is_diagnostic_item(sym::Result, recv_ty.did())
240+
&& path.ident.name == sym!(is_ok)
241+
{
242+
Some(Self::IsOk { receiver, side_effect_expr_span })
243+
} else {
244+
None
245+
}
246+
} else if let Some(macro_call) = root_macro_call(expr.span)
247+
&& cx.tcx.get_diagnostic_name(macro_call.def_id) == Some(sym::matches_macro)
248+
// we know for a fact that the wildcard pattern is the second arm
249+
&& let ExprKind::Match(scrutinee, [arm, _], _) = expr.kind
250+
&& path_to_local_id(scrutinee, filter_param_id)
251+
&& let PatKind::TupleStruct(QPath::Resolved(_, path), ..) = arm.pat.kind
252+
&& let Some(variant_def_id) = path.res.opt_def_id()
253+
{
254+
Some(OffendingFilterExpr::Matches { variant_def_id })
255+
} else {
256+
None
257+
}
258+
}
259+
}
260+
51261
/// is `filter(|x| x.is_some()).map(|x| x.unwrap())`
52262
fn is_filter_some_map_unwrap(
53263
cx: &LateContext<'_>,
@@ -102,55 +312,18 @@ pub(super) fn check(
102312
} else {
103313
(filter_param.pat, false)
104314
};
105-
// closure ends with is_some() or is_ok()
315+
106316
if let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind;
107-
if let ExprKind::MethodCall(path, filter_arg, [], _) = filter_body.value.kind;
108-
if let Some(opt_ty) = cx.typeck_results().expr_ty(filter_arg).peel_refs().ty_adt_def();
109-
if let Some(is_result) = if cx.tcx.is_diagnostic_item(sym::Option, opt_ty.did()) {
110-
Some(false)
111-
} else if cx.tcx.is_diagnostic_item(sym::Result, opt_ty.did()) {
112-
Some(true)
113-
} else {
114-
None
115-
};
116-
if path.ident.name.as_str() == if is_result { "is_ok" } else { "is_some" };
317+
if let Some(mut offending_expr) = OffendingFilterExpr::hir(cx, filter_body.value, filter_param_id);
117318

118-
// ...map(|x| ...unwrap())
119319
if let ExprKind::Closure(&Closure { body: map_body_id, .. }) = map_arg.kind;
120320
let map_body = cx.tcx.hir().body(map_body_id);
121321
if let [map_param] = map_body.params;
122322
if let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind;
123-
// closure ends with expect() or unwrap()
124-
if let ExprKind::MethodCall(seg, map_arg, ..) = map_body.value.kind;
125-
if matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or);
126-
127-
// .filter(..).map(|y| f(y).copied().unwrap())
128-
// ~~~~
129-
let map_arg_peeled = match map_arg.kind {
130-
ExprKind::MethodCall(method, original_arg, [], _) if acceptable_methods(method) => {
131-
original_arg
132-
},
133-
_ => map_arg,
134-
};
135323

136-
// .filter(|x| x.is_some()).map(|y| y[.acceptable_method()].unwrap())
137-
let simple_equal = path_to_local_id(filter_arg, filter_param_id)
138-
&& path_to_local_id(map_arg_peeled, map_param_id);
324+
if let Some(check_result) =
325+
offending_expr.check_map_call(cx, map_body, map_param_id, filter_param_id, is_filter_param_ref);
139326

140-
let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| {
141-
// in `filter(|x| ..)`, replace `*x` with `x`
142-
let a_path = if_chain! {
143-
if !is_filter_param_ref;
144-
if let ExprKind::Unary(UnOp::Deref, expr_path) = a.kind;
145-
then { expr_path } else { a }
146-
};
147-
// let the filter closure arg and the map closure arg be equal
148-
path_to_local_id(a_path, filter_param_id)
149-
&& path_to_local_id(b, map_param_id)
150-
&& cx.typeck_results().expr_ty_adjusted(a) == cx.typeck_results().expr_ty_adjusted(b)
151-
};
152-
153-
if simple_equal || SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg_peeled);
154327
then {
155328
let span = filter_span.with_hi(expr.span.hi());
156329
let (filter_name, lint) = if is_find {
@@ -159,22 +332,53 @@ pub(super) fn check(
159332
("filter", MANUAL_FILTER_MAP)
160333
};
161334
let msg = format!("`{filter_name}(..).map(..)` can be simplified as `{filter_name}_map(..)`");
162-
let (to_opt, deref) = if is_result {
163-
(".ok()", String::new())
164-
} else {
165-
let derefs = cx.typeck_results()
166-
.expr_adjustments(map_arg)
167-
.iter()
168-
.filter(|adj| matches!(adj.kind, Adjust::Deref(_)))
169-
.count();
170335

171-
("", "*".repeat(derefs))
336+
let (sugg, note_and_span, applicability) = match check_result {
337+
CheckResult::Method { map_arg, method, side_effect_expr_span } => {
338+
let (to_opt, deref) = match method {
339+
CalledMethod::ResultIsOk => (".ok()", String::new()),
340+
CalledMethod::OptionIsSome => {
341+
let derefs = cx.typeck_results()
342+
.expr_adjustments(map_arg)
343+
.iter()
344+
.filter(|adj| matches!(adj.kind, Adjust::Deref(_)))
345+
.count();
346+
347+
("", "*".repeat(derefs))
348+
}
349+
};
350+
351+
let sugg = format!(
352+
"{filter_name}_map(|{map_param_ident}| {deref}{}{to_opt})",
353+
snippet(cx, map_arg.span, ".."),
354+
);
355+
let (note_and_span, applicability) = if let Some(span) = side_effect_expr_span {
356+
let note = "the suggestion might change the behavior of the program when merging `filter` and `map`, \
357+
because this expression potentially contains side effects and will only execute once";
358+
359+
(Some((note, span)), Applicability::MaybeIncorrect)
360+
} else {
361+
(None, Applicability::MachineApplicable)
362+
};
363+
364+
(sugg, note_and_span, applicability)
365+
}
366+
CheckResult::PatternMatching { variant_span, variant_ident } => {
367+
let pat = snippet(cx, variant_span, "<pattern>");
368+
369+
(format!("{filter_name}_map(|{map_param_ident}| match {map_param_ident} {{ \
370+
{pat} => Some({variant_ident}), \
371+
_ => None \
372+
}})"), None, Applicability::MachineApplicable)
373+
}
172374
};
173-
let sugg = format!(
174-
"{filter_name}_map(|{map_param_ident}| {deref}{}{to_opt})",
175-
snippet(cx, map_arg.span, ".."),
176-
);
177-
span_lint_and_sugg(cx, lint, span, &msg, "try", sugg, Applicability::MachineApplicable);
375+
span_lint_and_then(cx, lint, span, &msg, |diag| {
376+
diag.span_suggestion(span, "try", sugg, applicability);
377+
378+
if let Some((note, span)) = note_and_span {
379+
diag.span_note(span, note);
380+
}
381+
});
178382
}
179383
}
180384
}

clippy_utils/src/higher.rs

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ impl<'hir> IfLet<'hir> {
138138
}
139139

140140
/// An `if let` or `match` expression. Useful for lints that trigger on one or the other.
141+
#[derive(Debug)]
141142
pub enum IfLetOrMatch<'hir> {
142143
/// Any `match` expression
143144
Match(&'hir Expr<'hir>, &'hir [Arm<'hir>], MatchSource),

tests/ui/manual_filter_map.fixed

+24
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,27 @@ fn issue_8920() {
120120
.iter()
121121
.filter_map(|f| f.result_field.to_owned().ok());
122122
}
123+
124+
fn issue8010() {
125+
#[derive(Clone)]
126+
enum Enum {
127+
A(i32),
128+
B,
129+
}
130+
131+
let iter = [Enum::A(123), Enum::B].into_iter();
132+
133+
let _x = iter.clone().filter_map(|x| match x { Enum::A(s) => Some(s), _ => None });
134+
let _x = iter.clone().filter(|x| matches!(x, Enum::B)).map(|x| match x {
135+
Enum::A(s) => s,
136+
_ => unreachable!(),
137+
});
138+
let _x = iter
139+
.clone()
140+
.filter_map(|x| match x { Enum::A(s) => Some(s), _ => None });
141+
#[allow(clippy::unused_unit)]
142+
let _x = iter
143+
.clone()
144+
.filter(|x| matches!(x, Enum::B))
145+
.map(|x| if let Enum::B = x { () } else { unreachable!() });
146+
}

0 commit comments

Comments
 (0)