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