diff --git a/clippy_lints/src/matches.rs b/clippy_lints/src/matches.rs index 2579404fb18..e0cbadeb645 100644 --- a/clippy_lints/src/matches.rs +++ b/clippy_lints/src/matches.rs @@ -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! { @@ -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 @@ -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; - } - rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false)) - }, - 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)) - }, - _ => 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; + } - 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); + 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; + } } + 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::Path(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 + }, + _ => false, + } +} + +/// 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, } } diff --git a/tests/ui/single_match.rs b/tests/ui/single_match.rs index b1819e08d53..bd371888046 100644 --- a/tests/ui/single_match.rs +++ b/tests/ui/single_match.rs @@ -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 { diff --git a/tests/ui/single_match.stderr b/tests/ui/single_match.stderr index c261b5111c8..318faf25717 100644 --- a/tests/ui/single_match.stderr +++ b/tests/ui/single_match.stderr @@ -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 | @@ -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 diff --git a/tests/ui/single_match_else.stderr b/tests/ui/single_match_else.stderr index c61d80a905c..21ea704b62a 100644 --- a/tests/ui/single_match_else.stderr +++ b/tests/ui/single_match_else.stderr @@ -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