Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

single_match: Don't lint non-exhaustive matches; support tuples #8322

Merged
merged 8 commits into from
Jan 30, 2022
131 changes: 113 additions & 18 deletions clippy_lints/src/matches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use rustc_semver::RustcVersion;
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::source_map::{Span, Spanned};
use rustc_span::{sym, symbol::kw};
use std::cmp::Ordering;
use std::cmp::{max, Ordering};
use std::collections::hash_map::Entry;

declare_clippy_lint! {
Expand Down Expand Up @@ -830,12 +830,12 @@ fn report_single_match_single_pattern(
);
}

fn check_single_match_opt_like(
cx: &LateContext<'_>,
fn check_single_match_opt_like<'a>(
cx: &LateContext<'a>,
ex: &Expr<'_>,
arms: &[Arm<'_>],
expr: &Expr<'_>,
ty: Ty<'_>,
ty: Ty<'a>,
els: Option<&Expr<'_>>,
) {
// list of candidate `Enum`s we know will never get any more members
Expand All @@ -849,25 +849,120 @@ fn check_single_match_opt_like(
(&paths::RESULT, "Ok"),
];

let path = match arms[1].pat.kind {
PatKind::TupleStruct(ref path, inner, _) => {
// Contains any non wildcard patterns (e.g., `Err(err)`)?
if !inner.iter().all(is_wild) {
return;
// We want to suggest to exclude an arm that contains only wildcards or forms the exhaustive
// match with the second branch, without enum variants in matches.
if !contains_only_wilds(arms[1].pat) && !form_exhaustive_matches(arms[0].pat, arms[1].pat) {
return;
}

let mut paths_and_types = Vec::new();
if !collect_pat_paths(&mut paths_and_types, cx, arms[1].pat, ty) {
return;
}

let in_candidate_enum = |path_info: &(String, &TyS<'_>)| -> bool {
let (path, ty) = path_info;
for &(ty_path, pat_path) in candidates {
if path == pat_path && match_type(cx, ty, ty_path) {
return true;
}
rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false))
}
false
};
if paths_and_types.iter().all(in_candidate_enum) {
report_single_match_single_pattern(cx, ex, arms, expr, els);
}
}

/// Collects paths and their types from the given patterns. Returns true if the given pattern could
/// be simplified, false otherwise.
fn collect_pat_paths<'a>(acc: &mut Vec<(String, Ty<'a>)>, cx: &LateContext<'a>, pat: &Pat<'_>, ty: Ty<'a>) -> bool {
match pat.kind {
PatKind::Wild => true,
PatKind::Tuple(inner, _) => inner.iter().all(|p| {
let p_ty = cx.typeck_results().pat_ty(p);
collect_pat_paths(acc, cx, p, p_ty)
}),
PatKind::TupleStruct(ref path, ..) => {
let path = rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| {
s.print_qpath(path, false);
});
acc.push((path, ty));
true
},
PatKind::Binding(BindingAnnotation::Unannotated, .., ident, None) => {
acc.push((ident.to_string(), ty));
true
},
PatKind::Binding(BindingAnnotation::Unannotated, .., ident, None) => ident.to_string(),
PatKind::Path(ref path) => {
rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false))
let path = rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| {
s.print_qpath(path, false);
});
acc.push((path, ty));
true
},
_ => return,
};
_ => false,
}
}

for &(ty_path, pat_path) in candidates {
if path == *pat_path && match_type(cx, ty, ty_path) {
report_single_match_single_pattern(cx, ex, arms, expr, els);
}
/// Returns true if the given arm of pattern matching contains wildcard patterns.
fn contains_only_wilds(pat: &Pat<'_>) -> bool {
match pat.kind {
PatKind::Wild => true,
PatKind::Tuple(inner, _) | PatKind::TupleStruct(_, inner, ..) => inner.iter().all(contains_only_wilds),
_ => false,
}
}

/// Returns true if the given patterns forms only exhaustive matches that don't contain enum
/// patterns without a wildcard.
fn form_exhaustive_matches(left: &Pat<'_>, right: &Pat<'_>) -> bool {
match (&left.kind, &right.kind) {
(PatKind::Wild, _) | (_, PatKind::Wild) => true,
(PatKind::Tuple(left_in, left_pos), PatKind::Tuple(right_in, right_pos)) => {
// We don't actually know the position and the presence of the `..` (dotdot) operator
// in the arms, so we need to evaluate the correct offsets here in order to iterate in
// both arms at the same time.
let len = max(
left_in.len() + {
if left_pos.is_some() { 1 } else { 0 }
},
right_in.len() + {
if right_pos.is_some() { 1 } else { 0 }
},
);
let mut left_pos = left_pos.unwrap_or(usize::MAX);
let mut right_pos = right_pos.unwrap_or(usize::MAX);
let mut left_dot_space = 0;
let mut right_dot_space = 0;
for i in 0..len {
let mut found_dotdot = false;
if i == left_pos {
left_dot_space += 1;
if left_dot_space < len - left_in.len() {
left_pos += 1;
}
found_dotdot = true;
}
if i == right_pos {
right_dot_space += 1;
if right_dot_space < len - right_in.len() {
right_pos += 1;
}
found_dotdot = true;
}
if found_dotdot {
continue;
}
if !contains_only_wilds(&left_in[i - left_dot_space])
&& !contains_only_wilds(&right_in[i - right_dot_space])
{
return false;
}
}
true
},
_ => false,
}
}

Expand Down
78 changes: 78 additions & 0 deletions tests/ui/single_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,84 @@ fn if_suggestion() {
};
}

