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} ;
23use clippy_utils:: source:: { indent_of, reindent_multiline, snippet} ;
34use 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 } ;
57use if_chain:: if_chain;
68use rustc_errors:: Applicability ;
79use rustc_hir as hir;
@@ -10,7 +12,7 @@ use rustc_hir::{Closure, Expr, ExprKind, PatKind, PathSegment, QPath, UnOp};
1012use rustc_lint:: LateContext ;
1113use rustc_middle:: ty:: adjustment:: Adjust ;
1214use rustc_span:: source_map:: Span ;
13- use rustc_span:: symbol:: { sym, Symbol } ;
15+ use rustc_span:: symbol:: { sym, Ident , Symbol } ;
1416use std:: borrow:: Cow ;
1517
1618use 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())`
52262fn 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}
0 commit comments