]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/collapsible_match.rs
Rollup merge of #96142 - cjgillot:no-crate-def-index, r=petrochenkov
[rust.git] / clippy_lints / src / collapsible_match.rs
1 use clippy_utils::diagnostics::span_lint_and_then;
2 use clippy_utils::higher::IfLetOrMatch;
3 use clippy_utils::visitors::is_local_used;
4 use clippy_utils::{is_lang_ctor, is_unit_expr, path_to_local, peel_blocks_with_stmt, peel_ref_operators, SpanlessEq};
5 use if_chain::if_chain;
6 use rustc_errors::MultiSpan;
7 use rustc_hir::LangItem::OptionNone;
8 use rustc_hir::{Arm, Expr, Guard, HirId, Pat, PatKind};
9 use rustc_lint::{LateContext, LateLintPass};
10 use rustc_session::{declare_lint_pass, declare_tool_lint};
11 use rustc_span::Span;
12
13 declare_clippy_lint! {
14     /// ### What it does
15     /// Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
16     /// without adding any branches.
17     ///
18     /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only
19     /// cases where merging would most likely make the code more readable.
20     ///
21     /// ### Why is this bad?
22     /// It is unnecessarily verbose and complex.
23     ///
24     /// ### Example
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     #[clippy::version = "1.50.0"]
46     pub COLLAPSIBLE_MATCH,
47     style,
48     "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
49 }
50
51 declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
52
53 impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
54     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
55         match IfLetOrMatch::parse(cx, expr) {
56             Some(IfLetOrMatch::Match(_, arms, _)) => {
57                 if let Some(els_arm) = arms.iter().rfind(|arm| arm_is_wild_like(cx, arm)) {
58                     for arm in arms {
59                         check_arm(cx, true, arm.pat, arm.body, arm.guard.as_ref(), Some(els_arm.body));
60                     }
61                 }
62             },
63             Some(IfLetOrMatch::IfLet(_, pat, body, els)) => {
64                 check_arm(cx, false, pat, body, None, els);
65             },
66             None => {},
67         }
68     }
69 }
70
71 fn check_arm<'tcx>(
72     cx: &LateContext<'tcx>,
73     outer_is_match: bool,
74     outer_pat: &'tcx Pat<'tcx>,
75     outer_then_body: &'tcx Expr<'tcx>,
76     outer_guard: Option<&'tcx Guard<'tcx>>,
77     outer_else_body: Option<&'tcx Expr<'tcx>>,
78 ) {
79     let inner_expr = peel_blocks_with_stmt(outer_then_body);
80     if_chain! {
81         if let Some(inner) = IfLetOrMatch::parse(cx, inner_expr);
82         if let Some((inner_scrutinee, inner_then_pat, inner_else_body)) = match inner {
83             IfLetOrMatch::IfLet(scrutinee, pat, _, els) => Some((scrutinee, pat, els)),
84             IfLetOrMatch::Match(scrutinee, arms, ..) => if_chain! {
85                 // if there are more than two arms, collapsing would be non-trivial
86                 if arms.len() == 2 && arms.iter().all(|a| a.guard.is_none());
87                 // one of the arms must be "wild-like"
88                 if let Some(wild_idx) = arms.iter().rposition(|a| arm_is_wild_like(cx, a));
89                 then {
90                     let (then, els) = (&arms[1 - wild_idx], &arms[wild_idx]);
91                     Some((scrutinee, then.pat, Some(els.body)))
92                 } else {
93                     None
94                 }
95             },
96         };
97         if outer_pat.span.ctxt() == inner_scrutinee.span.ctxt();
98         // match expression must be a local binding
99         // match <local> { .. }
100         if let Some(binding_id) = path_to_local(peel_ref_operators(cx, inner_scrutinee));
101         if !pat_contains_or(inner_then_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 "else" branches must be equal
106         if match (outer_else_body, inner_else_body) {
107             (None, None) => true,
108             (None, Some(e)) | (Some(e), None) => is_unit_expr(e),
109             (Some(a), Some(b)) => SpanlessEq::new(cx).eq_expr(a, b),
110         };
111         // the binding must not be used in the if guard
112         if outer_guard.map_or(true, |(Guard::If(e) | Guard::IfLet(_, e))| !is_local_used(cx, *e, binding_id));
113         // ...or anywhere in the inner expression
114         if match inner {
115             IfLetOrMatch::IfLet(_, _, body, els) => {
116                 !is_local_used(cx, body, binding_id) && els.map_or(true, |e| !is_local_used(cx, e, binding_id))
117             },
118             IfLetOrMatch::Match(_, arms, ..) => !arms.iter().any(|arm| is_local_used(cx, arm, binding_id)),
119         };
120         then {
121             let msg = format!(
122                 "this `{}` can be collapsed into the outer `{}`",
123                 if matches!(inner, IfLetOrMatch::Match(..)) { "match" } else { "if let" },
124                 if outer_is_match { "match" } else { "if let" },
125             );
126             span_lint_and_then(
127                 cx,
128                 COLLAPSIBLE_MATCH,
129                 inner_expr.span,
130                 &msg,
131                 |diag| {
132                     let mut help_span = MultiSpan::from_spans(vec![binding_span, inner_then_pat.span]);
133                     help_span.push_span_label(binding_span, "replace this binding");
134                     help_span.push_span_label(inner_then_pat.span, "with this pattern");
135                     diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
136                 },
137             );
138         }
139     }
140 }
141
142 /// A "wild-like" arm has a wild (`_`) or `None` pattern and no guard. Such arms can be "collapsed"
143 /// into a single wild arm without any significant loss in semantics or readability.
144 fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
145     if arm.guard.is_some() {
146         return false;
147     }
148     match arm.pat.kind {
149         PatKind::Binding(..) | PatKind::Wild => true,
150         PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
151         _ => false,
152     }
153 }
154
155 fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
156     let mut span = None;
157     pat.walk_short(|p| match &p.kind {
158         // ignore OR patterns
159         PatKind::Or(_) => false,
160         PatKind::Binding(_bm, _, _ident, _) => {
161             let found = p.hir_id == hir_id;
162             if found {
163                 span = Some(p.span);
164             }
165             !found
166         },
167         _ => true,
168     });
169     span
170 }
171
172 fn pat_contains_or(pat: &Pat<'_>) -> bool {
173     let mut result = false;
174     pat.walk(|p| {
175         let is_or = matches!(p.kind, PatKind::Or(_));
176         result |= is_or;
177         !is_or
178     });
179     result
180 }