// See: issue #8282
fn ranges() {
enum E {
V,
}
let x = (Some(E::V), Some(42));

// Don't lint, because the `E` enum can be extended with additional fields later. Thus, the
// proposed replacement to `if let Some(E::V)` may hide non-exhaustive warnings that appeared
// because of `match` construction.
match x {
(Some(E::V), _) => {},
(None, _) => {},
}

// lint
match x {
(Some(_), _) => {},
(None, _) => {},
}

// lint
match x {
(Some(E::V), _) => todo!(),
(_, _) => {},
}

// lint
match (Some(42), Some(E::V), Some(42)) {
(.., Some(E::V), _) => {},
(..) => {},
}

// Don't lint, see above.
match (Some(E::V), Some(E::V), Some(E::V)) {
(.., Some(E::V), _) => {},
(.., None, _) => {},
}

// Don't lint, see above.
match (Some(E::V), Some(E::V), Some(E::V)) {
(Some(E::V), ..) => {},
(None, ..) => {},
}

// Don't lint, see above.
match (Some(E::V), Some(E::V), Some(E::V)) {
(_, Some(E::V), ..) => {},
(_, None, ..) => {},
}
}

fn skip_type_aliases() {
enum OptionEx {
Some(i32),
None,
}
enum ResultEx {
Err(i32),
Ok(i32),
}

use OptionEx::{None, Some};
use ResultEx::{Err, Ok};

// don't lint
match Err(42) {
Ok(_) => dummy(),
Err(_) => (),
};

// don't lint
match Some(1i32) {
Some(_) => dummy(),
None => (),
};
}

macro_rules! single_match {
($num:literal) => {
match $num {
Expand Down
38 changes: 28 additions & 10 deletions tests/ui/single_match.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,6 @@ LL | | _ => {},
LL | | };
| |_____^ help: try this: `if let (2..=3, 7..=9) = z { dummy() }`

error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
--> $DIR/single_match.rs:54:5
|
LL | / match x {
LL | | Some(y) => dummy(),
LL | | None => (),
LL | | };
| |_____^ help: try this: `if let Some(y) = x { dummy() }`

error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
--> $DIR/single_match.rs:59:5
|
Expand Down Expand Up @@ -128,5 +119,32 @@ LL | | _ => (),
LL | | };
| |_____^ help: try this: `if let None = x { println!() }`

error: aborting due to 13 previous errors
error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
--> $DIR/single_match.rs:164:5
|
LL | / match x {
LL | | (Some(_), _) => {},
LL | | (None, _) => {},
LL | | }
| |_____^ help: try this: `if let (Some(_), _) = x {}`

error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
--> $DIR/single_match.rs:170:5
|
LL | / match x {
LL | | (Some(E::V), _) => todo!(),
LL | | (_, _) => {},
LL | | }
| |_____^ help: try this: `if let (Some(E::V), _) = x { todo!() }`

error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
--> $DIR/single_match.rs:176:5
|
LL | / match (Some(42), Some(E::V), Some(42)) {
LL | | (.., Some(E::V), _) => {},
LL | | (..) => {},
LL | | }
| |_____^ help: try this: `if let (.., Some(E::V), _) = (Some(42), Some(E::V), Some(42)) {}`

error: aborting due to 15 previous errors

42 changes: 1 addition & 41 deletions tests/ui/single_match_else.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,5 @@ LL + None
LL + }
|

error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
--> $DIR/single_match_else.rs:70:5
|
LL | / match Some(1) {
LL | | Some(a) => println!("${:?}", a),
LL | | None => {
LL | | println!("else block");
LL | | return
LL | | },
LL | | }
| |_____^
|
help: try this
|
LL ~ if let Some(a) = Some(1) { println!("${:?}", a) } else {
LL + println!("else block");
LL + return
LL + }
|

error: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`
--> $DIR/single_match_else.rs:79:5
|
LL | / match Some(1) {
LL | | Some(a) => println!("${:?}", a),
LL | | None => {
LL | | println!("else block");
LL | | return;
LL | | },
LL | | }
| |_____^
|
help: try this
|
LL ~ if let Some(a) = Some(1) { println!("${:?}", a) } else {
LL + println!("else block");
LL + return;
LL + }
|

error: aborting due to 3 previous errors
error: aborting due to previous error