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