]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/matches/collapsible_match.rs
Auto merge of #105252 - bjorn3:codegen_less_pair_values, r=nagisa
[rust.git] / src / tools / clippy / clippy_lints / src / matches / collapsible_match.rs
1 use clippy_utils::diagnostics::span_lint_and_then;
2 use clippy_utils::higher::IfLetOrMatch;
3 use clippy_utils::source::snippet;
4 use clippy_utils::visitors::is_local_used;
5 use clippy_utils::{
6     is_res_lang_ctor, is_unit_expr, path_to_local, peel_blocks_with_stmt, peel_ref_operators, SpanlessEq,
7 };
8 use if_chain::if_chain;
9 use rustc_errors::MultiSpan;
10 use rustc_hir::LangItem::OptionNone;
11 use rustc_hir::{Arm, Expr, Guard, HirId, Let, Pat, PatKind};
12 use rustc_lint::LateContext;
13 use rustc_span::Span;
14
15 use super::COLLAPSIBLE_MATCH;
16
17 pub(super) fn check_match<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>]) {
18     if let Some(els_arm) = arms.iter().rfind(|arm| arm_is_wild_like(cx, arm)) {
19         for arm in arms {
20             check_arm(cx, true, arm.pat, arm.body, arm.guard.as_ref(), Some(els_arm.body));
21         }
22     }
23 }
24
25 pub(super) fn check_if_let<'tcx>(
26     cx: &LateContext<'tcx>,
27     pat: &'tcx Pat<'_>,
28     body: &'tcx Expr<'_>,
29     else_expr: Option<&'tcx Expr<'_>>,
30 ) {
31     check_arm(cx, false, pat, body, None, else_expr);
32 }
33
34 fn check_arm<'tcx>(
35     cx: &LateContext<'tcx>,
36     outer_is_match: bool,
37     outer_pat: &'tcx Pat<'tcx>,
38     outer_then_body: &'tcx Expr<'tcx>,
39     outer_guard: Option<&'tcx Guard<'tcx>>,
40     outer_else_body: Option<&'tcx Expr<'tcx>>,
41 ) {
42     let inner_expr = peel_blocks_with_stmt(outer_then_body);
43     if_chain! {
44         if let Some(inner) = IfLetOrMatch::parse(cx, inner_expr);
45         if let Some((inner_scrutinee, inner_then_pat, inner_else_body)) = match inner {
46             IfLetOrMatch::IfLet(scrutinee, pat, _, els) => Some((scrutinee, pat, els)),
47             IfLetOrMatch::Match(scrutinee, arms, ..) => if_chain! {
48                 // if there are more than two arms, collapsing would be non-trivial
49                 if arms.len() == 2 && arms.iter().all(|a| a.guard.is_none());
50                 // one of the arms must be "wild-like"
51                 if let Some(wild_idx) = arms.iter().rposition(|a| arm_is_wild_like(cx, a));
52                 then {
53                     let (then, els) = (&arms[1 - wild_idx], &arms[wild_idx]);
54                     Some((scrutinee, then.pat, Some(els.body)))
55                 } else {
56                     None
57                 }
58             },
59         };
60         if outer_pat.span.ctxt() == inner_scrutinee.span.ctxt();
61         // match expression must be a local binding
62         // match <local> { .. }
63         if let Some(binding_id) = path_to_local(peel_ref_operators(cx, inner_scrutinee));
64         if !pat_contains_or(inner_then_pat);
65         // the binding must come from the pattern of the containing match arm
66         // ..<local>.. => match <local> { .. }
67         if let (Some(binding_span), is_innermost_parent_pat_struct)
68             = find_pat_binding_and_is_innermost_parent_pat_struct(outer_pat, binding_id);
69         // the "else" branches must be equal
70         if match (outer_else_body, inner_else_body) {
71             (None, None) => true,
72             (None, Some(e)) | (Some(e), None) => is_unit_expr(e),
73             (Some(a), Some(b)) => SpanlessEq::new(cx).eq_expr(a, b),
74         };
75         // the binding must not be used in the if guard
76         if outer_guard.map_or(
77             true,
78             |(Guard::If(e) | Guard::IfLet(Let { init: e, .. }))| !is_local_used(cx, *e, binding_id)
79         );
80         // ...or anywhere in the inner expression
81         if match inner {
82             IfLetOrMatch::IfLet(_, _, body, els) => {
83                 !is_local_used(cx, body, binding_id) && els.map_or(true, |e| !is_local_used(cx, e, binding_id))
84             },
85             IfLetOrMatch::Match(_, arms, ..) => !arms.iter().any(|arm| is_local_used(cx, arm, binding_id)),
86         };
87         then {
88             let msg = format!(
89                 "this `{}` can be collapsed into the outer `{}`",
90                 if matches!(inner, IfLetOrMatch::Match(..)) { "match" } else { "if let" },
91                 if outer_is_match { "match" } else { "if let" },
92             );
93             // collapsing patterns need an explicit field name in struct pattern matching
94             // ex: Struct {x: Some(1)}
95             let replace_msg = if is_innermost_parent_pat_struct {
96                 format!(", prefixed by {}:", snippet(cx, binding_span, "their field name"))
97             } else {
98                 String::new()
99             };
100             span_lint_and_then(
101                 cx,
102                 COLLAPSIBLE_MATCH,
103                 inner_expr.span,
104                 &msg,
105                 |diag| {
106                     let mut help_span = MultiSpan::from_spans(vec![binding_span, inner_then_pat.span]);
107                     help_span.push_span_label(binding_span, "replace this binding");
108                     help_span.push_span_label(inner_then_pat.span, format!("with this pattern{replace_msg}"));
109                     diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
110                 },
111             );
112         }
113     }
114 }
115
116 /// A "wild-like" arm has a wild (`_`) or `None` pattern and no guard. Such arms can be "collapsed"
117 /// into a single wild arm without any significant loss in semantics or readability.
118 fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
119     if arm.guard.is_some() {
120         return false;
121     }
122     match arm.pat.kind {
123         PatKind::Binding(..) | PatKind::Wild => true,
124         PatKind::Path(ref qpath) => is_res_lang_ctor(cx, cx.qpath_res(qpath, arm.pat.hir_id), OptionNone),
125         _ => false,
126     }
127 }
128
129 fn find_pat_binding_and_is_innermost_parent_pat_struct(pat: &Pat<'_>, hir_id: HirId) -> (Option<Span>, bool) {
130     let mut span = None;
131     let mut is_innermost_parent_pat_struct = false;
132     pat.walk_short(|p| match &p.kind {
133         // ignore OR patterns
134         PatKind::Or(_) => false,
135         PatKind::Binding(_bm, _, _ident, _) => {
136             let found = p.hir_id == hir_id;
137             if found {
138                 span = Some(p.span);
139             }
140             !found
141         },
142         _ => {
143             is_innermost_parent_pat_struct = matches!(p.kind, PatKind::Struct(..));
144             true
145         },
146     });
147     (span, is_innermost_parent_pat_struct)
148 }
149
150 fn pat_contains_or(pat: &Pat<'_>) -> bool {
151     let mut result = false;
152     pat.walk(|p| {
153         let is_or = matches!(p.kind, PatKind::Or(_));
154         result |= is_or;
155         !is_or
156     });
157     result
158 }