]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/collapsible_match.rs
Merge remote-tracking branch 'upstream/master' into rustup
[rust.git] / clippy_lints / src / collapsible_match.rs
1 use crate::utils::visitors::LocalUsedVisitor;
2 use crate::utils::{path_to_local, span_lint_and_then, SpanlessEq};
3 use if_chain::if_chain;
4 use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
5 use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, Pat, PatKind, QPath, StmtKind, UnOp};
6 use rustc_lint::{LateContext, LateLintPass};
7 use rustc_middle::ty::{DefIdTree, TyCtxt, TypeckResults};
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(arm, cx.tcx)) {
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     if_chain! {
65         let expr = strip_singleton_blocks(arm.body);
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(strip_ref_operators(expr_in, cx.typeck_results()));
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(arm_inner, cx.tcx));
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(arm: &Arm<'_>, tcx: TyCtxt<'_>) -> 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(QPath::Resolved(None, path)) if is_none_ctor(path.res, tcx) => true,
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 }
165
166 fn is_none_ctor(res: Res, tcx: TyCtxt<'_>) -> bool {
167     if let Some(none_id) = tcx.lang_items().option_none_variant() {
168         if let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = res {
169             if let Some(variant_id) = tcx.parent(id) {
170                 return variant_id == none_id;
171             }
172         }
173     }
174     false
175 }
176
177 /// Removes `AddrOf` operators (`&`) or deref operators (`*`), but only if a reference type is
178 /// dereferenced. An overloaded deref such as `Vec` to slice would not be removed.
179 fn strip_ref_operators<'hir>(mut expr: &'hir Expr<'hir>, typeck_results: &TypeckResults<'_>) -> &'hir Expr<'hir> {
180     loop {
181         match expr.kind {
182             ExprKind::AddrOf(_, _, e) => expr = e,
183             ExprKind::Unary(UnOp::Deref, e) if typeck_results.expr_ty(e).is_ref() => expr = e,
184             _ => break,
185         }
186     }
187     expr
188 }