]> git.proxmox.com Git - rustc.git/blame - src/tools/clippy/clippy_lints/src/collapsible_match.rs
New upstream version 1.62.1+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / collapsible_match.rs
CommitLineData
cdc7bbd5 1use clippy_utils::diagnostics::span_lint_and_then;
c295e0f8
XL
2use clippy_utils::higher::IfLetOrMatch;
3use clippy_utils::visitors::is_local_used;
a2a8927a 4use clippy_utils::{is_lang_ctor, is_unit_expr, path_to_local, peel_blocks_with_stmt, peel_ref_operators, SpanlessEq};
f20569fa 5use if_chain::if_chain;
04454e1e 6use rustc_errors::MultiSpan;
cdc7bbd5 7use rustc_hir::LangItem::OptionNone;
a2a8927a 8use rustc_hir::{Arm, Expr, Guard, HirId, Pat, PatKind};
f20569fa 9use rustc_lint::{LateContext, LateLintPass};
f20569fa 10use rustc_session::{declare_lint_pass, declare_tool_lint};
04454e1e 11use rustc_span::Span;
f20569fa
XL
12
13declare_clippy_lint! {
94222f64
XL
14 /// ### What it does
15 /// Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
f20569fa
XL
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 ///
94222f64
XL
21 /// ### Why is this bad?
22 /// It is unnecessarily verbose and complex.
f20569fa 23 ///
94222f64 24 /// ### Example
f20569fa
XL
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 /// ```
a2a8927a 45 #[clippy::version = "1.50.0"]
f20569fa
XL
46 pub COLLAPSIBLE_MATCH,
47 style,
48 "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
49}
50
51declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
52
53impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
54 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
94222f64
XL
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 }
f20569fa 61 }
c295e0f8 62 },
94222f64
XL
63 Some(IfLetOrMatch::IfLet(_, pat, body, els)) => {
64 check_arm(cx, false, pat, body, None, els);
c295e0f8
XL
65 },
66 None => {},
f20569fa
XL
67 }
68 }
69}
70
94222f64
XL
71fn 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>>,
c295e0f8 77 outer_else_body: Option<&'tcx Expr<'tcx>>,
94222f64 78) {
a2a8927a 79 let inner_expr = peel_blocks_with_stmt(outer_then_body);
f20569fa 80 if_chain! {
94222f64
XL
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();
f20569fa
XL
98 // match expression must be a local binding
99 // match <local> { .. }
94222f64
XL
100 if let Some(binding_id) = path_to_local(peel_ref_operators(cx, inner_scrutinee));
101 if !pat_contains_or(inner_then_pat);
f20569fa
XL
102 // the binding must come from the pattern of the containing match arm
103 // ..<local>.. => match <local> { .. }
94222f64
XL
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 };
f20569fa 111 // the binding must not be used in the if guard
c295e0f8 112 if outer_guard.map_or(true, |(Guard::If(e) | Guard::IfLet(_, e))| !is_local_used(cx, *e, binding_id));
94222f64
XL
113 // ...or anywhere in the inner expression
114 if match inner {
115 IfLetOrMatch::IfLet(_, _, body, els) => {
c295e0f8 116 !is_local_used(cx, body, binding_id) && els.map_or(true, |e| !is_local_used(cx, e, binding_id))
94222f64 117 },
c295e0f8 118 IfLetOrMatch::Match(_, arms, ..) => !arms.iter().any(|arm| is_local_used(cx, arm, binding_id)),
f20569fa 119 };
f20569fa 120 then {
94222f64
XL
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 );
f20569fa
XL
126 span_lint_and_then(
127 cx,
128 COLLAPSIBLE_MATCH,
94222f64
XL
129 inner_expr.span,
130 &msg,
f20569fa 131 |diag| {
94222f64 132 let mut help_span = MultiSpan::from_spans(vec![binding_span, inner_then_pat.span]);
04454e1e
FG
133 help_span.push_span_label(binding_span, "replace this binding");
134 help_span.push_span_label(inner_then_pat.span, "with this pattern");
f20569fa
XL
135 diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
136 },
137 );
138 }
139 }
140}
141
94222f64
XL
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.
cdc7bbd5 144fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
f20569fa
XL
145 if arm.guard.is_some() {
146 return false;
147 }
148 match arm.pat.kind {
149 PatKind::Binding(..) | PatKind::Wild => true,
cdc7bbd5 150 PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
f20569fa
XL
151 _ => false,
152 }
153}
154
155fn 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
172fn 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}