]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/matches/match_same_arms.rs
Split out `overlapping_arms`
[rust.git] / clippy_lints / src / matches / match_same_arms.rs
1 use clippy_utils::diagnostics::span_lint_and_then;
2 use clippy_utils::source::snippet;
3 use clippy_utils::{path_to_local, search_same, SpanlessEq, SpanlessHash};
4 use rustc_hir::{Arm, Expr, ExprKind, HirId, HirIdMap, HirIdSet, MatchSource, Pat, PatKind};
5 use rustc_lint::LateContext;
6 use rustc_middle::ty::TyS;
7 use std::collections::hash_map::Entry;
8
9 use super::MATCH_SAME_ARMS;
10
11 pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) {
12     if let ExprKind::Match(_, arms, MatchSource::Normal) = expr.kind {
13         let hash = |&(_, arm): &(usize, &Arm<'_>)| -> u64 {
14             let mut h = SpanlessHash::new(cx);
15             h.hash_expr(arm.body);
16             h.finish()
17         };
18
19         let eq = |&(lindex, lhs): &(usize, &Arm<'_>), &(rindex, rhs): &(usize, &Arm<'_>)| -> bool {
20             let min_index = usize::min(lindex, rindex);
21             let max_index = usize::max(lindex, rindex);
22
23             let mut local_map: HirIdMap<HirId> = HirIdMap::default();
24             let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| {
25                 if_chain! {
26                     if let Some(a_id) = path_to_local(a);
27                     if let Some(b_id) = path_to_local(b);
28                     let entry = match local_map.entry(a_id) {
29                         Entry::Vacant(entry) => entry,
30                         // check if using the same bindings as before
31                         Entry::Occupied(entry) => return *entry.get() == b_id,
32                     };
33                     // the names technically don't have to match; this makes the lint more conservative
34                     if cx.tcx.hir().name(a_id) == cx.tcx.hir().name(b_id);
35                     if TyS::same_type(cx.typeck_results().expr_ty(a), cx.typeck_results().expr_ty(b));
36                     if pat_contains_local(lhs.pat, a_id);
37                     if pat_contains_local(rhs.pat, b_id);
38                     then {
39                         entry.insert(b_id);
40                         true
41                     } else {
42                         false
43                     }
44                 }
45             };
46             // Arms with a guard are ignored, those can’t always be merged together
47             // This is also the case for arms in-between each there is an arm with a guard
48             (min_index..=max_index).all(|index| arms[index].guard.is_none())
49                 && SpanlessEq::new(cx)
50                     .expr_fallback(eq_fallback)
51                     .eq_expr(lhs.body, rhs.body)
52                 // these checks could be removed to allow unused bindings
53                 && bindings_eq(lhs.pat, local_map.keys().copied().collect())
54                 && bindings_eq(rhs.pat, local_map.values().copied().collect())
55         };
56
57         let indexed_arms: Vec<(usize, &Arm<'_>)> = arms.iter().enumerate().collect();
58         for (&(_, i), &(_, j)) in search_same(&indexed_arms, hash, eq) {
59             span_lint_and_then(
60                 cx,
61                 MATCH_SAME_ARMS,
62                 j.body.span,
63                 "this `match` has identical arm bodies",
64                 |diag| {
65                     diag.span_note(i.body.span, "same as this");
66
67                     // Note: this does not use `span_suggestion` on purpose:
68                     // there is no clean way
69                     // to remove the other arm. Building a span and suggest to replace it to ""
70                     // makes an even more confusing error message. Also in order not to make up a
71                     // span for the whole pattern, the suggestion is only shown when there is only
72                     // one pattern. The user should know about `|` if they are already using it…
73
74                     let lhs = snippet(cx, i.pat.span, "<pat1>");
75                     let rhs = snippet(cx, j.pat.span, "<pat2>");
76
77                     if let PatKind::Wild = j.pat.kind {
78                         // if the last arm is _, then i could be integrated into _
79                         // note that i.pat cannot be _, because that would mean that we're
80                         // hiding all the subsequent arms, and rust won't compile
81                         diag.span_note(
82                             i.body.span,
83                             &format!(
84                                 "`{}` has the same arm body as the `_` wildcard, consider removing it",
85                                 lhs
86                             ),
87                         );
88                     } else {
89                         diag.span_help(i.pat.span, &format!("consider refactoring into `{} | {}`", lhs, rhs,))
90                             .help("...or consider changing the match arm bodies");
91                     }
92                 },
93             );
94         }
95     }
96 }
97
98 fn pat_contains_local(pat: &Pat<'_>, id: HirId) -> bool {
99     let mut result = false;
100     pat.walk_short(|p| {
101         result |= matches!(p.kind, PatKind::Binding(_, binding_id, ..) if binding_id == id);
102         !result
103     });
104     result
105 }
106
107 /// Returns true if all the bindings in the `Pat` are in `ids` and vice versa
108 fn bindings_eq(pat: &Pat<'_>, mut ids: HirIdSet) -> bool {
109     let mut result = true;
110     pat.each_binding_or_first(&mut |_, id, _, _| result &= ids.remove(&id));
111     result && ids.is_empty()
112 }