Skip to content

Commit a5a07e5

Browse files
committed
single_match: Don't lint non-exhaustive matches; support tuples
This commit changes the behavior of `single_match` lint. After that, we won't lint non-exhaustive matches like this: ```rust match Some(v) { Some(a) => println!("${:?}", a), None => {}, } ``` The rationale is that, because the type of `a` could be changed, so the user can get non-exhaustive match after applying the suggested lint (see #8282 (comment) for context). We also will lint `match` constructions with tuples. When we see the tuples on the both arms, we will check them both at the same time, and if they form exhaustive match, we could display the warning. Closes #8282
1 parent 496f26c commit a5a07e5

File tree

4 files changed

+206
-69
lines changed

4 files changed

+206
-69
lines changed

clippy_lints/src/matches.rs

Lines changed: 127 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use clippy_utils::diagnostics::{
55
use clippy_utils::macros::{is_panic, root_macro_call};
66
use clippy_utils::source::{expr_block, indent_of, snippet, snippet_block, snippet_opt, snippet_with_applicability};
77
use clippy_utils::sugg::Sugg;
8-
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, match_type, peel_mid_ty_refs};
8+
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, peel_mid_ty_refs};
99
use clippy_utils::visitors::is_local_used;
1010
use clippy_utils::{
1111
get_parent_expr, is_lang_ctor, is_lint_allowed, is_refutable, is_unit_expr, is_wild, meets_msrv, msrvs,
@@ -31,7 +31,7 @@ use rustc_semver::RustcVersion;
3131
use rustc_session::{declare_tool_lint, impl_lint_pass};
3232
use rustc_span::source_map::{Span, Spanned};
3333
use rustc_span::sym;
34-
use std::cmp::Ordering;
34+
use std::cmp::{max, Ordering};
3535
use std::collections::hash_map::Entry;
3636

3737
declare_clippy_lint! {
@@ -741,7 +741,7 @@ fn check_single_match(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], exp
741741
let ty = cx.typeck_results().expr_ty(ex);
742742
if *ty.kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id) {
743743
check_single_match_single_pattern(cx, ex, arms, expr, els);
744-
check_single_match_opt_like(cx, ex, arms, expr, ty, els);
744+
check_single_match_opt_like(cx, ex, arms, expr, els);
745745
}
746746
}
747747
}
@@ -835,7 +835,6 @@ fn check_single_match_opt_like(
835835
ex: &Expr<'_>,
836836
arms: &[Arm<'_>],
837837
expr: &Expr<'_>,
838-
ty: Ty<'_>,
839838
els: Option<&Expr<'_>>,
840839
) {
841840
// list of candidate `Enum`s we know will never get any more members
@@ -849,25 +848,135 @@ fn check_single_match_opt_like(
849848
(&paths::RESULT, "Ok"),
850849
];
851850

852-
let path = match arms[1].pat.kind {
853-
PatKind::TupleStruct(ref path, inner, _) => {
854-
// Contains any non wildcard patterns (e.g., `Err(err)`)?
855-
if !inner.iter().all(is_wild) {
856-
return;
851+
// We want to suggest to exclude an arm that contains only wildcards or forms the ehaustive
852+
// match with the second branch.
853+
if !contains_only_wilds(arms[1].pat) && !form_exhaustive_tuples(arms[0].pat, arms[1].pat) {
854+
return;
855+
}
856+
857+
let mut paths = Vec::new();
858+
if !collect_pat_paths(&mut paths, arms[1].pat) {
859+
return;
860+
}
861+
862+
let in_candidate_enum = |path: &String| -> bool {
863+
for &(_, pat_path) in candidates {
864+
if path == pat_path {
865+
return true;
866+
}
867+
}
868+
false
869+
};
870+
if paths.iter().all(in_candidate_enum) {
871+
report_single_match_single_pattern(cx, ex, arms, expr, els);
872+
}
873+
}
874+
875+
/// Collects paths from the given paths. Returns true if the given pattern could be simplified,
876+
/// false otherwise.
877+
fn collect_pat_paths(acc: &mut Vec<String>, pat: &Pat<'_>) -> bool {
878+
match pat.kind {
879+
PatKind::Wild => true,
880+
PatKind::Tuple(inner, _) => {
881+
for p in inner {
882+
if !collect_pat_paths(acc, p) {
883+
return false;
884+
}
857885
}
858-
rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false))
886+
true
887+
},
888+
PatKind::TupleStruct(ref path, ..) => {
889+
acc.push(rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| {
890+
s.print_qpath(path, false);
891+
}));
892+
true
893+
},
894+
PatKind::Binding(BindingAnnotation::Unannotated, .., ident, None) => {
895+
acc.push(ident.to_string());
896+
true
859897
},
860-
PatKind::Binding(BindingAnnotation::Unannotated, .., ident, None) => ident.to_string(),
861898
PatKind::Path(ref path) => {
862-
rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false))
899+
acc.push(rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| {
900+
s.print_qpath(path, false);
901+
}));
902+
true
863903
},
864-
_ => return,
865-
};
904+
_ => false,
905+
}
906+
}
866907

