]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/collapsible_match.rs
Rollup merge of #88012 - sunfishcode:sunfishcode/wasi-raw-fd-c-int, r=alexcrichton
[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::{higher, 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::{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
13     /// 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?
20     /// It is unnecessarily verbose and complex.
21     ///
22     /// ### Example
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
48 declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
49
50 impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
51     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
52         if let Some(higher::IfLet {
53             let_pat,
54             if_then,
55             if_else,
56             ..
57         }) = higher::IfLet::hir(expr)
58         {
59             check_arm(cx, if_then, None, let_pat, if_else);
60
61             check_if_let(cx, if_then, let_pat);
62         }
63
64         if let ExprKind::Match(_expr, arms, _source) = expr.kind {
65             if let Some(wild_arm) = arms.iter().rfind(|arm| is_wild_like(cx, &arm.pat.kind, &arm.guard)) {
66                 for arm in arms {
67                     check_arm(cx, arm.body, arm.guard.as_ref(), arm.pat, Some(wild_arm.body));
68                 }
69             }
70
71             if let Some(first_arm) = arms.get(0) {
72                 check_if_let(cx, &first_arm.body, &first_arm.pat);
73             }
74         }
75     }
76 }
77
78 fn check_arm<'tcx>(
79     cx: &LateContext<'tcx>,
80     outer_block: &'tcx Expr<'tcx>,
81     outer_guard: Option<&Guard<'tcx>>,
82     outer_pat: &'tcx Pat<'tcx>,
83     wild_outer_block: Option<&'tcx Expr<'tcx>>,
84 ) {
85     let expr = strip_singleton_blocks(outer_block);
86     if_chain! {
87         if let ExprKind::Match(expr_in, arms_inner, _) = expr.kind;
88         // the outer arm pattern and the inner match
89         if expr_in.span.ctxt() == outer_pat.span.ctxt();
90         // there must be no more than two arms in the inner match for this lint
91         if arms_inner.len() == 2;
92         // no if guards on the inner match
93         if arms_inner.iter().all(|arm| arm.guard.is_none());
94         // match expression must be a local binding
95         // match <local> { .. }
96         if let Some(binding_id) = path_to_local(peel_ref_operators(cx, expr_in));
97         // one of the branches must be "wild-like"
98         if let Some(wild_inner_arm_idx) = arms_inner.iter().rposition(|arm_inner| is_wild_like(cx, &arm_inner.pat.kind, &arm_inner.guard));
99         let (wild_inner_arm, non_wild_inner_arm) =
100             (&arms_inner[wild_inner_arm_idx], &arms_inner[1 - wild_inner_arm_idx]);
101         if !pat_contains_or(non_wild_inner_arm.pat);
102         // the binding must come from the pattern of the containing match arm
103         // ..<local>.. => match <local> { .. }
104         if let Some(binding_span) = find_pat_binding(outer_pat, binding_id);
105         // the "wild-like" branches must be equal
106         if wild_outer_block.map(|el| SpanlessEq::new(cx).eq_expr(wild_inner_arm.body, el)).unwrap_or(true);
107         // the binding must not be used in the if guard
108         let mut used_visitor = LocalUsedVisitor::new(cx, binding_id);
109         if match outer_guard {
110             None => true,
111             Some(Guard::If(expr) | Guard::IfLet(_, expr)) => !used_visitor.check_expr(expr),
112         };
113         // ...or anywhere in the inner match
114         if !arms_inner.iter().any(|arm| used_visitor.check_arm(arm));
115         then {
116             span_lint_and_then(
117                 cx,
118                 COLLAPSIBLE_MATCH,
119                 expr.span,
120                 "unnecessary nested match",
121                 |diag| {
122                     let mut help_span = MultiSpan::from_spans(vec![binding_span, non_wild_inner_arm.pat.span]);
123                     help_span.push_span_label(binding_span, "replace this binding".into());
124                     help_span.push_span_label(non_wild_inner_arm.pat.span, "with this pattern".into());
125                     diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
126                 },
127             );
128         }
129     }
130 }
131
132 fn check_if_let<'tcx>(cx: &LateContext<'tcx>, outer_expr: &'tcx Expr<'tcx>, outer_pat: &'tcx Pat<'tcx>) {
133     let block_inner = strip_singleton_blocks(outer_expr);
134     if_chain! {
135         if let Some(higher::IfLet { if_then: inner_if_then, let_expr: inner_let_expr, let_pat: inner_let_pat, .. }) = higher::IfLet::hir(block_inner);
136         if let Some(binding_id) = path_to_local(peel_ref_operators(cx, inner_let_expr));
137         if let Some(binding_span) = find_pat_binding(outer_pat, binding_id);
138         let mut used_visitor = LocalUsedVisitor::new(cx, binding_id);
139         if !used_visitor.check_expr(inner_if_then);
140         then {
141             span_lint_and_then(
142                 cx,
143                 COLLAPSIBLE_MATCH,
144                 block_inner.span,
145                 "unnecessary nested `if let` or `match`",
146                 |diag| {
147                     let mut help_span = MultiSpan::from_spans(vec![binding_span, inner_let_pat.span]);
148                     help_span.push_span_label(binding_span, "replace this binding".into());
149                     help_span.push_span_label(inner_let_pat.span, "with this pattern".into());
150                     diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
151                 },
152             );
153         }
154     }
155 }
156
157 fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
158     while let ExprKind::Block(block, _) = expr.kind {
159         match (block.stmts, block.expr) {
160             ([stmt], None) => match stmt.kind {
161                 StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e,
162                 _ => break,
163             },
164             ([], Some(e)) => expr = e,
165             _ => break,
166         }
167     }
168     expr
169 }
170
171 /// A "wild-like" pattern is wild ("_") or `None`.
172 /// For this lint to apply, both the outer and inner patterns
173 /// must have "wild-like" branches that can be combined.
174 fn is_wild_like(cx: &LateContext<'_>, pat_kind: &PatKind<'_>, arm_guard: &Option<Guard<'_>>) -> bool {
175     if arm_guard.is_some() {
176         return false;
177     }
178     match pat_kind {
179         PatKind::Binding(..) | PatKind::Wild => true,
180         PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
181         _ => false,
182     }
183 }
184
185 fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
186     let mut span = None;
187     pat.walk_short(|p| match &p.kind {
188         // ignore OR patterns
189         PatKind::Or(_) => false,
190         PatKind::Binding(_bm, _, _ident, _) => {
191             let found = p.hir_id == hir_id;
192             if found {
193                 span = Some(p.span);
194             }
195             !found
196         },
197         _ => true,
198     });
199     span
200 }
201
202 fn pat_contains_or(pat: &Pat<'_>) -> bool {
203     let mut result = false;
204     pat.walk(|p| {
205         let is_or = matches!(p.kind, PatKind::Or(_));
206         result |= is_or;
207         !is_or
208     });
209     result
210 }