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_ref_operators, SpanlessEq};
5 use if_chain::if_chain;
6 use rustc_hir::LangItem::OptionNone;
7 use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, Pat, PatKind, StmtKind};
8 use rustc_lint::{LateContext, LateLintPass};
9 use rustc_session::{declare_lint_pass, declare_tool_lint};
10 use rustc_span::{MultiSpan, Span};
12 declare_clippy_lint! {
14 /// Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
15 /// without adding any branches.
17 /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only
18 /// cases where merging would most likely make the code more readable.
20 /// ### Why is this bad?
21 /// It is unnecessarily verbose and complex.
25 /// fn func(opt: Option<Result<u64, String>>) {
26 /// let n = match opt {
27 /// Some(n) => match n {
37 /// fn func(opt: Option<Result<u64, String>>) {
38 /// let n = match opt {
44 pub COLLAPSIBLE_MATCH,
46 "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
49 declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
51 impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
52 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
53 match IfLetOrMatch::parse(cx, expr) {
54 Some(IfLetOrMatch::Match(_, arms, _)) => {
55 if let Some(els_arm) = arms.iter().rfind(|arm| arm_is_wild_like(cx, arm)) {
57 check_arm(cx, true, arm.pat, arm.body, arm.guard.as_ref(), Some(els_arm.body));
61 Some(IfLetOrMatch::IfLet(_, pat, body, els)) => {
62 check_arm(cx, false, pat, body, None, els);
70 cx: &LateContext<'tcx>,
72 outer_pat: &'tcx Pat<'tcx>,
73 outer_then_body: &'tcx Expr<'tcx>,
74 outer_guard: Option<&'tcx Guard<'tcx>>,
75 outer_else_body: Option<&'tcx Expr<'tcx>>,
77 let inner_expr = strip_singleton_blocks(outer_then_body);
79 if let Some(inner) = IfLetOrMatch::parse(cx, inner_expr);
80 if let Some((inner_scrutinee, inner_then_pat, inner_else_body)) = match inner {
81 IfLetOrMatch::IfLet(scrutinee, pat, _, els) => Some((scrutinee, pat, els)),
82 IfLetOrMatch::Match(scrutinee, arms, ..) => if_chain! {
83 // if there are more than two arms, collapsing would be non-trivial
84 if arms.len() == 2 && arms.iter().all(|a| a.guard.is_none());
85 // one of the arms must be "wild-like"
86 if let Some(wild_idx) = arms.iter().rposition(|a| arm_is_wild_like(cx, a));
88 let (then, els) = (&arms[1 - wild_idx], &arms[wild_idx]);
89 Some((scrutinee, then.pat, Some(els.body)))
95 if outer_pat.span.ctxt() == inner_scrutinee.span.ctxt();
96 // match expression must be a local binding
97 // match <local> { .. }
98 if let Some(binding_id) = path_to_local(peel_ref_operators(cx, inner_scrutinee));
99 if !pat_contains_or(inner_then_pat);
100 // the binding must come from the pattern of the containing match arm
101 // ..<local>.. => match <local> { .. }
102 if let Some(binding_span) = find_pat_binding(outer_pat, binding_id);
103 // the "else" branches must be equal
104 if match (outer_else_body, inner_else_body) {
105 (None, None) => true,
106 (None, Some(e)) | (Some(e), None) => is_unit_expr(e),
107 (Some(a), Some(b)) => SpanlessEq::new(cx).eq_expr(a, b),
109 // the binding must not be used in the if guard
110 if outer_guard.map_or(true, |(Guard::If(e) | Guard::IfLet(_, e))| !is_local_used(cx, *e, binding_id));
111 // ...or anywhere in the inner expression
113 IfLetOrMatch::IfLet(_, _, body, els) => {
114 !is_local_used(cx, body, binding_id) && els.map_or(true, |e| !is_local_used(cx, e, binding_id))
116 IfLetOrMatch::Match(_, arms, ..) => !arms.iter().any(|arm| is_local_used(cx, arm, binding_id)),
120 "this `{}` can be collapsed into the outer `{}`",
121 if matches!(inner, IfLetOrMatch::Match(..)) { "match" } else { "if let" },
122 if outer_is_match { "match" } else { "if let" },
130 let mut help_span = MultiSpan::from_spans(vec![binding_span, inner_then_pat.span]);
131 help_span.push_span_label(binding_span, "replace this binding".into());
132 help_span.push_span_label(inner_then_pat.span, "with this pattern".into());
133 diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
140 fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
141 while let ExprKind::Block(block, _) = expr.kind {
142 match (block.stmts, block.expr) {
143 ([stmt], None) => match stmt.kind {
144 StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e,
147 ([], Some(e)) => expr = e,
154 /// A "wild-like" arm has a wild (`_`) or `None` pattern and no guard. Such arms can be "collapsed"
155 /// into a single wild arm without any significant loss in semantics or readability.
156 fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
157 if arm.guard.is_some() {
161 PatKind::Binding(..) | PatKind::Wild => true,
162 PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
167 fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
169 pat.walk_short(|p| match &p.kind {
170 // ignore OR patterns
171 PatKind::Or(_) => false,
172 PatKind::Binding(_bm, _, _ident, _) => {
173 let found = p.hir_id == hir_id;
184 fn pat_contains_or(pat: &Pat<'_>) -> bool {
185 let mut result = false;
187 let is_or = matches!(p.kind, PatKind::Or(_));