1 use crate::utils::visitors::LocalUsedVisitor;
2 use crate::utils::{span_lint_and_then, SpanlessEq};
3 use if_chain::if_chain;
4 use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
5 use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, Pat, PatKind, QPath, StmtKind};
6 use rustc_lint::{LateContext, LateLintPass};
7 use rustc_middle::ty::{DefIdTree, TyCtxt};
8 use rustc_session::{declare_lint_pass, declare_tool_lint};
9 use rustc_span::{MultiSpan, Span};
11 declare_clippy_lint! {
12 /// **What it does:** Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
13 /// without adding any branches.
15 /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only
16 /// cases where merging would most likely make the code more readable.
18 /// **Why is this bad?** It is unnecessarily verbose and complex.
20 /// **Known problems:** None.
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 if let ExprKind::Match(_expr, arms, _source) = expr.kind {
54 if let Some(wild_arm) = arms.iter().rfind(|arm| arm_is_wild_like(arm, cx.tcx)) {
56 check_arm(arm, wild_arm, cx);
63 fn check_arm(arm: &Arm<'_>, wild_outer_arm: &Arm<'_>, cx: &LateContext<'_>) {
65 let expr = strip_singleton_blocks(arm.body);
66 if let ExprKind::Match(expr_in, arms_inner, _) = expr.kind;
67 // the outer arm pattern and the inner match
68 if expr_in.span.ctxt() == arm.pat.span.ctxt();
69 // there must be no more than two arms in the inner match for this lint
70 if arms_inner.len() == 2;
71 // no if guards on the inner match
72 if arms_inner.iter().all(|arm| arm.guard.is_none());
73 // match expression must be a local binding
74 // match <local> { .. }
75 if let ExprKind::Path(QPath::Resolved(None, path)) = expr_in.kind;
76 if let Res::Local(binding_id) = path.res;
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(arm_inner, cx.tcx));
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 if !matches!(arm.guard, Some(Guard::If(guard)) if LocalUsedVisitor::new(binding_id).check_expr(guard));
89 // ...or anywhere in the inner match
90 if !arms_inner.iter().any(|arm| LocalUsedVisitor::new(binding_id).check_arm(arm));
96 "Unnecessary nested match",
98 let mut help_span = MultiSpan::from_spans(vec![binding_span, non_wild_inner_arm.pat.span]);
99 help_span.push_span_label(binding_span, "Replace this binding".into());
100 help_span.push_span_label(non_wild_inner_arm.pat.span, "with this pattern".into());
101 diag.span_help(help_span, "The outer pattern can be modified to include the inner pattern.");
108 fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
109 while let ExprKind::Block(block, _) = expr.kind {
110 match (block.stmts, block.expr) {
111 ([stmt], None) => match stmt.kind {
112 StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e,
115 ([], Some(e)) => expr = e,
122 /// A "wild-like" pattern is wild ("_") or `None`.
123 /// For this lint to apply, both the outer and inner match expressions
124 /// must have "wild-like" branches that can be combined.
125 fn arm_is_wild_like(arm: &Arm<'_>, tcx: TyCtxt<'_>) -> bool {
126 if arm.guard.is_some() {
130 PatKind::Binding(..) | PatKind::Wild => true,
131 PatKind::Path(QPath::Resolved(None, path)) if is_none_ctor(path.res, tcx) => true,
136 fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
138 pat.walk_short(|p| match &p.kind {
139 // ignore OR patterns
140 PatKind::Or(_) => false,
141 PatKind::Binding(_bm, _, _ident, _) => {
142 let found = p.hir_id == hir_id;
153 fn pat_contains_or(pat: &Pat<'_>) -> bool {
154 let mut result = false;
156 let is_or = matches!(p.kind, PatKind::Or(_));
163 fn is_none_ctor(res: Res, tcx: TyCtxt<'_>) -> bool {
164 if let Some(none_id) = tcx.lang_items().option_none_variant() {
165 if let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = res {
166 if let Some(variant_id) = tcx.parent(id) {
167 return variant_id == none_id;