867-
for &(ty_path, pat_path) in candidates {
868-
if path == *pat_path && match_type(cx, ty, ty_path) {
869-
report_single_match_single_pattern(cx, ex, arms, expr, els);
870-
}
908+
/// Returns true if the given arm of pattern matching contains wildcard patterns.
909+
fn contains_only_wilds(pat: &Pat<'_>) -> bool {
910+
match pat.kind {
911+
PatKind::Wild => true,
912+
PatKind::Tuple(inner, _) | PatKind::TupleStruct(_, inner, ..) => inner.iter().all(contains_only_wilds),
913+
_ => false,
914+
}
915+
}
916+
917+
/// Returns true if the given patterns form the tuples that exhaustively matches all possible
918+
/// parameters.
919+
///
920+
/// Here are some examples:
921+
///
922+
/// ```
923+
/// // Doesn't form exhaustive match, because the first arm may contain not only E::V.
924+
/// match x {
925+
/// (Some(E::V), _) => todo!(),
926+
/// (None, _) => {}
927+
/// }
928+
///
929+
/// // Forms exhaustive match, because the patterns cover all possible cases at the same positions.
930+
/// match x {
931+
/// (Some(_), _) => todo!(),
932+
/// (None, _) => {}
933+
/// }
934+
/// ```
935+
fn form_exhaustive_tuples(left: &Pat<'_>, right: &Pat<'_>) -> bool {
936+
match (&left.kind, &right.kind) {
937+
(PatKind::Wild, _) | (_, PatKind::Wild) => true,
938+
(PatKind::Tuple(left_in, left_pos), PatKind::Tuple(right_in, right_pos)) => {
939+
// We don't actually know the position and presence of the `..` (dotdot) operator in
940+
// the arms, so we need to evaluate the correct offsets here in order to iterate in
941+
// both arms at the same time.
942+
let len = max(
943+
left_in.len() + {
944+
if left_pos.is_some() { 1 } else { 0 }
945+
},
946+
right_in.len() + {
947+
if right_pos.is_some() { 1 } else { 0 }
948+
},
949+
);
950+
let mut left_pos = left_pos.unwrap_or(usize::MAX);
951+
let mut right_pos = right_pos.unwrap_or(usize::MAX);
952+
let mut left_span = 0;
953+
let mut right_span = 0;
954+
for i in 0..len {
955+
let mut found_dotdot = false;
956+
if i == left_pos {
957+
left_span += 1;
958+
if left_span < len - left_in.len() {
959+
left_pos += 1;
960+
}
961+
found_dotdot = true;
962+
}
963+
if i == right_pos {
964+
right_span += 1;
965+
if right_span < len - right_in.len() {
966+
right_pos += 1;
967+
}
968+
found_dotdot = true;
969+
}
970+
if found_dotdot {
971+
continue;
972+
}
973+
if !contains_only_wilds(&left_in[i - left_span]) && !contains_only_wilds(&right_in[i - right_span]) {
974+
return false;
975+
}
976+
}
977+
true
978+
},
979+
_ => false,
871980
}
872981
}
873982

tests/ui/single_match.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,56 @@ fn if_suggestion() {
145145
};
146146
}
147147

