Skip to content

Commit 7b89fa9

Browse files
committed
Auto merge of #8322 - jubnzv:8282-single-match, r=llogiq
single_match: Don't lint non-exhaustive matches; support tuples `single_match` lint: * Don't lint exhaustive enum patterns without a wild. Rationale: The definition of the enum could be changed, so the user can get non-exhaustive match after applying the suggested lint (see #8282 (comment) for context). * Lint `match` constructions with tuples (as suggested at #8282 (comment)) Closes #8282
2 parents 7ceffde + a8fdf5c commit 7b89fa9

File tree

4 files changed

+220
-69
lines changed

4 files changed

+220
-69
lines changed

clippy_lints/src/matches.rs

Lines changed: 113 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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, symbol::kw};
34-
use std::cmp::Ordering;
34+
use std::cmp::{max, Ordering};
3535
use std::collections::hash_map::Entry;
3636

3737
declare_clippy_lint! {
@@ -830,12 +830,12 @@ fn report_single_match_single_pattern(
830830
);
831831
}
832832

833-
fn check_single_match_opt_like(
834-
cx: &LateContext<'_>,
833+
fn check_single_match_opt_like<'a>(
834+
cx: &LateContext<'a>,
835835
ex: &Expr<'_>,
836836
arms: &[Arm<'_>],
837837
expr: &Expr<'_>,
838-
ty: Ty<'_>,
838+
ty: Ty<'a>,
839839
els: Option<&Expr<'_>>,
840840
) {
841841
// list of candidate `Enum`s we know will never get any more members
@@ -849,25 +849,120 @@ fn check_single_match_opt_like(
849849
(&paths::RESULT, "Ok"),
850850
];
851851

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;
852+
// We want to suggest to exclude an arm that contains only wildcards or forms the exhaustive
853+
// match with the second branch, without enum variants in matches.
854+
if !contains_only_wilds(arms[1].pat) && !form_exhaustive_matches(arms[0].pat, arms[1].pat) {
855+
return;
856+
}
857+
858+
let mut paths_and_types = Vec::new();
859+
if !collect_pat_paths(&mut paths_and_types, cx, arms[1].pat, ty) {
860+
return;
861+
}
862+
863+
let in_candidate_enum = |path_info: &(String, &TyS<'_>)| -> bool {
864+
let (path, ty) = path_info;
865+
for &(ty_path, pat_path) in candidates {
866+
if path == pat_path && match_type(cx, ty, ty_path) {
867+
return true;
857868
}
858-
rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false))
869+
}
870+
false
871+
};
872+
if paths_and_types.iter().all(in_candidate_enum) {
873+
report_single_match_single_pattern(cx, ex, arms, expr, els);
874+
}
875+
}
876+
877+
/// Collects paths and their types from the given patterns. Returns true if the given pattern could
878+
/// be simplified, false otherwise.
879+
fn collect_pat_paths<'a>(acc: &mut Vec<(String, Ty<'a>)>, cx: &LateContext<'a>, pat: &Pat<'_>, ty: Ty<'a>) -> bool {
880+
match pat.kind {
881+
PatKind::Wild => true,
882+
PatKind::Tuple(inner, _) => inner.iter().all(|p| {
883+
let p_ty = cx.typeck_results().pat_ty(p);
884+
collect_pat_paths(acc, cx, p, p_ty)
885+
}),
886+
PatKind::TupleStruct(ref path, ..) => {
887+
let path = rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| {
888+
s.print_qpath(path, false);
889+
});
890+
acc.push((path, ty));
891+
true
892+
},
893+
PatKind::Binding(BindingAnnotation::Unannotated, .., ident, None) => {
894+
acc.push((ident.to_string(), ty));
895+
true
859896
},
860-
PatKind::Binding(BindingAnnotation::Unannotated, .., ident, None) => ident.to_string(),
861897
PatKind::Path(ref path) => {
862-
rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false))
898+
let path = rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| {
899+
s.print_qpath(path, false);
900+
});
901+
acc.push((path, ty));
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 forms only exhaustive matches that don't contain enum
918+
/// patterns without a wildcard.
919+
fn form_exhaustive_matches(left: &Pat<'_>, right: &Pat<'_>) -> bool {
920+
match (&left.kind, &right.kind) {
921+
(PatKind::Wild, _) | (_, PatKind::Wild) => true,
922+
(PatKind::Tuple(left_in, left_pos), PatKind::Tuple(right_in, right_pos)) => {
923+
// We don't actually know the position and the presence of the `..` (dotdot) operator
924+
// in the arms, so we need to evaluate the correct offsets here in order to iterate in
925+
// both arms at the same time.
926+
let len = max(
927+
left_in.len() + {
928+
if left_pos.is_some() { 1 } else { 0 }
929+
},
930+
right_in.len() + {
931+
if right_pos.is_some() { 1 } else { 0 }
932+
},
933+
);
934+
let mut left_pos = left_pos.unwrap_or(usize::MAX);
935+
let mut right_pos = right_pos.unwrap_or(usize::MAX);
936+
let mut left_dot_space = 0;
937+
let mut right_dot_space = 0;
938+
for i in 0..len {
939+
let mut found_dotdot = false;
940+
if i == left_pos {
941+
left_dot_space += 1;
942+
if left_dot_space < len - left_in.len() {
943+
left_pos += 1;
944+
}
945+
found_dotdot = true;
946+
}
947+
if i == right_pos {
948+
right_dot_space += 1;
949+
if right_dot_space < len - right_in.len() {
950+
right_pos += 1;
951+
}
952+
found_dotdot = true;
953+
}
954+
if found_dotdot {
955+
continue;
956+
}
957+
if !contains_only_wilds(&left_in[i - left_dot_space])
958+
&& !contains_only_wilds(&right_in[i - right_dot_space])
959+
{
960+
return false;
961+
}
962+
}
963+
true
964+
},
965+
_ => false,
871966
}
872967
}
873968

