]>
Commit | Line | Data |
---|---|---|
cdc7bbd5 XL |
1 | use clippy_utils::diagnostics::span_lint_and_then; |
2 | use clippy_utils::visitors::LocalUsedVisitor; | |
94222f64 | 3 | use clippy_utils::{higher, is_lang_ctor, is_unit_expr, path_to_local, peel_ref_operators, SpanlessEq}; |
f20569fa | 4 | use if_chain::if_chain; |
cdc7bbd5 | 5 | use rustc_hir::LangItem::OptionNone; |
94222f64 | 6 | use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, MatchSource, Pat, PatKind, StmtKind}; |
f20569fa | 7 | use rustc_lint::{LateContext, LateLintPass}; |
f20569fa XL |
8 | use rustc_session::{declare_lint_pass, declare_tool_lint}; |
9 | use rustc_span::{MultiSpan, Span}; | |
10 | ||
11 | declare_clippy_lint! { | |
94222f64 XL |
12 | /// ### What it does |
13 | /// Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together | |
f20569fa XL |
14 | /// without adding any branches. |
15 | /// | |
16 | /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only | |
17 | /// cases where merging would most likely make the code more readable. | |
18 | /// | |
94222f64 XL |
19 | /// ### Why is this bad? |
20 | /// It is unnecessarily verbose and complex. | |
f20569fa | 21 | /// |
94222f64 | 22 | /// ### Example |
f20569fa XL |
23 | /// ```rust |
24 | /// fn func(opt: Option<Result<u64, String>>) { | |
25 | /// let n = match opt { | |
26 | /// Some(n) => match n { | |
27 | /// Ok(n) => n, | |
28 | /// _ => return, | |
29 | /// } | |
30 | /// None => return, | |
31 | /// }; | |
32 | /// } | |
33 | /// ``` | |
34 | /// Use instead: | |
35 | /// ```rust | |
36 | /// fn func(opt: Option<Result<u64, String>>) { | |
37 | /// let n = match opt { | |
38 | /// Some(Ok(n)) => n, | |
39 | /// _ => return, | |
40 | /// }; | |
41 | /// } | |
42 | /// ``` | |
43 | pub COLLAPSIBLE_MATCH, | |
44 | style, | |
45 | "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together." | |
46 | } | |
47 | ||
48 | declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]); | |
49 | ||
50 | impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch { | |
51 | fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) { | |
94222f64 XL |
52 | match IfLetOrMatch::parse(cx, expr) { |
53 | Some(IfLetOrMatch::Match(_, arms, _)) => { | |
54 | if let Some(els_arm) = arms.iter().rfind(|arm| arm_is_wild_like(cx, arm)) { | |
55 | for arm in arms { | |
56 | check_arm(cx, true, arm.pat, arm.body, arm.guard.as_ref(), Some(els_arm.body)); | |
57 | } | |
f20569fa XL |
58 | } |
59 | } | |
94222f64 XL |
60 | Some(IfLetOrMatch::IfLet(_, pat, body, els)) => { |
61 | check_arm(cx, false, pat, body, None, els); | |
62 | } | |
63 | None => {} | |
f20569fa XL |
64 | } |
65 | } | |
66 | } | |
67 | ||
94222f64 XL |
68 | fn check_arm<'tcx>( |
69 | cx: &LateContext<'tcx>, | |
70 | outer_is_match: bool, | |
71 | outer_pat: &'tcx Pat<'tcx>, | |
72 | outer_then_body: &'tcx Expr<'tcx>, | |
73 | outer_guard: Option<&'tcx Guard<'tcx>>, | |
74 | outer_else_body: Option<&'tcx Expr<'tcx>> | |
75 | ) { | |
76 | let inner_expr = strip_singleton_blocks(outer_then_body); | |
f20569fa | 77 | if_chain! { |
94222f64 XL |
78 | if let Some(inner) = IfLetOrMatch::parse(cx, inner_expr); |
79 | if let Some((inner_scrutinee, inner_then_pat, inner_else_body)) = match inner { | |
80 | IfLetOrMatch::IfLet(scrutinee, pat, _, els) => Some((scrutinee, pat, els)), | |
81 | IfLetOrMatch::Match(scrutinee, arms, ..) => if_chain! { | |
82 | // if there are more than two arms, collapsing would be non-trivial | |
83 | if arms.len() == 2 && arms.iter().all(|a| a.guard.is_none()); | |
84 | // one of the arms must be "wild-like" | |
85 | if let Some(wild_idx) = arms.iter().rposition(|a| arm_is_wild_like(cx, a)); | |
86 | then { | |
87 | let (then, els) = (&arms[1 - wild_idx], &arms[wild_idx]); | |
88 | Some((scrutinee, then.pat, Some(els.body))) | |
89 | } else { | |
90 | None | |
91 | } | |
92 | }, | |
93 | }; | |
94 | if outer_pat.span.ctxt() == inner_scrutinee.span.ctxt(); | |
f20569fa XL |
95 | // match expression must be a local binding |
96 | // match <local> { .. } | |
94222f64 XL |
97 | if let Some(binding_id) = path_to_local(peel_ref_operators(cx, inner_scrutinee)); |
98 | if !pat_contains_or(inner_then_pat); | |
f20569fa XL |
99 | // the binding must come from the pattern of the containing match arm |
100 | // ..<local>.. => match <local> { .. } | |
94222f64 XL |
101 | if let Some(binding_span) = find_pat_binding(outer_pat, binding_id); |
102 | // the "else" branches must be equal | |
103 | if match (outer_else_body, inner_else_body) { | |
104 | (None, None) => true, | |
105 | (None, Some(e)) | (Some(e), None) => is_unit_expr(e), | |
106 | (Some(a), Some(b)) => SpanlessEq::new(cx).eq_expr(a, b), | |
107 | }; | |
f20569fa XL |
108 | // the binding must not be used in the if guard |
109 | let mut used_visitor = LocalUsedVisitor::new(cx, binding_id); | |
94222f64 XL |
110 | if outer_guard.map_or(true, |(Guard::If(e) | Guard::IfLet(_, e))| !used_visitor.check_expr(e)); |
111 | // ...or anywhere in the inner expression | |
112 | if match inner { | |
113 | IfLetOrMatch::IfLet(_, _, body, els) => { | |
114 | !used_visitor.check_expr(body) && els.map_or(true, |e| !used_visitor.check_expr(e)) | |
115 | }, | |
116 | IfLetOrMatch::Match(_, arms, ..) => !arms.iter().any(|arm| used_visitor.check_arm(arm)), | |
f20569fa | 117 | }; |
f20569fa | 118 | then { |
94222f64 XL |
119 | let msg = format!( |
120 | "this `{}` can be collapsed into the outer `{}`", | |
121 | if matches!(inner, IfLetOrMatch::Match(..)) { "match" } else { "if let" }, | |
122 | if outer_is_match { "match" } else { "if let" }, | |
123 | ); | |
f20569fa XL |
124 | span_lint_and_then( |
125 | cx, | |
126 | COLLAPSIBLE_MATCH, | |
94222f64 XL |
127 | inner_expr.span, |
128 | &msg, | |
f20569fa | 129 | |diag| { |
94222f64 | 130 | let mut help_span = MultiSpan::from_spans(vec![binding_span, inner_then_pat.span]); |
f20569fa | 131 | help_span.push_span_label(binding_span, "replace this binding".into()); |
94222f64 | 132 | help_span.push_span_label(inner_then_pat.span, "with this pattern".into()); |
f20569fa XL |
133 | diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern"); |
134 | }, | |
135 | ); | |
136 | } | |
137 | } | |
138 | } | |
139 | ||
140 | fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> { | |
141 | while let ExprKind::Block(block, _) = expr.kind { | |
142 | match (block.stmts, block.expr) { | |
143 | ([stmt], None) => match stmt.kind { | |
144 | StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e, | |
145 | _ => break, | |
146 | }, | |
147 | ([], Some(e)) => expr = e, | |
148 | _ => break, | |
149 | } | |
150 | } | |
151 | expr | |
152 | } | |
153 | ||
94222f64 XL |
154 | enum IfLetOrMatch<'hir> { |
155 | Match(&'hir Expr<'hir>, &'hir [Arm<'hir>], MatchSource), | |
156 | /// scrutinee, pattern, then block, else block | |
157 | IfLet(&'hir Expr<'hir>, &'hir Pat<'hir>, &'hir Expr<'hir>, Option<&'hir Expr<'hir>>), | |
158 | } | |
159 | ||
160 | impl<'hir> IfLetOrMatch<'hir> { | |
161 | fn parse(cx: &LateContext<'_>, expr: &Expr<'hir>) -> Option<Self> { | |
162 | match expr.kind { | |
163 | ExprKind::Match(expr, arms, source) => Some(Self::Match(expr, arms, source)), | |
164 | _ => higher::IfLet::hir(cx, expr).map(|higher::IfLet { let_expr, let_pat, if_then, if_else }| { | |
165 | Self::IfLet(let_expr, let_pat, if_then, if_else) | |
166 | }) | |
167 | } | |
168 | } | |
169 | } | |
170 | ||
171 | /// A "wild-like" arm has a wild (`_`) or `None` pattern and no guard. Such arms can be "collapsed" | |
172 | /// into a single wild arm without any significant loss in semantics or readability. | |
cdc7bbd5 | 173 | fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool { |
f20569fa XL |
174 | if arm.guard.is_some() { |
175 | return false; | |
176 | } | |
177 | match arm.pat.kind { | |
178 | PatKind::Binding(..) | PatKind::Wild => true, | |
cdc7bbd5 | 179 | PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone), |
f20569fa XL |
180 | _ => false, |
181 | } | |
182 | } | |
183 | ||
184 | fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> { | |
185 | let mut span = None; | |
186 | pat.walk_short(|p| match &p.kind { | |
187 | // ignore OR patterns | |
188 | PatKind::Or(_) => false, | |
189 | PatKind::Binding(_bm, _, _ident, _) => { | |
190 | let found = p.hir_id == hir_id; | |
191 | if found { | |
192 | span = Some(p.span); | |
193 | } | |
194 | !found | |
195 | }, | |
196 | _ => true, | |
197 | }); | |
198 | span | |
199 | } | |
200 | ||
201 | fn pat_contains_or(pat: &Pat<'_>) -> bool { | |
202 | let mut result = false; | |
203 | pat.walk(|p| { | |
204 | let is_or = matches!(p.kind, PatKind::Or(_)); | |
205 | result |= is_or; | |
206 | !is_or | |
207 | }); | |
208 | result | |
209 | } |