148+
// See: issue #8282
149+
fn ranges() {
150+
enum E {
151+
V,
152+
}
153+
let x = (Some(E::V), Some(42));
154+
155+
// don't lint
156+
match x {
157+
(Some(E::V), _) => {},
158+
(None, _) => {},
159+
}
160+
161+
// lint
162+
match x {
163+
(Some(_), _) => {},
164+
(None, _) => {},
165+
}
166+
167+
// lint
168+
match x {
169+
(Some(E::V), _) => todo!(),
170+
(_, _) => {},
171+
}
172+
173+
// lint
174+
match (Some(42), Some(E::V), Some(42)) {
175+
(.., Some(E::V), _) => {},
176+
(..) => {},
177+
}
178+
179+
// don't lint
180+
match (Some(E::V), Some(E::V), Some(E::V)) {
181+
(.., Some(E::V), _) => {},
182+
(.., None, _) => {},
183+
}
184+
185+
// don't lint
186+
match (Some(E::V), Some(E::V), Some(E::V)) {
187+
(Some(E::V), ..) => {},
188+
(None, ..) => {},
189+
}
190+
191+
// don't lint
192+
match (Some(E::V), Some(E::V), Some(E::V)) {
193+
(_, Some(E::V), ..) => {},
194+
(_, None, ..) => {},
195+
}
196+
}
197+
148198
macro_rules! single_match {
149199
($num:literal) => {
150200
match $num {

tests/ui/single_match.stderr

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,6 @@ LL | | _ => {},
3838
LL | | };
3939
| |_____^ help: try this: `if let (2..=3, 7..=9) = z { dummy() }`
4040

41-
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
42-
--> $DIR/single_match.rs:54:5
43-
|
44-
LL | / match x {
45-
LL | | Some(y) => dummy(),
46-
LL | | None => (),
47-
LL | | };
48-
| |_____^ help: try this: `if let Some(y) = x { dummy() }`
49-
5041
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
5142
--> $DIR/single_match.rs:59:5
5243
|
@@ -128,5 +119,32 @@ LL | | _ => (),
128119
LL | | };
129120
| |_____^ help: try this: `if let None = x { println!() }`
130121

131-
error: aborting due to 13 previous errors
122+
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
123+
--> $DIR/single_match.rs:162:5
124+
|
125+
LL | / match x {
126+
LL | | (Some(_), _) => {},
127+
LL | | (None, _) => {},
128+
LL | | }
129+
| |_____^ help: try this: `if let (Some(_), _) = x {}`
130+
131+
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
132+
--> $DIR/single_match.rs:168:5
133+
|
134+
LL | / match x {
135+
LL | | (Some(E::V), _) => todo!(),
136+
LL | | (_, _) => {},
137+
LL | | }
138+
| |_____^ help: try this: `if let (Some(E::V), _) = x { todo!() }`
139+
140+
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
141+
--> $DIR/single_match.rs:174:5
142+
|
143+
LL | / match (Some(42), Some(E::V), Some(42)) {
144+
LL | | (.., Some(E::V), _) => {},
145+
LL | | (..) => {},
146+
LL | | }
147+
| |_____^ help: try this: `if let (.., Some(E::V), _) = (Some(42), Some(E::V), Some(42)) {}`
148+
149+
error: aborting due to 15 previous errors
132150

tests/ui/single_match_else.stderr

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,5 @@ LL + None
1919
LL + }
2020
|
2121

22-
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
23-
--> $DIR/single_match_else.rs:70:5
24-
|
25-
LL | / match Some(1) {
26-
LL | | Some(a) => println!("${:?}", a),
27-
LL | | None => {
28-
LL | | println!("else block");
29-
LL | | return
30-
LL | | },
31-
LL | | }
32-
| |_____^
33-
|
34-
help: try this
35-
|
36-
LL ~ if let Some(a) = Some(1) { println!("${:?}", a) } else {
37-
LL + println!("else block");
38-
LL + return
39-
LL + }
40-
|
41-
42-
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
43-
--> $DIR/single_match_else.rs:79:5
44-
|
45-
LL | / match Some(1) {
46-
LL | | Some(a) => println!("${:?}", a),
47-
LL | | None => {
48-
LL | | println!("else block");
49-
LL | | return;
50-
LL | | },
51-
LL | | }
52-
| |_____^
53-
|
54-
help: try this
55-
|
56-
LL ~ if let Some(a) = Some(1) { println!("${:?}", a) } else {
57-
LL + println!("else block");
58-
LL + return;
59-
LL + }
60-
|
61-
62-
error: aborting due to 3 previous errors
22+
error: aborting due to previous error
6323

0 commit comments

Comments
 (0)