]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/collapsible_match.rs
Rollup merge of #85760 - ChrisDenton:path-doc-platform-specific, r=m-ou-se
[rust.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, 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, UnOp};
7 use rustc_lint::{LateContext, LateLintPass};
8 use rustc_middle::ty::TypeckResults;
9 use rustc_session::{declare_lint_pass, declare_tool_lint};
10 use rustc_span::{MultiSpan, Span};
11
12 declare_clippy_lint! {
13     /// **What it does:** Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
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     ///
19     /// **Why is this bad?** It is unnecessarily verbose and complex.
20     ///
21     /// **Known problems:** None.
22     ///
23     /// **Example:**
24     ///
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     /// ```
45     pub COLLAPSIBLE_MATCH,
46     style,
47     "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
48 }
49
50 declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
51
52 impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
53     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
54         if let ExprKind::Match(_expr, arms, _source) = expr.kind {
55             if let Some(wild_arm) = arms.iter().rfind(|arm| arm_is_wild_like(cx, arm)) {
56                 for arm in arms {
57                     check_arm(arm, wild_arm, cx);
58                 }
59             }
60         }
61     }
62 }
63
64 fn check_arm<'tcx>(arm: &Arm<'tcx>, wild_outer_arm: &Arm<'tcx>, cx: &LateContext<'tcx>) {
65     let expr = strip_singleton_blocks(arm.body);
66     if_chain! {
67         if let ExprKind::Match(expr_in, arms_inner, _) = expr.kind;
68         // the outer arm pattern and the inner match
69         if expr_in.span.ctxt() == arm.pat.span.ctxt();
70         // there must be no more than two arms in the inner match for this lint
71         if arms_inner.len() == 2;
72         // no if guards on the inner match
73         if arms_inner.iter().all(|arm| arm.guard.is_none());
74         // match expression must be a local binding
75         // match <local> { .. }
76         if let Some(binding_id) = path_to_local(strip_ref_operators(expr_in, cx.typeck_results()));
77         // one of the branches must be "wild-like"
78         if let Some(wild_inner_arm_idx) = arms_inner.iter().rposition(|arm_inner| arm_is_wild_like(cx, arm_inner));
79         let (wild_inner_arm, non_wild_inner_arm) =
80             (&arms_inner[wild_inner_arm_idx], &arms_inner[1 - wild_inner_arm_idx]);
81         if !pat_contains_or(non_wild_inner_arm.pat);
82         // the binding must come from the pattern of the containing match arm
83         // ..<local>.. => match <local> { .. }
84         if let Some(binding_span) = find_pat_binding(arm.pat, binding_id);
85         // the "wild-like" branches must be equal
86         if SpanlessEq::new(cx).eq_expr(wild_inner_arm.body, wild_outer_arm.body);
87         // the binding must not be used in the if guard
88         let mut used_visitor = LocalUsedVisitor::new(cx, binding_id);
89         if match arm.guard {
90             None => true,
91             Some(Guard::If(expr) | Guard::IfLet(_, expr)) => !used_visitor.check_expr(expr),
92         };
93         // ...or anywhere in the inner match
94         if !arms_inner.iter().any(|arm| used_visitor.check_arm(arm));
95         then {
96             span_lint_and_then(
97                 cx,
98                 COLLAPSIBLE_MATCH,
99                 expr.span,
100                 "unnecessary nested match",
101                 |diag| {
102                     let mut help_span = MultiSpan::from_spans(vec![binding_span, non_wild_inner_arm.pat.span]);
103                     help_span.push_span_label(binding_span, "replace this binding".into());
104                     help_span.push_span_label(non_wild_inner_arm.pat.span, "with this pattern".into());
105                     diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
106                 },
107             );
108         }
109     }
110 }
111
112 fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
113     while let ExprKind::Block(block, _) = expr.kind {
114         match (block.stmts, block.expr) {
115             ([stmt], None) => match stmt.kind {
116                 StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e,
117                 _ => break,
118             },
119             ([], Some(e)) => expr = e,
120             _ => break,
121         }
122     }
123     expr
124 }
125
126 /// A "wild-like" pattern is wild ("_") or `None`.
127 /// For this lint to apply, both the outer and inner match expressions
128 /// must have "wild-like" branches that can be combined.
129 fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
130     if arm.guard.is_some() {
131         return false;
132     }
133     match arm.pat.kind {
134         PatKind::Binding(..) | PatKind::Wild => true,
135         PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
136         _ => false,
137     }
138 }
139
140 fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
141     let mut span = None;
142     pat.walk_short(|p| match &p.kind {
143         // ignore OR patterns
144         PatKind::Or(_) => false,
145         PatKind::Binding(_bm, _, _ident, _) => {
146             let found = p.hir_id == hir_id;
147             if found {
148                 span = Some(p.span);
149             }
150             !found
151         },
152         _ => true,
153     });
154     span
155 }
156
157 fn pat_contains_or(pat: &Pat<'_>) -> bool {
158     let mut result = false;
159     pat.walk(|p| {
160         let is_or = matches!(p.kind, PatKind::Or(_));
161         result |= is_or;
162         !is_or
163     });
164     result
165 }
166
167 /// Removes `AddrOf` operators (`&`) or deref operators (`*`), but only if a reference type is
168 /// dereferenced. An overloaded deref such as `Vec` to slice would not be removed.
169 fn strip_ref_operators<'hir>(mut expr: &'hir Expr<'hir>, typeck_results: &TypeckResults<'_>) -> &'hir Expr<'hir> {
170     loop {
171         match expr.kind {
172             ExprKind::AddrOf(_, _, e) => expr = e,
173             ExprKind::Unary(UnOp::Deref, e) if typeck_results.expr_ty(e).is_ref() => expr = e,
174             _ => break,
175         }
176     }
177     expr
178 }