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};
2
3
use clippy_utils::source::{indent_of, reindent_multiline, snippet};
3
4
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};
5
7
use if_chain::if_chain;
6
8
use rustc_errors::Applicability;
7
9
use rustc_hir as hir;
@@ -10,7 +12,7 @@ use rustc_hir::{Closure, Expr, ExprKind, PatKind, PathSegment, QPath, UnOp};
10
12
use rustc_lint::LateContext;
11
13
use rustc_middle::ty::adjustment::Adjust;
12
14
use rustc_span::source_map::Span;
13
- use rustc_span::symbol::{sym, Symbol};
15
+ use rustc_span::symbol::{sym, Ident, Symbol};
14
16
use std::borrow::Cow;
15
17
16
18
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
48
50
is_method(cx, map_arg, sym::unwrap) && is_method(cx, filter_arg, sym!(is_some))
49
51
}
50
52
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
+
51
261
/// is `filter(|x| x.is_some()).map(|x| x.unwrap())`
52
262
fn is_filter_some_map_unwrap(
53
263
cx: &LateContext<'_>,
@@ -102,55 +312,18 @@ pub(super) fn check(
102
312
} else {
103
313
(filter_param.pat, false)
104
314
};
105
- // closure ends with is_some() or is_ok()
315
+
106
316
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);
117
318
118
- // ...map(|x| ...unwrap())
119
319
if let ExprKind::Closure(&Closure { body: map_body_id, .. }) = map_arg.kind;
120
320
let map_body = cx.tcx.hir().body(map_body_id);
121
321
if let [map_param] = map_body.params;
122
322
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
- };
135
323
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);
139
326
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);
154
327
then {
155
328
let span = filter_span.with_hi(expr.span.hi());
156
329
let (filter_name, lint) = if is_find {
@@ -159,22 +332,53 @@ pub(super) fn check(
159
332
("filter", MANUAL_FILTER_MAP)
160
333
};
161
334
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();
170
335
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
+ }
172
374
};
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
+ });
178
382
}
179
383
}
180
384
}
0 commit comments