]> git.proxmox.com Git - rustc.git/blob - src/tools/clippy/clippy_lints/src/collapsible_match.rs
New upstream version 1.55.0+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / collapsible_match.rs
1 use clippy_utils::diagnostics::span_lint_and_then;
2 use clippy_utils::visitors::LocalUsedVisitor;
3 use clippy_utils::{is_lang_ctor, path_to_local, peel_ref_operators, SpanlessEq};
4 use if_chain::if_chain;
5 use rustc_hir::LangItem::OptionNone;
6 use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, Pat, PatKind, StmtKind};
7 use rustc_lint::{LateContext, LateLintPass};
8 use rustc_session::{declare_lint_pass, declare_tool_lint};
9 use rustc_span::{MultiSpan, Span};
10
11 declare_clippy_lint! {
12 /// **What it does:** Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
13 /// without adding any branches.
14 ///
15 /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only
16 /// cases where merging would most likely make the code more readable.
17 ///
18 /// **Why is this bad?** It is unnecessarily verbose and complex.
19 ///
20 /// **Known problems:** None.
21 ///
22 /// **Example:**
23 ///
24 /// ```rust
25 /// fn func(opt: Option<Result<u64, String>>) {
26 /// let n = match opt {
27 /// Some(n) => match n {
28 /// Ok(n) => n,
29 /// _ => return,
30 /// }
31 /// None => return,
32 /// };
33 /// }
34 /// ```
35 /// Use instead:
36 /// ```rust
37 /// fn func(opt: Option<Result<u64, String>>) {
38 /// let n = match opt {
39 /// Some(Ok(n)) => n,
40 /// _ => return,
41 /// };
42 /// }
43 /// ```
44 pub COLLAPSIBLE_MATCH,
45 style,
46 "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
47 }
48
49 declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
50
51 impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
52 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
53 if let ExprKind::Match(_expr, arms, _source) = expr.kind {
54 if let Some(wild_arm) = arms.iter().rfind(|arm| arm_is_wild_like(cx, arm)) {
55 for arm in arms {
56 check_arm(arm, wild_arm, cx);
57 }
58 }
59 }
60 }
61 }
62
63 fn check_arm<'tcx>(arm: &Arm<'tcx>, wild_outer_arm: &Arm<'tcx>, cx: &LateContext<'tcx>) {
64 let expr = strip_singleton_blocks(arm.body);
65 if_chain! {
66 if let ExprKind::Match(expr_in, arms_inner, _) = expr.kind;
67 // the outer arm pattern and the inner match
68 if expr_in.span.ctxt() == arm.pat.span.ctxt();
69 // there must be no more than two arms in the inner match for this lint
70 if arms_inner.len() == 2;
71 // no if guards on the inner match
72 if arms_inner.iter().all(|arm| arm.guard.is_none());
73 // match expression must be a local binding
74 // match <local> { .. }
75 if let Some(binding_id) = path_to_local(peel_ref_operators(cx, expr_in));
76 // one of the branches must be "wild-like"
77 if let Some(wild_inner_arm_idx) = arms_inner.iter().rposition(|arm_inner| arm_is_wild_like(cx, arm_inner));
78 let (wild_inner_arm, non_wild_inner_arm) =
79 (&arms_inner[wild_inner_arm_idx], &arms_inner[1 - wild_inner_arm_idx]);
80 if !pat_contains_or(non_wild_inner_arm.pat);
81 // the binding must come from the pattern of the containing match arm
82 // ..<local>.. => match <local> { .. }
83 if let Some(binding_span) = find_pat_binding(arm.pat, binding_id);
84 // the "wild-like" branches must be equal
85 if SpanlessEq::new(cx).eq_expr(wild_inner_arm.body, wild_outer_arm.body);
86 // the binding must not be used in the if guard
87 let mut used_visitor = LocalUsedVisitor::new(cx, binding_id);
88 if match arm.guard {
89 None => true,
90 Some(Guard::If(expr) | Guard::IfLet(_, expr)) => !used_visitor.check_expr(expr),
91 };
92 // ...or anywhere in the inner match
93 if !arms_inner.iter().any(|arm| used_visitor.check_arm(arm));
94 then {
95 span_lint_and_then(
96 cx,
97 COLLAPSIBLE_MATCH,
98 expr.span,
99 "unnecessary nested match",
100 |diag| {
101 let mut help_span = MultiSpan::from_spans(vec![binding_span, non_wild_inner_arm.pat.span]);
102 help_span.push_span_label(binding_span, "replace this binding".into());
103 help_span.push_span_label(non_wild_inner_arm.pat.span, "with this pattern".into());
104 diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
105 },
106 );
107 }
108 }
109 }
110
111 fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
112 while let ExprKind::Block(block, _) = expr.kind {
113 match (block.stmts, block.expr) {
114 ([stmt], None) => match stmt.kind {
115 StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e,
116 _ => break,
117 },
118 ([], Some(e)) => expr = e,
119 _ => break,
120 }
121 }
122 expr
123 }
124
125 /// A "wild-like" pattern is wild ("_") or `None`.
126 /// For this lint to apply, both the outer and inner match expressions
127 /// must have "wild-like" branches that can be combined.
128 fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
129 if arm.guard.is_some() {
130 return false;
131 }
132 match arm.pat.kind {
133 PatKind::Binding(..) | PatKind::Wild => true,
134 PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
135 _ => false,
136 }
137 }
138
139 fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
140 let mut span = None;
141 pat.walk_short(|p| match &p.kind {
142 // ignore OR patterns
143 PatKind::Or(_) => false,
144 PatKind::Binding(_bm, _, _ident, _) => {
145 let found = p.hir_id == hir_id;
146 if found {
147 span = Some(p.span);
148 }
149 !found
150 },
151 _ => true,
152 });
153 span
154 }
155
156 fn pat_contains_or(pat: &Pat<'_>) -> bool {
157 let mut result = false;
158 pat.walk(|p| {
159 let is_or = matches!(p.kind, PatKind::Or(_));
160 result |= is_or;
161 !is_or
162 });
163 result
164 }