1+ //! Lint a `match` or `if let .. { .. } else { .. }` expr that could be replaced by `matches!`
2+
13use super :: REDUNDANT_PATTERN_MATCHING ;
24use clippy_utils:: diagnostics:: span_lint_and_sugg;
35use clippy_utils:: source:: snippet_with_applicability;
46use clippy_utils:: { is_lint_allowed, is_wild, span_contains_comment} ;
57use rustc_ast:: LitKind ;
68use rustc_errors:: Applicability ;
7- use rustc_hir:: { Arm , Attribute , BorrowKind , Expr , ExprKind , Pat , PatKind , QPath } ;
9+ use rustc_hir:: { Arm , BorrowKind , Expr , ExprKind , Pat , PatKind , QPath } ;
810use rustc_lint:: { LateContext , LintContext } ;
911use rustc_middle:: ty;
1012use rustc_span:: source_map:: Spanned ;
1113
1214use super :: MATCH_LIKE_MATCHES_MACRO ;
1315
14- /// Lint a `match` or `if let .. { .. } else { .. }` expr that could be replaced by `matches!`
1516pub ( crate ) fn check_if_let < ' tcx > (
1617 cx : & LateContext < ' tcx > ,
1718 expr : & ' tcx Expr < ' _ > ,
@@ -20,16 +21,42 @@ pub(crate) fn check_if_let<'tcx>(
2021 then_expr : & ' tcx Expr < ' _ > ,
2122 else_expr : & ' tcx Expr < ' _ > ,
2223) {
23- find_matches_sugg (
24- cx,
25- let_expr,
26- IntoIterator :: into_iter ( [
27- ( & [ ] [ ..] , Some ( let_pat) , then_expr, None ) ,
28- ( & [ ] [ ..] , None , else_expr, None ) ,
29- ] ) ,
30- expr,
31- true ,
32- ) ;
24+ if !span_contains_comment ( cx. sess ( ) . source_map ( ) , expr. span )
25+ && cx. typeck_results ( ) . expr_ty ( expr) . is_bool ( )
26+ && let Some ( b0) = find_bool_lit ( then_expr)
27+ && let Some ( b1) = find_bool_lit ( else_expr)
28+ && b0 != b1
29+ {
30+ if !is_lint_allowed ( cx, REDUNDANT_PATTERN_MATCHING , let_pat. hir_id ) && is_some_wild ( let_pat. kind ) {
31+ return ;
32+ }
33+
34+ // The suggestion may be incorrect, because some arms can have `cfg` attributes
35+ // evaluated into `false` and so such arms will be stripped before.
36+ let mut applicability = Applicability :: MaybeIncorrect ;
37+ let pat = snippet_with_applicability ( cx, let_pat. span , ".." , & mut applicability) ;
38+
39+ // strip potential borrows (#6503), but only if the type is a reference
40+ let mut ex_new = let_expr;
41+ if let ExprKind :: AddrOf ( BorrowKind :: Ref , .., ex_inner) = let_expr. kind
42+ && let ty:: Ref ( ..) = cx. typeck_results ( ) . expr_ty ( ex_inner) . kind ( )
43+ {
44+ ex_new = ex_inner;
45+ }
46+ span_lint_and_sugg (
47+ cx,
48+ MATCH_LIKE_MATCHES_MACRO ,
49+ expr. span ,
50+ "if let .. else expression looks like `matches!` macro" ,
51+ "try" ,
52+ format ! (
53+ "{}matches!({}, {pat})" ,
54+ if b0 { "" } else { "!" } ,
55+ snippet_with_applicability( cx, ex_new. span, ".." , & mut applicability) ,
56+ ) ,
57+ applicability,
58+ ) ;
59+ }
3360}
3461
3562pub ( super ) fn check_match < ' tcx > (
@@ -38,55 +65,80 @@ pub(super) fn check_match<'tcx>(
3865 scrutinee : & ' tcx Expr < ' _ > ,
3966 arms : & ' tcx [ Arm < ' tcx > ] ,
4067) -> bool {
41- find_matches_sugg (
42- cx,
43- scrutinee,
44- arms. iter ( )
45- . map ( |arm| ( cx. tcx . hir_attrs ( arm. hir_id ) , Some ( arm. pat ) , arm. body , arm. guard ) ) ,
46- e,
47- false ,
48- )
49- }
50-
51- /// Lint a `match` or `if let` for replacement by `matches!`
52- fn find_matches_sugg < ' a , ' b , I > (
53- cx : & LateContext < ' _ > ,
54- ex : & Expr < ' _ > ,
55- mut iter : I ,
56- expr : & Expr < ' _ > ,
57- is_if_let : bool ,
58- ) -> bool
59- where
60- ' b : ' a ,
61- I : Clone
62- + DoubleEndedIterator
63- + ExactSizeIterator
64- + Iterator < Item = ( & ' a [ Attribute ] , Option < & ' a Pat < ' b > > , & ' a Expr < ' b > , Option < & ' a Expr < ' b > > ) > ,
65- {
66- if !span_contains_comment ( cx. sess ( ) . source_map ( ) , expr. span )
67- && iter. len ( ) >= 2
68- && cx. typeck_results ( ) . expr_ty ( expr) . is_bool ( )
69- && let Some ( ( _, last_pat_opt, last_expr, _) ) = iter. next_back ( )
70- && let iter_without_last = iter. clone ( )
71- && let Some ( ( first_attrs, _, first_expr, first_guard) ) = iter. next ( )
72- && let Some ( b0) = find_bool_lit ( & first_expr. kind )
73- && let Some ( b1) = find_bool_lit ( & last_expr. kind )
68+ if let Some ( ( last_arm, arms_without_last) ) = arms. split_last ( )
69+ && let Some ( ( first_arm, middle_arms) ) = arms_without_last. split_first ( )
70+ && !span_contains_comment ( cx. sess ( ) . source_map ( ) , e. span )
71+ && cx. typeck_results ( ) . expr_ty ( e) . is_bool ( )
72+ && let Some ( b0) = find_bool_lit ( first_arm. body )
73+ && let Some ( b1) = find_bool_lit ( last_arm. body )
7474 && b0 != b1
75- && ( first_guard. is_none ( ) || iter. len ( ) == 0 )
76- && first_attrs. is_empty ( )
77- && iter. all ( |arm| find_bool_lit ( & arm. 2 . kind ) . is_some_and ( |b| b == b0) && arm. 3 . is_none ( ) && arm. 0 . is_empty ( ) )
75+ // We handle two cases:
76+ && (
77+ // - There are no middle arms, i.e., 2 arms in total
78+ //
79+ // In that case, the first arm may or may not have a guard, because this:
80+ // ```rs
81+ // match e {
82+ // Either::Left $(if $guard)|+ => true, // or `false`, but then we'll need `!matches!(..)`
83+ // _ => false,
84+ // }
85+ // ```
86+ // can always become this:
87+ // ```rs
88+ // matches!(e, Either::Left $(if $guard)|+)
89+ // ```
90+ middle_arms. is_empty ( )
91+
92+ // - (added in #6216) There are middle arms
93+ //
94+ // In that case, neither they nor the first arm may have guards
95+ // -- otherwise, they couldn't be combined into an or-pattern in `matches!`
96+ //
97+ // This:
98+ // ```rs
99+ // match e {
100+ // Either3::First => true,
101+ // Either3::Second => true,
102+ // _ /* matches `Either3::Third` */ => false,
103+ // }
104+ // ```
105+ // can become this:
106+ // ```rs
107+ // matches!(e, Either3::First | Either3::Second)
108+ // ```
109+ //
110+ // But this:
111+ // ```rs
112+ // match e {
113+ // Either3::First if X => true,
114+ // Either3::Second => true,
115+ // _ => false,
116+ // }
117+ // ```
118+ // cannot be transformed.
119+ //
120+ // We set an additional constraint of all of them needing to return the same bool,
121+ // so we don't lint things like:
122+ // ```rs
123+ // match e {
124+ // Either3::First => true,
125+ // Either3::Second => false,
126+ // _ => false,
127+ // }
128+ // ```
129+ // This is not *strictly* necessary, but it simplifies the logic a bit
130+ || arms_without_last. iter ( ) . all ( |arm| {
131+ cx. tcx . hir_attrs ( arm. hir_id ) . is_empty ( ) && arm. guard . is_none ( ) && find_bool_lit ( arm. body ) == Some ( b0)
132+ } )
133+ )
78134 {
79- if let Some ( last_pat) = last_pat_opt
80- && !is_wild ( last_pat)
81- {
135+ if !is_wild ( last_arm. pat ) {
82136 return false ;
83137 }
84138
85- for arm in iter_without_last. clone ( ) {
86- if let Some ( pat) = arm. 1
87- && !is_lint_allowed ( cx, REDUNDANT_PATTERN_MATCHING , pat. hir_id )
88- && is_some ( pat. kind )
89- {
139+ for arm in arms_without_last {
140+ let pat = arm. pat ;
141+ if !is_lint_allowed ( cx, REDUNDANT_PATTERN_MATCHING , pat. hir_id ) && is_some_wild ( pat. kind ) {
90142 return false ;
91143 }
92144 }
@@ -96,14 +148,12 @@ where
96148 let mut applicability = Applicability :: MaybeIncorrect ;
97149 let pat = {
98150 use itertools:: Itertools as _;
99- iter_without_last
100- . filter_map ( |arm| {
101- let pat_span = arm. 1 ?. span ;
102- Some ( snippet_with_applicability ( cx, pat_span, ".." , & mut applicability) )
103- } )
151+ arms_without_last
152+ . iter ( )
153+ . map ( |arm| snippet_with_applicability ( cx, arm. pat . span , ".." , & mut applicability) )
104154 . join ( " | " )
105155 } ;
106- let pat_and_guard = if let Some ( g) = first_guard {
156+ let pat_and_guard = if let Some ( g) = first_arm . guard {
107157 format ! (
108158 "{pat} if {}" ,
109159 snippet_with_applicability( cx, g. span, ".." , & mut applicability)
@@ -113,20 +163,17 @@ where
113163 } ;
114164
115165 // strip potential borrows (#6503), but only if the type is a reference
116- let mut ex_new = ex ;
117- if let ExprKind :: AddrOf ( BorrowKind :: Ref , .., ex_inner) = ex . kind
166+ let mut ex_new = scrutinee ;
167+ if let ExprKind :: AddrOf ( BorrowKind :: Ref , .., ex_inner) = scrutinee . kind
118168 && let ty:: Ref ( ..) = cx. typeck_results ( ) . expr_ty ( ex_inner) . kind ( )
119169 {
120170 ex_new = ex_inner;
121171 }
122172 span_lint_and_sugg (
123173 cx,
124174 MATCH_LIKE_MATCHES_MACRO ,
125- expr. span ,
126- format ! (
127- "{} expression looks like `matches!` macro" ,
128- if is_if_let { "if let .. else" } else { "match" }
129- ) ,
175+ e. span ,
176+ "match expression looks like `matches!` macro" ,
130177 "try" ,
131178 format ! (
132179 "{}matches!({}, {pat_and_guard})" ,
@@ -142,11 +189,11 @@ where
142189}
143190
144191/// Extract a `bool` or `{ bool }`
145- fn find_bool_lit ( ex : & ExprKind < ' _ > ) -> Option < bool > {
146- match ex {
192+ fn find_bool_lit ( ex : & Expr < ' _ > ) -> Option < bool > {
193+ match ex. kind {
147194 ExprKind :: Lit ( Spanned {
148195 node : LitKind :: Bool ( b) , ..
149- } ) => Some ( * b) ,
196+ } ) => Some ( b) ,
150197 ExprKind :: Block (
151198 rustc_hir:: Block {
152199 stmts : [ ] ,
@@ -168,8 +215,9 @@ fn find_bool_lit(ex: &ExprKind<'_>) -> Option<bool> {
168215 }
169216}
170217
171- fn is_some ( path_kind : PatKind < ' _ > ) -> bool {
172- match path_kind {
218+ /// Checks whether a pattern is `Some(_)`
219+ fn is_some_wild ( pat_kind : PatKind < ' _ > ) -> bool {
220+ match pat_kind {
173221 PatKind :: TupleStruct ( QPath :: Resolved ( _, path) , [ first, ..] , _) if is_wild ( first) => {
174222 let name = path. segments [ 0 ] . ident ;
175223 name. name == rustc_span:: sym:: Some
0 commit comments