tests/ui/single_match.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,84 @@ 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, because the `E` enum can be extended with additional fields later. Thus, the
156+
// proposed replacement to `if let Some(E::V)` may hide non-exhaustive warnings that appeared
157+
// because of `match` construction.
158+
match x {
159+
(Some(E::V), _) => {},
160+
(None, _) => {},
161+
}
162+
163+
// lint
164+
match x {
165+
(Some(_), _) => {},
166+
(None, _) => {},
167+
}
168+
169+
// lint
170+
match x {
171+
(Some(E::V), _) => todo!(),
172+
(_, _) => {},
173+
}
174+
175+
// lint
176+
match (Some(42), Some(E::V), Some(42)) {
177+
(.., Some(E::V), _) => {},
178+
(..) => {},
179+
}
180+
181+
// Don't lint, see above.
182+
match (Some(E::V), Some(E::V), Some(E::V)) {
183+
(.., Some(E::V), _) => {},
184+
(.., None, _) => {},
185+
}
186+
187+
// Don't lint, see above.
188+
match (Some(E::V), Some(E::V), Some(E::V)) {
189+
(Some(E::V), ..) => {},
190+
(None, ..) => {},
191+
}
192+
193+
// Don't lint, see above.
194+
match (Some(E::V), Some(E::V), Some(E::V)) {
195+
(_, Some(E::V), ..) => {},
196+
(_, None, ..) => {},
197+
}
198+
}
199+
200+
fn skip_type_aliases() {
201+
enum OptionEx {
202+
Some(i32),
203+
None,
204+
}
205+
enum ResultEx {
206+
Err(i32),
207+
Ok(i32),
208+
}
209+
210+
use OptionEx::{None, Some};
211+
use ResultEx::{Err, Ok};
212+
213+
// don't lint
214+
match Err(42) {
215+
Ok(_) => dummy(),
216+
Err(_) => (),
217+
};
218+
219+
// don't lint
220+
match Some(1i32) {
221+
Some(_) => dummy(),
222+
None => (),
223+
};
224+
}
225+
148226
macro_rules! single_match {
149227
($num:literal) => {
150228
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:164: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:170: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:176: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)