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};
11 declare_clippy_lint! {
13 /// Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
14 /// without adding any branches.
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.
19 /// ### Why is this bad?
20 /// It is unnecessarily verbose and complex.
24 /// fn func(opt: Option<Result<u64, String>>) {
25 /// let n = match opt {
26 /// Some(n) => match n {
36 /// fn func(opt: Option<Result<u64, String>>) {
37 /// let n = match opt {
43 pub COLLAPSIBLE_MATCH,
45 "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
48 declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
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 {
57 }) = higher::IfLet::hir(expr)
59 check_arm(cx, if_then, None, let_pat, if_else);
61 check_if_let(cx, if_then, let_pat);
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)) {
67 check_arm(cx, arm.body, arm.guard.as_ref(), arm.pat, Some(wild_arm.body));
71 if let Some(first_arm) = arms.get(0) {
72 check_if_let(cx, &first_arm.body, &first_arm.pat);
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>>,
85 let expr = strip_singleton_blocks(outer_block);
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 {
111 Some(Guard::If(expr) | Guard::IfLet(_, expr)) => !used_visitor.check_expr(expr),
113 // ...or anywhere in the inner match
114 if !arms_inner.iter().any(|arm| used_visitor.check_arm(arm));
120 "unnecessary nested match",
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");
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);
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);
145 "unnecessary nested `if let` or `match`",
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");
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,
164 ([], Some(e)) => expr = e,
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() {
179 PatKind::Binding(..) | PatKind::Wild => true,
180 PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
185 fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
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;
202 fn pat_contains_or(pat: &Pat<'_>) -> bool {
203 let mut result = false;
205 let is_or = matches!(p.kind, PatKind::Or(_));