]> git.proxmox.com Git - rustc.git/blame - src/tools/clippy/clippy_lints/src/collapsible_match.rs
New upstream version 1.56.0~beta.4+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / collapsible_match.rs
CommitLineData
cdc7bbd5
XL
1use clippy_utils::diagnostics::span_lint_and_then;
2use clippy_utils::visitors::LocalUsedVisitor;
94222f64 3use clippy_utils::{higher, is_lang_ctor, is_unit_expr, path_to_local, peel_ref_operators, SpanlessEq};
f20569fa 4use if_chain::if_chain;
cdc7bbd5 5use rustc_hir::LangItem::OptionNone;
94222f64 6use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, MatchSource, Pat, PatKind, StmtKind};
f20569fa 7use rustc_lint::{LateContext, LateLintPass};
f20569fa
XL
8use rustc_session::{declare_lint_pass, declare_tool_lint};
9use rustc_span::{MultiSpan, Span};
10
11declare_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
48declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
49
50impl<'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
68fn 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
140fn 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
154enum 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
160impl<'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 173fn 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
184fn 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
201fn 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}