diff --git a/clippy_lints/src/matches.rs b/clippy_lints/src/matches.rs deleted file mode 100644 index 67fb918d20c0..000000000000 --- a/clippy_lints/src/matches.rs +++ /dev/null @@ -1,2445 +0,0 @@ -use clippy_utils::consts::{constant, constant_full_int, miri_to_const, FullInt}; -use clippy_utils::diagnostics::{ - multispan_sugg, span_lint_and_help, span_lint_and_note, span_lint_and_sugg, span_lint_and_then, -}; -use clippy_utils::macros::{is_panic, root_macro_call}; -use clippy_utils::source::{expr_block, indent_of, snippet, snippet_block, snippet_opt, snippet_with_applicability}; -use clippy_utils::sugg::Sugg; -use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, match_type, peel_mid_ty_refs}; -use clippy_utils::visitors::is_local_used; -use clippy_utils::{ - get_parent_expr, is_lang_ctor, is_lint_allowed, is_refutable, is_unit_expr, is_wild, meets_msrv, msrvs, - path_to_local, path_to_local_id, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs, recurse_or_patterns, - strip_pat_refs, -}; -use clippy_utils::{higher, peel_blocks_with_stmt}; -use clippy_utils::{paths, search_same, SpanlessEq, SpanlessHash}; -use core::iter::{once, ExactSizeIterator}; -use if_chain::if_chain; -use rustc_ast::ast::{Attribute, LitKind}; -use rustc_errors::Applicability; -use rustc_hir::def::{CtorKind, DefKind, Res}; -use rustc_hir::LangItem::{OptionNone, OptionSome}; -use rustc_hir::{ - self as hir, Arm, BindingAnnotation, Block, BorrowKind, Expr, ExprKind, Guard, HirId, Local, MatchSource, - Mutability, Node, Pat, PatKind, PathSegment, QPath, RangeEnd, TyKind, -}; -use rustc_hir::{HirIdMap, HirIdSet}; -use rustc_lint::{LateContext, LateLintPass}; -use rustc_middle::ty::{self, Ty, TyS, VariantDef}; -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::{max, Ordering}; -use std::collections::hash_map::Entry; - -declare_clippy_lint! { - /// ### What it does - /// Checks for matches with a single arm where an `if let` - /// will usually suffice. - /// - /// ### Why is this bad? - /// Just readability – `if let` nests less than a `match`. - /// - /// ### Example - /// ```rust - /// # fn bar(stool: &str) {} - /// # let x = Some("abc"); - /// // Bad - /// match x { - /// Some(ref foo) => bar(foo), - /// _ => (), - /// } - /// - /// // Good - /// if let Some(ref foo) = x { - /// bar(foo); - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub SINGLE_MATCH, - style, - "a `match` statement with a single nontrivial arm (i.e., where the other arm is `_ => {}`) instead of `if let`" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for matches with two arms where an `if let else` will - /// usually suffice. - /// - /// ### Why is this bad? - /// Just readability – `if let` nests less than a `match`. - /// - /// ### Known problems - /// Personal style preferences may differ. - /// - /// ### Example - /// Using `match`: - /// - /// ```rust - /// # fn bar(foo: &usize) {} - /// # let other_ref: usize = 1; - /// # let x: Option<&usize> = Some(&1); - /// match x { - /// Some(ref foo) => bar(foo), - /// _ => bar(&other_ref), - /// } - /// ``` - /// - /// Using `if let` with `else`: - /// - /// ```rust - /// # fn bar(foo: &usize) {} - /// # let other_ref: usize = 1; - /// # let x: Option<&usize> = Some(&1); - /// if let Some(ref foo) = x { - /// bar(foo); - /// } else { - /// bar(&other_ref); - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub SINGLE_MATCH_ELSE, - pedantic, - "a `match` statement with two arms where the second arm's pattern is a placeholder instead of a specific match pattern" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for matches where all arms match a reference, - /// suggesting to remove the reference and deref the matched expression - /// instead. It also checks for `if let &foo = bar` blocks. - /// - /// ### Why is this bad? - /// It just makes the code less readable. That reference - /// destructuring adds nothing to the code. - /// - /// ### Example - /// ```rust,ignore - /// // Bad - /// match x { - /// &A(ref y) => foo(y), - /// &B => bar(), - /// _ => frob(&x), - /// } - /// - /// // Good - /// match *x { - /// A(ref y) => foo(y), - /// B => bar(), - /// _ => frob(x), - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MATCH_REF_PATS, - style, - "a `match` or `if let` with all arms prefixed with `&` instead of deref-ing the match expression" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for matches where match expression is a `bool`. It - /// suggests to replace the expression with an `if...else` block. - /// - /// ### Why is this bad? - /// It makes the code less readable. - /// - /// ### Example - /// ```rust - /// # fn foo() {} - /// # fn bar() {} - /// let condition: bool = true; - /// match condition { - /// true => foo(), - /// false => bar(), - /// } - /// ``` - /// Use if/else instead: - /// ```rust - /// # fn foo() {} - /// # fn bar() {} - /// let condition: bool = true; - /// if condition { - /// foo(); - /// } else { - /// bar(); - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MATCH_BOOL, - pedantic, - "a `match` on a boolean expression instead of an `if..else` block" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for overlapping match arms. - /// - /// ### Why is this bad? - /// It is likely to be an error and if not, makes the code - /// less obvious. - /// - /// ### Example - /// ```rust - /// let x = 5; - /// match x { - /// 1..=10 => println!("1 ... 10"), - /// 5..=15 => println!("5 ... 15"), - /// _ => (), - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MATCH_OVERLAPPING_ARM, - style, - "a `match` with overlapping arms" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for arm which matches all errors with `Err(_)` - /// and take drastic actions like `panic!`. - /// - /// ### Why is this bad? - /// It is generally a bad practice, similar to - /// catching all exceptions in java with `catch(Exception)` - /// - /// ### Example - /// ```rust - /// let x: Result = Ok(3); - /// match x { - /// Ok(_) => println!("ok"), - /// Err(_) => panic!("err"), - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MATCH_WILD_ERR_ARM, - pedantic, - "a `match` with `Err(_)` arm and take drastic actions" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for match which is used to add a reference to an - /// `Option` value. - /// - /// ### Why is this bad? - /// Using `as_ref()` or `as_mut()` instead is shorter. - /// - /// ### Example - /// ```rust - /// let x: Option<()> = None; - /// - /// // Bad - /// let r: Option<&()> = match x { - /// None => None, - /// Some(ref v) => Some(v), - /// }; - /// - /// // Good - /// let r: Option<&()> = x.as_ref(); - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MATCH_AS_REF, - complexity, - "a `match` on an Option value instead of using `as_ref()` or `as_mut`" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for wildcard enum matches using `_`. - /// - /// ### Why is this bad? - /// New enum variants added by library updates can be missed. - /// - /// ### Known problems - /// Suggested replacements may be incorrect if guards exhaustively cover some - /// variants, and also may not use correct path to enum if it's not present in the current scope. - /// - /// ### Example - /// ```rust - /// # enum Foo { A(usize), B(usize) } - /// # let x = Foo::B(1); - /// // Bad - /// match x { - /// Foo::A(_) => {}, - /// _ => {}, - /// } - /// - /// // Good - /// match x { - /// Foo::A(_) => {}, - /// Foo::B(_) => {}, - /// } - /// ``` - #[clippy::version = "1.34.0"] - pub WILDCARD_ENUM_MATCH_ARM, - restriction, - "a wildcard enum match arm using `_`" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for wildcard enum matches for a single variant. - /// - /// ### Why is this bad? - /// New enum variants added by library updates can be missed. - /// - /// ### Known problems - /// Suggested replacements may not use correct path to enum - /// if it's not present in the current scope. - /// - /// ### Example - /// ```rust - /// # enum Foo { A, B, C } - /// # let x = Foo::B; - /// // Bad - /// match x { - /// Foo::A => {}, - /// Foo::B => {}, - /// _ => {}, - /// } - /// - /// // Good - /// match x { - /// Foo::A => {}, - /// Foo::B => {}, - /// Foo::C => {}, - /// } - /// ``` - #[clippy::version = "1.45.0"] - pub MATCH_WILDCARD_FOR_SINGLE_VARIANTS, - pedantic, - "a wildcard enum match for a single variant" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for wildcard pattern used with others patterns in same match arm. - /// - /// ### Why is this bad? - /// Wildcard pattern already covers any other pattern as it will match anyway. - /// It makes the code less readable, especially to spot wildcard pattern use in match arm. - /// - /// ### Example - /// ```rust - /// // Bad - /// match "foo" { - /// "a" => {}, - /// "bar" | _ => {}, - /// } - /// - /// // Good - /// match "foo" { - /// "a" => {}, - /// _ => {}, - /// } - /// ``` - #[clippy::version = "1.42.0"] - pub WILDCARD_IN_OR_PATTERNS, - complexity, - "a wildcard pattern used with others patterns in same match arm" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for matches being used to destructure a single-variant enum - /// or tuple struct where a `let` will suffice. - /// - /// ### Why is this bad? - /// Just readability – `let` doesn't nest, whereas a `match` does. - /// - /// ### Example - /// ```rust - /// enum Wrapper { - /// Data(i32), - /// } - /// - /// let wrapper = Wrapper::Data(42); - /// - /// let data = match wrapper { - /// Wrapper::Data(i) => i, - /// }; - /// ``` - /// - /// The correct use would be: - /// ```rust - /// enum Wrapper { - /// Data(i32), - /// } - /// - /// let wrapper = Wrapper::Data(42); - /// let Wrapper::Data(data) = wrapper; - /// ``` - #[clippy::version = "pre 1.29.0"] - pub INFALLIBLE_DESTRUCTURING_MATCH, - style, - "a `match` statement with a single infallible arm instead of a `let`" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for useless match that binds to only one value. - /// - /// ### Why is this bad? - /// Readability and needless complexity. - /// - /// ### Known problems - /// Suggested replacements may be incorrect when `match` - /// is actually binding temporary value, bringing a 'dropped while borrowed' error. - /// - /// ### Example - /// ```rust - /// # let a = 1; - /// # let b = 2; - /// - /// // Bad - /// match (a, b) { - /// (c, d) => { - /// // useless match - /// } - /// } - /// - /// // Good - /// let (c, d) = (a, b); - /// ``` - #[clippy::version = "1.43.0"] - pub MATCH_SINGLE_BINDING, - complexity, - "a match with a single binding instead of using `let` statement" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for unnecessary '..' pattern binding on struct when all fields are explicitly matched. - /// - /// ### Why is this bad? - /// Correctness and readability. It's like having a wildcard pattern after - /// matching all enum variants explicitly. - /// - /// ### Example - /// ```rust - /// # struct A { a: i32 } - /// let a = A { a: 5 }; - /// - /// // Bad - /// match a { - /// A { a: 5, .. } => {}, - /// _ => {}, - /// } - /// - /// // Good - /// match a { - /// A { a: 5 } => {}, - /// _ => {}, - /// } - /// ``` - #[clippy::version = "1.43.0"] - pub REST_PAT_IN_FULLY_BOUND_STRUCTS, - restriction, - "a match on a struct that binds all fields but still uses the wildcard pattern" -} - -declare_clippy_lint! { - /// ### What it does - /// Lint for redundant pattern matching over `Result`, `Option`, - /// `std::task::Poll` or `std::net::IpAddr` - /// - /// ### Why is this bad? - /// It's more concise and clear to just use the proper - /// utility function - /// - /// ### Known problems - /// This will change the drop order for the matched type. Both `if let` and - /// `while let` will drop the value at the end of the block, both `if` and `while` will drop the - /// value before entering the block. For most types this change will not matter, but for a few - /// types this will not be an acceptable change (e.g. locks). See the - /// [reference](https://doc.rust-lang.org/reference/destructors.html#drop-scopes) for more about - /// drop order. - /// - /// ### Example - /// ```rust - /// # use std::task::Poll; - /// # use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - /// if let Ok(_) = Ok::(42) {} - /// if let Err(_) = Err::(42) {} - /// if let None = None::<()> {} - /// if let Some(_) = Some(42) {} - /// if let Poll::Pending = Poll::Pending::<()> {} - /// if let Poll::Ready(_) = Poll::Ready(42) {} - /// if let IpAddr::V4(_) = IpAddr::V4(Ipv4Addr::LOCALHOST) {} - /// if let IpAddr::V6(_) = IpAddr::V6(Ipv6Addr::LOCALHOST) {} - /// match Ok::(42) { - /// Ok(_) => true, - /// Err(_) => false, - /// }; - /// ``` - /// - /// The more idiomatic use would be: - /// - /// ```rust - /// # use std::task::Poll; - /// # use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - /// if Ok::(42).is_ok() {} - /// if Err::(42).is_err() {} - /// if None::<()>.is_none() {} - /// if Some(42).is_some() {} - /// if Poll::Pending::<()>.is_pending() {} - /// if Poll::Ready(42).is_ready() {} - /// if IpAddr::V4(Ipv4Addr::LOCALHOST).is_ipv4() {} - /// if IpAddr::V6(Ipv6Addr::LOCALHOST).is_ipv6() {} - /// Ok::(42).is_ok(); - /// ``` - #[clippy::version = "1.31.0"] - pub REDUNDANT_PATTERN_MATCHING, - style, - "use the proper utility function avoiding an `if let`" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for `match` or `if let` expressions producing a - /// `bool` that could be written using `matches!` - /// - /// ### Why is this bad? - /// Readability and needless complexity. - /// - /// ### Known problems - /// This lint falsely triggers, if there are arms with - /// `cfg` attributes that remove an arm evaluating to `false`. - /// - /// ### Example - /// ```rust - /// let x = Some(5); - /// - /// // Bad - /// let a = match x { - /// Some(0) => true, - /// _ => false, - /// }; - /// - /// let a = if let Some(0) = x { - /// true - /// } else { - /// false - /// }; - /// - /// // Good - /// let a = matches!(x, Some(0)); - /// ``` - #[clippy::version = "1.47.0"] - pub MATCH_LIKE_MATCHES_MACRO, - style, - "a match that could be written with the matches! macro" -} - -declare_clippy_lint! { - /// ### What it does - /// Checks for `match` with identical arm bodies. - /// - /// ### Why is this bad? - /// This is probably a copy & paste error. If arm bodies - /// are the same on purpose, you can factor them - /// [using `|`](https://doc.rust-lang.org/book/patterns.html#multiple-patterns). - /// - /// ### Known problems - /// False positive possible with order dependent `match` - /// (see issue - /// [#860](https://github.com/rust-lang/rust-clippy/issues/860)). - /// - /// ### Example - /// ```rust,ignore - /// match foo { - /// Bar => bar(), - /// Quz => quz(), - /// Baz => bar(), // <= oops - /// } - /// ``` - /// - /// This should probably be - /// ```rust,ignore - /// match foo { - /// Bar => bar(), - /// Quz => quz(), - /// Baz => baz(), // <= fixed - /// } - /// ``` - /// - /// or if the original code was not a typo: - /// ```rust,ignore - /// match foo { - /// Bar | Baz => bar(), // <= shows the intent better - /// Quz => quz(), - /// } - /// ``` - #[clippy::version = "pre 1.29.0"] - pub MATCH_SAME_ARMS, - pedantic, - "`match` with identical arm bodies" -} - -#[derive(Default)] -pub struct Matches { - msrv: Option, - infallible_destructuring_match_linted: bool, -} - -impl Matches { - #[must_use] - pub fn new(msrv: Option) -> Self { - Self { - msrv, - ..Matches::default() - } - } -} - -impl_lint_pass!(Matches => [ - SINGLE_MATCH, - MATCH_REF_PATS, - MATCH_BOOL, - SINGLE_MATCH_ELSE, - MATCH_OVERLAPPING_ARM, - MATCH_WILD_ERR_ARM, - MATCH_AS_REF, - WILDCARD_ENUM_MATCH_ARM, - MATCH_WILDCARD_FOR_SINGLE_VARIANTS, - WILDCARD_IN_OR_PATTERNS, - MATCH_SINGLE_BINDING, - INFALLIBLE_DESTRUCTURING_MATCH, - REST_PAT_IN_FULLY_BOUND_STRUCTS, - REDUNDANT_PATTERN_MATCHING, - MATCH_LIKE_MATCHES_MACRO, - MATCH_SAME_ARMS, -]); - -impl<'tcx> LateLintPass<'tcx> for Matches { - fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { - if expr.span.from_expansion() { - return; - } - - redundant_pattern_match::check(cx, expr); - - if meets_msrv(self.msrv.as_ref(), &msrvs::MATCHES_MACRO) { - if !check_match_like_matches(cx, expr) { - lint_match_arms(cx, expr); - } - } else { - lint_match_arms(cx, expr); - } - - if let ExprKind::Match(ex, arms, MatchSource::Normal) = expr.kind { - check_single_match(cx, ex, arms, expr); - check_match_bool(cx, ex, arms, expr); - check_overlapping_arms(cx, ex, arms); - check_wild_err_arm(cx, ex, arms); - check_wild_enum_match(cx, ex, arms); - check_match_as_ref(cx, ex, arms, expr); - check_wild_in_or_pats(cx, arms); - - if self.infallible_destructuring_match_linted { - self.infallible_destructuring_match_linted = false; - } else { - check_match_single_binding(cx, ex, arms, expr); - } - } - if let ExprKind::Match(ex, arms, _) = expr.kind { - check_match_ref_pats(cx, ex, arms.iter().map(|el| el.pat), expr); - } - } - - fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'_>) { - if_chain! { - if !local.span.from_expansion(); - if let Some(expr) = local.init; - if let ExprKind::Match(target, arms, MatchSource::Normal) = expr.kind; - if arms.len() == 1 && arms[0].guard.is_none(); - if let PatKind::TupleStruct( - QPath::Resolved(None, variant_name), args, _) = arms[0].pat.kind; - if args.len() == 1; - if let PatKind::Binding(_, arg, ..) = strip_pat_refs(&args[0]).kind; - let body = peel_blocks(arms[0].body); - if path_to_local_id(body, arg); - - then { - let mut applicability = Applicability::MachineApplicable; - self.infallible_destructuring_match_linted = true; - span_lint_and_sugg( - cx, - INFALLIBLE_DESTRUCTURING_MATCH, - local.span, - "you seem to be trying to use `match` to destructure a single infallible pattern. \ - Consider using `let`", - "try this", - format!( - "let {}({}) = {};", - snippet_with_applicability(cx, variant_name.span, "..", &mut applicability), - snippet_with_applicability(cx, local.pat.span, "..", &mut applicability), - snippet_with_applicability(cx, target.span, "..", &mut applicability), - ), - applicability, - ); - } - } - } - - fn check_pat(&mut self, cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>) { - if_chain! { - if !pat.span.from_expansion(); - if let PatKind::Struct(QPath::Resolved(_, path), fields, true) = pat.kind; - if let Some(def_id) = path.res.opt_def_id(); - let ty = cx.tcx.type_of(def_id); - if let ty::Adt(def, _) = ty.kind(); - if def.is_struct() || def.is_union(); - if fields.len() == def.non_enum_variant().fields.len(); - - then { - span_lint_and_help( - cx, - REST_PAT_IN_FULLY_BOUND_STRUCTS, - pat.span, - "unnecessary use of `..` pattern in struct binding. All fields were already bound", - None, - "consider removing `..` from this binding", - ); - } - } - } - - extract_msrv_attr!(LateContext); -} - -#[rustfmt::skip] -fn check_single_match(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { - if arms.len() == 2 && arms[0].guard.is_none() && arms[1].guard.is_none() { - if expr.span.from_expansion() { - // Don't lint match expressions present in - // macro_rules! block - return; - } - if let PatKind::Or(..) = arms[0].pat.kind { - // don't lint for or patterns for now, this makes - // the lint noisy in unnecessary situations - return; - } - let els = arms[1].body; - let els = if is_unit_expr(peel_blocks(els)) { - None - } else if let ExprKind::Block(Block { stmts, expr: block_expr, .. }, _) = els.kind { - if stmts.len() == 1 && block_expr.is_none() || stmts.is_empty() && block_expr.is_some() { - // single statement/expr "else" block, don't lint - return; - } - // block with 2+ statements or 1 expr and 1+ statement - Some(els) - } else { - // not a block, don't lint - return; - }; - - let ty = cx.typeck_results().expr_ty(ex); - if *ty.kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id) { - check_single_match_single_pattern(cx, ex, arms, expr, els); - check_single_match_opt_like(cx, ex, arms, expr, ty, els); - } - } -} - -fn check_single_match_single_pattern( - cx: &LateContext<'_>, - ex: &Expr<'_>, - arms: &[Arm<'_>], - expr: &Expr<'_>, - els: Option<&Expr<'_>>, -) { - if is_wild(arms[1].pat) { - report_single_match_single_pattern(cx, ex, arms, expr, els); - } -} - -fn report_single_match_single_pattern( - cx: &LateContext<'_>, - ex: &Expr<'_>, - arms: &[Arm<'_>], - expr: &Expr<'_>, - els: Option<&Expr<'_>>, -) { - let lint = if els.is_some() { SINGLE_MATCH_ELSE } else { SINGLE_MATCH }; - let els_str = els.map_or(String::new(), |els| { - format!(" else {}", expr_block(cx, els, None, "..", Some(expr.span))) - }); - - let (pat, pat_ref_count) = peel_hir_pat_refs(arms[0].pat); - let (msg, sugg) = if_chain! { - if let PatKind::Path(_) | PatKind::Lit(_) = pat.kind; - let (ty, ty_ref_count) = peel_mid_ty_refs(cx.typeck_results().expr_ty(ex)); - if let Some(spe_trait_id) = cx.tcx.lang_items().structural_peq_trait(); - if let Some(pe_trait_id) = cx.tcx.lang_items().eq_trait(); - if ty.is_integral() || ty.is_char() || ty.is_str() - || (implements_trait(cx, ty, spe_trait_id, &[]) - && implements_trait(cx, ty, pe_trait_id, &[ty.into()])); - then { - // scrutinee derives PartialEq and the pattern is a constant. - let pat_ref_count = match pat.kind { - // string literals are already a reference. - PatKind::Lit(Expr { kind: ExprKind::Lit(lit), .. }) if lit.node.is_str() => pat_ref_count + 1, - _ => pat_ref_count, - }; - // References are only implicitly added to the pattern, so no overflow here. - // e.g. will work: match &Some(_) { Some(_) => () } - // will not: match Some(_) { &Some(_) => () } - let ref_count_diff = ty_ref_count - pat_ref_count; - - // Try to remove address of expressions first. - let (ex, removed) = peel_n_hir_expr_refs(ex, ref_count_diff); - let ref_count_diff = ref_count_diff - removed; - - let msg = "you seem to be trying to use `match` for an equality check. Consider using `if`"; - let sugg = format!( - "if {} == {}{} {}{}", - snippet(cx, ex.span, ".."), - // PartialEq for different reference counts may not exist. - "&".repeat(ref_count_diff), - snippet(cx, arms[0].pat.span, ".."), - expr_block(cx, arms[0].body, None, "..", Some(expr.span)), - els_str, - ); - (msg, sugg) - } else { - let msg = "you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`"; - let sugg = format!( - "if let {} = {} {}{}", - snippet(cx, arms[0].pat.span, ".."), - snippet(cx, ex.span, ".."), - expr_block(cx, arms[0].body, None, "..", Some(expr.span)), - els_str, - ); - (msg, sugg) - } - }; - - span_lint_and_sugg( - cx, - lint, - expr.span, - msg, - "try this", - sugg, - Applicability::HasPlaceholders, - ); -} - -fn check_single_match_opt_like<'a>( - cx: &LateContext<'a>, - ex: &Expr<'_>, - arms: &[Arm<'_>], - expr: &Expr<'_>, - ty: Ty<'a>, - els: Option<&Expr<'_>>, -) { - // list of candidate `Enum`s we know will never get any more members - let candidates = &[ - (&paths::COW, "Borrowed"), - (&paths::COW, "Cow::Borrowed"), - (&paths::COW, "Cow::Owned"), - (&paths::COW, "Owned"), - (&paths::OPTION, "None"), - (&paths::RESULT, "Err"), - (&paths::RESULT, "Ok"), - ]; - - // 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; - } - } - 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, - } -} - -fn check_match_bool(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { - // Type of expression is `bool`. - if *cx.typeck_results().expr_ty(ex).kind() == ty::Bool { - span_lint_and_then( - cx, - MATCH_BOOL, - expr.span, - "you seem to be trying to match on a boolean expression", - move |diag| { - if arms.len() == 2 { - // no guards - let exprs = if let PatKind::Lit(arm_bool) = arms[0].pat.kind { - if let ExprKind::Lit(ref lit) = arm_bool.kind { - match lit.node { - LitKind::Bool(true) => Some((&*arms[0].body, &*arms[1].body)), - LitKind::Bool(false) => Some((&*arms[1].body, &*arms[0].body)), - _ => None, - } - } else { - None - } - } else { - None - }; - - if let Some((true_expr, false_expr)) = exprs { - let sugg = match (is_unit_expr(true_expr), is_unit_expr(false_expr)) { - (false, false) => Some(format!( - "if {} {} else {}", - snippet(cx, ex.span, "b"), - expr_block(cx, true_expr, None, "..", Some(expr.span)), - expr_block(cx, false_expr, None, "..", Some(expr.span)) - )), - (false, true) => Some(format!( - "if {} {}", - snippet(cx, ex.span, "b"), - expr_block(cx, true_expr, None, "..", Some(expr.span)) - )), - (true, false) => { - let test = Sugg::hir(cx, ex, ".."); - Some(format!( - "if {} {}", - !test, - expr_block(cx, false_expr, None, "..", Some(expr.span)) - )) - }, - (true, true) => None, - }; - - if let Some(sugg) = sugg { - diag.span_suggestion( - expr.span, - "consider using an `if`/`else` expression", - sugg, - Applicability::HasPlaceholders, - ); - } - } - } - }, - ); - } -} - -fn check_overlapping_arms<'tcx>(cx: &LateContext<'tcx>, ex: &'tcx Expr<'_>, arms: &'tcx [Arm<'_>]) { - if arms.len() >= 2 && cx.typeck_results().expr_ty(ex).is_integral() { - let ranges = all_ranges(cx, arms, cx.typeck_results().expr_ty(ex)); - if !ranges.is_empty() { - if let Some((start, end)) = overlapping(&ranges) { - span_lint_and_note( - cx, - MATCH_OVERLAPPING_ARM, - start.span, - "some ranges overlap", - Some(end.span), - "overlaps with this", - ); - } - } - } -} - -fn check_wild_err_arm<'tcx>(cx: &LateContext<'tcx>, ex: &Expr<'tcx>, arms: &[Arm<'tcx>]) { - let ex_ty = cx.typeck_results().expr_ty(ex).peel_refs(); - if is_type_diagnostic_item(cx, ex_ty, sym::Result) { - for arm in arms { - if let PatKind::TupleStruct(ref path, inner, _) = arm.pat.kind { - let path_str = rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false)); - if path_str == "Err" { - let mut matching_wild = inner.iter().any(is_wild); - let mut ident_bind_name = kw::Underscore; - if !matching_wild { - // Looking for unused bindings (i.e.: `_e`) - for pat in inner.iter() { - if let PatKind::Binding(_, id, ident, None) = pat.kind { - if ident.as_str().starts_with('_') && !is_local_used(cx, arm.body, id) { - ident_bind_name = ident.name; - matching_wild = true; - } - } - } - } - if_chain! { - if matching_wild; - if let Some(macro_call) = root_macro_call(peel_blocks_with_stmt(arm.body).span); - if is_panic(cx, macro_call.def_id); - then { - // `Err(_)` or `Err(_e)` arm with `panic!` found - span_lint_and_note(cx, - MATCH_WILD_ERR_ARM, - arm.pat.span, - &format!("`Err({})` matches all errors", ident_bind_name), - None, - "match each error separately or use the error output, or use `.except(msg)` if the error case is unreachable", - ); - } - } - } - } - } - } -} - -enum CommonPrefixSearcher<'a> { - None, - Path(&'a [PathSegment<'a>]), - Mixed, -} -impl<'a> CommonPrefixSearcher<'a> { - fn with_path(&mut self, path: &'a [PathSegment<'a>]) { - match path { - [path @ .., _] => self.with_prefix(path), - [] => (), - } - } - - fn with_prefix(&mut self, path: &'a [PathSegment<'a>]) { - match self { - Self::None => *self = Self::Path(path), - Self::Path(self_path) - if path - .iter() - .map(|p| p.ident.name) - .eq(self_path.iter().map(|p| p.ident.name)) => {}, - Self::Path(_) => *self = Self::Mixed, - Self::Mixed => (), - } - } -} - -fn is_hidden(cx: &LateContext<'_>, variant_def: &VariantDef) -> bool { - let attrs = cx.tcx.get_attrs(variant_def.def_id); - clippy_utils::attrs::is_doc_hidden(attrs) || clippy_utils::attrs::is_unstable(attrs) -} - -#[allow(clippy::too_many_lines)] -fn check_wild_enum_match(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) { - let ty = cx.typeck_results().expr_ty(ex).peel_refs(); - let adt_def = match ty.kind() { - ty::Adt(adt_def, _) - if adt_def.is_enum() - && !(is_type_diagnostic_item(cx, ty, sym::Option) || is_type_diagnostic_item(cx, ty, sym::Result)) => - { - adt_def - }, - _ => return, - }; - - // First pass - check for violation, but don't do much book-keeping because this is hopefully - // the uncommon case, and the book-keeping is slightly expensive. - let mut wildcard_span = None; - let mut wildcard_ident = None; - let mut has_non_wild = false; - for arm in arms { - match peel_hir_pat_refs(arm.pat).0.kind { - PatKind::Wild => wildcard_span = Some(arm.pat.span), - PatKind::Binding(_, _, ident, None) => { - wildcard_span = Some(arm.pat.span); - wildcard_ident = Some(ident); - }, - _ => has_non_wild = true, - } - } - let wildcard_span = match wildcard_span { - Some(x) if has_non_wild => x, - _ => return, - }; - - // Accumulate the variants which should be put in place of the wildcard because they're not - // already covered. - let has_hidden = adt_def.variants.iter().any(|x| is_hidden(cx, x)); - let mut missing_variants: Vec<_> = adt_def.variants.iter().filter(|x| !is_hidden(cx, x)).collect(); - - let mut path_prefix = CommonPrefixSearcher::None; - for arm in arms { - // Guards mean that this case probably isn't exhaustively covered. Technically - // this is incorrect, as we should really check whether each variant is exhaustively - // covered by the set of guards that cover it, but that's really hard to do. - recurse_or_patterns(arm.pat, |pat| { - let path = match &peel_hir_pat_refs(pat).0.kind { - PatKind::Path(path) => { - #[allow(clippy::match_same_arms)] - let id = match cx.qpath_res(path, pat.hir_id) { - Res::Def( - DefKind::Const | DefKind::ConstParam | DefKind::AnonConst | DefKind::InlineConst, - _, - ) => return, - Res::Def(_, id) => id, - _ => return, - }; - if arm.guard.is_none() { - missing_variants.retain(|e| e.ctor_def_id != Some(id)); - } - path - }, - PatKind::TupleStruct(path, patterns, ..) => { - if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() { - if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p)) { - missing_variants.retain(|e| e.ctor_def_id != Some(id)); - } - } - path - }, - PatKind::Struct(path, patterns, ..) => { - if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() { - if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p.pat)) { - missing_variants.retain(|e| e.def_id != id); - } - } - path - }, - _ => return, - }; - match path { - QPath::Resolved(_, path) => path_prefix.with_path(path.segments), - QPath::TypeRelative( - hir::Ty { - kind: TyKind::Path(QPath::Resolved(_, path)), - .. - }, - _, - ) => path_prefix.with_prefix(path.segments), - _ => (), - } - }); - } - - let format_suggestion = |variant: &VariantDef| { - format!( - "{}{}{}{}", - if let Some(ident) = wildcard_ident { - format!("{} @ ", ident.name) - } else { - String::new() - }, - if let CommonPrefixSearcher::Path(path_prefix) = path_prefix { - let mut s = String::new(); - for seg in path_prefix { - s.push_str(seg.ident.as_str()); - s.push_str("::"); - } - s - } else { - let mut s = cx.tcx.def_path_str(adt_def.did); - s.push_str("::"); - s - }, - variant.name, - match variant.ctor_kind { - CtorKind::Fn if variant.fields.len() == 1 => "(_)", - CtorKind::Fn => "(..)", - CtorKind::Const => "", - CtorKind::Fictive => "{ .. }", - } - ) - }; - - match missing_variants.as_slice() { - [] => (), - [x] if !adt_def.is_variant_list_non_exhaustive() && !has_hidden => span_lint_and_sugg( - cx, - MATCH_WILDCARD_FOR_SINGLE_VARIANTS, - wildcard_span, - "wildcard matches only a single variant and will also match any future added variants", - "try this", - format_suggestion(x), - Applicability::MaybeIncorrect, - ), - variants => { - let mut suggestions: Vec<_> = variants.iter().copied().map(format_suggestion).collect(); - let message = if adt_def.is_variant_list_non_exhaustive() || has_hidden { - suggestions.push("_".into()); - "wildcard matches known variants and will also match future added variants" - } else { - "wildcard match will also match any future added variants" - }; - - span_lint_and_sugg( - cx, - WILDCARD_ENUM_MATCH_ARM, - wildcard_span, - message, - "try this", - suggestions.join(" | "), - Applicability::MaybeIncorrect, - ); - }, - }; -} - -fn check_match_ref_pats<'a, 'b, I>(cx: &LateContext<'_>, ex: &Expr<'_>, pats: I, expr: &Expr<'_>) -where - 'b: 'a, - I: Clone + Iterator>, -{ - if !has_multiple_ref_pats(pats.clone()) { - return; - } - - let (first_sugg, msg, title); - let span = ex.span.source_callsite(); - if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, inner) = ex.kind { - first_sugg = once((span, Sugg::hir_with_macro_callsite(cx, inner, "..").to_string())); - msg = "try"; - title = "you don't need to add `&` to both the expression and the patterns"; - } else { - first_sugg = once((span, Sugg::hir_with_macro_callsite(cx, ex, "..").deref().to_string())); - msg = "instead of prefixing all patterns with `&`, you can dereference the expression"; - title = "you don't need to add `&` to all patterns"; - } - - let remaining_suggs = pats.filter_map(|pat| { - if let PatKind::Ref(refp, _) = pat.kind { - Some((pat.span, snippet(cx, refp.span, "..").to_string())) - } else { - None - } - }); - - span_lint_and_then(cx, MATCH_REF_PATS, expr.span, title, |diag| { - if !expr.span.from_expansion() { - multispan_sugg(diag, msg, first_sugg.chain(remaining_suggs)); - } - }); -} - -fn check_match_as_ref(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { - if arms.len() == 2 && arms[0].guard.is_none() && arms[1].guard.is_none() { - let arm_ref: Option = if is_none_arm(cx, &arms[0]) { - is_ref_some_arm(cx, &arms[1]) - } else if is_none_arm(cx, &arms[1]) { - is_ref_some_arm(cx, &arms[0]) - } else { - None - }; - if let Some(rb) = arm_ref { - let suggestion = if rb == BindingAnnotation::Ref { - "as_ref" - } else { - "as_mut" - }; - - let output_ty = cx.typeck_results().expr_ty(expr); - let input_ty = cx.typeck_results().expr_ty(ex); - - let cast = if_chain! { - if let ty::Adt(_, substs) = input_ty.kind(); - let input_ty = substs.type_at(0); - if let ty::Adt(_, substs) = output_ty.kind(); - let output_ty = substs.type_at(0); - if let ty::Ref(_, output_ty, _) = *output_ty.kind(); - if input_ty != output_ty; - then { - ".map(|x| x as _)" - } else { - "" - } - }; - - let mut applicability = Applicability::MachineApplicable; - span_lint_and_sugg( - cx, - MATCH_AS_REF, - expr.span, - &format!("use `{}()` instead", suggestion), - "try this", - format!( - "{}.{}(){}", - snippet_with_applicability(cx, ex.span, "_", &mut applicability), - suggestion, - cast, - ), - applicability, - ); - } - } -} - -fn check_wild_in_or_pats(cx: &LateContext<'_>, arms: &[Arm<'_>]) { - for arm in arms { - if let PatKind::Or(fields) = arm.pat.kind { - // look for multiple fields in this arm that contains at least one Wild pattern - if fields.len() > 1 && fields.iter().any(is_wild) { - span_lint_and_help( - cx, - WILDCARD_IN_OR_PATTERNS, - arm.pat.span, - "wildcard pattern covers any other pattern as it will match anyway", - None, - "consider handling `_` separately", - ); - } - } - } -} - -/// Lint a `match` or `if let .. { .. } else { .. }` expr that could be replaced by `matches!` -fn check_match_like_matches<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool { - if let Some(higher::IfLet { - let_pat, - let_expr, - if_then, - if_else: Some(if_else), - }) = higher::IfLet::hir(cx, expr) - { - return find_matches_sugg( - cx, - let_expr, - IntoIterator::into_iter([(&[][..], Some(let_pat), if_then, None), (&[][..], None, if_else, None)]), - expr, - true, - ); - } - - if let ExprKind::Match(scrut, arms, MatchSource::Normal) = expr.kind { - return find_matches_sugg( - cx, - scrut, - arms.iter().map(|arm| { - ( - cx.tcx.hir().attrs(arm.hir_id), - Some(arm.pat), - arm.body, - arm.guard.as_ref(), - ) - }), - expr, - false, - ); - } - - false -} - -/// Lint a `match` or `if let` for replacement by `matches!` -fn find_matches_sugg<'a, 'b, I>( - cx: &LateContext<'_>, - ex: &Expr<'_>, - mut iter: I, - expr: &Expr<'_>, - is_if_let: bool, -) -> bool -where - 'b: 'a, - I: Clone - + DoubleEndedIterator - + ExactSizeIterator - + Iterator< - Item = ( - &'a [Attribute], - Option<&'a Pat<'b>>, - &'a Expr<'b>, - Option<&'a Guard<'b>>, - ), - >, -{ - if_chain! { - if iter.len() >= 2; - if cx.typeck_results().expr_ty(expr).is_bool(); - if let Some((_, last_pat_opt, last_expr, _)) = iter.next_back(); - let iter_without_last = iter.clone(); - if let Some((first_attrs, _, first_expr, first_guard)) = iter.next(); - if let Some(b0) = find_bool_lit(&first_expr.kind, is_if_let); - if let Some(b1) = find_bool_lit(&last_expr.kind, is_if_let); - if b0 != b1; - if first_guard.is_none() || iter.len() == 0; - if first_attrs.is_empty(); - if iter - .all(|arm| { - find_bool_lit(&arm.2.kind, is_if_let).map_or(false, |b| b == b0) && arm.3.is_none() && arm.0.is_empty() - }); - then { - if let Some(last_pat) = last_pat_opt { - if !is_wild(last_pat) { - return false; - } - } - - // The suggestion may be incorrect, because some arms can have `cfg` attributes - // evaluated into `false` and so such arms will be stripped before. - let mut applicability = Applicability::MaybeIncorrect; - let pat = { - use itertools::Itertools as _; - iter_without_last - .filter_map(|arm| { - let pat_span = arm.1?.span; - Some(snippet_with_applicability(cx, pat_span, "..", &mut applicability)) - }) - .join(" | ") - }; - let pat_and_guard = if let Some(Guard::If(g)) = first_guard { - format!("{} if {}", pat, snippet_with_applicability(cx, g.span, "..", &mut applicability)) - } else { - pat - }; - - // strip potential borrows (#6503), but only if the type is a reference - let mut ex_new = ex; - if let ExprKind::AddrOf(BorrowKind::Ref, .., ex_inner) = ex.kind { - if let ty::Ref(..) = cx.typeck_results().expr_ty(ex_inner).kind() { - ex_new = ex_inner; - } - }; - span_lint_and_sugg( - cx, - MATCH_LIKE_MATCHES_MACRO, - expr.span, - &format!("{} expression looks like `matches!` macro", if is_if_let { "if let .. else" } else { "match" }), - "try this", - format!( - "{}matches!({}, {})", - if b0 { "" } else { "!" }, - snippet_with_applicability(cx, ex_new.span, "..", &mut applicability), - pat_and_guard, - ), - applicability, - ); - true - } else { - false - } - } -} - -/// Extract a `bool` or `{ bool }` -fn find_bool_lit(ex: &ExprKind<'_>, is_if_let: bool) -> Option { - match ex { - ExprKind::Lit(Spanned { - node: LitKind::Bool(b), .. - }) => Some(*b), - ExprKind::Block( - rustc_hir::Block { - stmts: &[], - expr: Some(exp), - .. - }, - _, - ) if is_if_let => { - if let ExprKind::Lit(Spanned { - node: LitKind::Bool(b), .. - }) = exp.kind - { - Some(b) - } else { - None - } - }, - _ => None, - } -} - -#[allow(clippy::too_many_lines)] -fn check_match_single_binding<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], expr: &Expr<'_>) { - if expr.span.from_expansion() || arms.len() != 1 || is_refutable(cx, arms[0].pat) { - return; - } - - // HACK: - // This is a hack to deal with arms that are excluded by macros like `#[cfg]`. It is only used here - // to prevent false positives as there is currently no better way to detect if code was excluded by - // a macro. See PR #6435 - if_chain! { - if let Some(match_snippet) = snippet_opt(cx, expr.span); - if let Some(arm_snippet) = snippet_opt(cx, arms[0].span); - if let Some(ex_snippet) = snippet_opt(cx, ex.span); - let rest_snippet = match_snippet.replace(&arm_snippet, "").replace(&ex_snippet, ""); - if rest_snippet.contains("=>"); - then { - // The code it self contains another thick arrow "=>" - // -> Either another arm or a comment - return; - } - } - - let matched_vars = ex.span; - let bind_names = arms[0].pat.span; - let match_body = peel_blocks(arms[0].body); - let mut snippet_body = if match_body.span.from_expansion() { - Sugg::hir_with_macro_callsite(cx, match_body, "..").to_string() - } else { - snippet_block(cx, match_body.span, "..", Some(expr.span)).to_string() - }; - - // Do we need to add ';' to suggestion ? - match match_body.kind { - ExprKind::Block(block, _) => { - // macro + expr_ty(body) == () - if block.span.from_expansion() && cx.typeck_results().expr_ty(match_body).is_unit() { - snippet_body.push(';'); - } - }, - _ => { - // expr_ty(body) == () - if cx.typeck_results().expr_ty(match_body).is_unit() { - snippet_body.push(';'); - } - }, - } - - let mut applicability = Applicability::MaybeIncorrect; - match arms[0].pat.kind { - PatKind::Binding(..) | PatKind::Tuple(_, _) | PatKind::Struct(..) => { - // If this match is in a local (`let`) stmt - let (target_span, sugg) = if let Some(parent_let_node) = opt_parent_let(cx, ex) { - ( - parent_let_node.span, - format!( - "let {} = {};\n{}let {} = {};", - snippet_with_applicability(cx, bind_names, "..", &mut applicability), - snippet_with_applicability(cx, matched_vars, "..", &mut applicability), - " ".repeat(indent_of(cx, expr.span).unwrap_or(0)), - snippet_with_applicability(cx, parent_let_node.pat.span, "..", &mut applicability), - snippet_body - ), - ) - } else { - // If we are in closure, we need curly braces around suggestion - let mut indent = " ".repeat(indent_of(cx, ex.span).unwrap_or(0)); - let (mut cbrace_start, mut cbrace_end) = ("".to_string(), "".to_string()); - if let Some(parent_expr) = get_parent_expr(cx, expr) { - if let ExprKind::Closure(..) = parent_expr.kind { - cbrace_end = format!("\n{}}}", indent); - // Fix body indent due to the closure - indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); - cbrace_start = format!("{{\n{}", indent); - } - } - // If the parent is already an arm, and the body is another match statement, - // we need curly braces around suggestion - let parent_node_id = cx.tcx.hir().get_parent_node(expr.hir_id); - if let Node::Arm(arm) = &cx.tcx.hir().get(parent_node_id) { - if let ExprKind::Match(..) = arm.body.kind { - cbrace_end = format!("\n{}}}", indent); - // Fix body indent due to the match - indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); - cbrace_start = format!("{{\n{}", indent); - } - } - ( - expr.span, - format!( - "{}let {} = {};\n{}{}{}", - cbrace_start, - snippet_with_applicability(cx, bind_names, "..", &mut applicability), - snippet_with_applicability(cx, matched_vars, "..", &mut applicability), - indent, - snippet_body, - cbrace_end - ), - ) - }; - span_lint_and_sugg( - cx, - MATCH_SINGLE_BINDING, - target_span, - "this match could be written as a `let` statement", - "consider using `let` statement", - sugg, - applicability, - ); - }, - PatKind::Wild => { - if ex.can_have_side_effects() { - let indent = " ".repeat(indent_of(cx, expr.span).unwrap_or(0)); - let sugg = format!( - "{};\n{}{}", - snippet_with_applicability(cx, ex.span, "..", &mut applicability), - indent, - snippet_body - ); - span_lint_and_sugg( - cx, - MATCH_SINGLE_BINDING, - expr.span, - "this match could be replaced by its scrutinee and body", - "consider using the scrutinee and body instead", - sugg, - applicability, - ); - } else { - span_lint_and_sugg( - cx, - MATCH_SINGLE_BINDING, - expr.span, - "this match could be replaced by its body itself", - "consider using the match body instead", - snippet_body, - Applicability::MachineApplicable, - ); - } - }, - _ => (), - } -} - -/// Returns true if the `ex` match expression is in a local (`let`) statement -fn opt_parent_let<'a>(cx: &LateContext<'a>, ex: &Expr<'a>) -> Option<&'a Local<'a>> { - let map = &cx.tcx.hir(); - if_chain! { - if let Some(Node::Expr(parent_arm_expr)) = map.find(map.get_parent_node(ex.hir_id)); - if let Some(Node::Local(parent_let_expr)) = map.find(map.get_parent_node(parent_arm_expr.hir_id)); - then { - return Some(parent_let_expr); - } - } - None -} - -/// Gets the ranges for each range pattern arm. Applies `ty` bounds for open ranges. -fn all_ranges<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>], ty: Ty<'tcx>) -> Vec> { - arms.iter() - .filter_map(|arm| { - if let Arm { pat, guard: None, .. } = *arm { - if let PatKind::Range(ref lhs, ref rhs, range_end) = pat.kind { - let lhs_const = match lhs { - Some(lhs) => constant(cx, cx.typeck_results(), lhs)?.0, - None => miri_to_const(ty.numeric_min_val(cx.tcx)?)?, - }; - let rhs_const = match rhs { - Some(rhs) => constant(cx, cx.typeck_results(), rhs)?.0, - None => miri_to_const(ty.numeric_max_val(cx.tcx)?)?, - }; - - let lhs_val = lhs_const.int_value(cx, ty)?; - let rhs_val = rhs_const.int_value(cx, ty)?; - - let rhs_bound = match range_end { - RangeEnd::Included => EndBound::Included(rhs_val), - RangeEnd::Excluded => EndBound::Excluded(rhs_val), - }; - return Some(SpannedRange { - span: pat.span, - node: (lhs_val, rhs_bound), - }); - } - - if let PatKind::Lit(value) = pat.kind { - let value = constant_full_int(cx, cx.typeck_results(), value)?; - return Some(SpannedRange { - span: pat.span, - node: (value, EndBound::Included(value)), - }); - } - } - None - }) - .collect() -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum EndBound { - Included(T), - Excluded(T), -} - -#[derive(Debug, Eq, PartialEq)] -struct SpannedRange { - pub span: Span, - pub node: (T, EndBound), -} - -// Checks if arm has the form `None => None` -fn is_none_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool { - matches!(arm.pat.kind, PatKind::Path(ref qpath) if is_lang_ctor(cx, qpath, OptionNone)) -} - -// Checks if arm has the form `Some(ref v) => Some(v)` (checks for `ref` and `ref mut`) -fn is_ref_some_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> Option { - if_chain! { - if let PatKind::TupleStruct(ref qpath, [first_pat, ..], _) = arm.pat.kind; - if is_lang_ctor(cx, qpath, OptionSome); - if let PatKind::Binding(rb, .., ident, _) = first_pat.kind; - if rb == BindingAnnotation::Ref || rb == BindingAnnotation::RefMut; - if let ExprKind::Call(e, args) = peel_blocks(arm.body).kind; - if let ExprKind::Path(ref some_path) = e.kind; - if is_lang_ctor(cx, some_path, OptionSome) && args.len() == 1; - if let ExprKind::Path(QPath::Resolved(_, path2)) = args[0].kind; - if path2.segments.len() == 1 && ident.name == path2.segments[0].ident.name; - then { - return Some(rb) - } - } - None -} - -fn has_multiple_ref_pats<'a, 'b, I>(pats: I) -> bool -where - 'b: 'a, - I: Iterator>, -{ - let mut ref_count = 0; - for opt in pats.map(|pat| match pat.kind { - PatKind::Ref(..) => Some(true), // &-patterns - PatKind::Wild => Some(false), // an "anything" wildcard is also fine - _ => None, // any other pattern is not fine - }) { - if let Some(inner) = opt { - if inner { - ref_count += 1; - } - } else { - return false; - } - } - ref_count > 1 -} - -fn overlapping(ranges: &[SpannedRange]) -> Option<(&SpannedRange, &SpannedRange)> -where - T: Copy + Ord, -{ - #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] - enum BoundKind { - EndExcluded, - Start, - EndIncluded, - } - - #[derive(Copy, Clone, Debug, Eq, PartialEq)] - struct RangeBound<'a, T>(T, BoundKind, &'a SpannedRange); - - impl<'a, T: Copy + Ord> PartialOrd for RangeBound<'a, T> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - - impl<'a, T: Copy + Ord> Ord for RangeBound<'a, T> { - fn cmp(&self, RangeBound(other_value, other_kind, _): &Self) -> Ordering { - let RangeBound(self_value, self_kind, _) = *self; - (self_value, self_kind).cmp(&(*other_value, *other_kind)) - } - } - - let mut values = Vec::with_capacity(2 * ranges.len()); - - for r @ SpannedRange { node: (start, end), .. } in ranges { - values.push(RangeBound(*start, BoundKind::Start, r)); - values.push(match end { - EndBound::Excluded(val) => RangeBound(*val, BoundKind::EndExcluded, r), - EndBound::Included(val) => RangeBound(*val, BoundKind::EndIncluded, r), - }); - } - - values.sort(); - - let mut started = vec![]; - - for RangeBound(_, kind, range) in values { - match kind { - BoundKind::Start => started.push(range), - BoundKind::EndExcluded | BoundKind::EndIncluded => { - let mut overlap = None; - - while let Some(last_started) = started.pop() { - if last_started == range { - break; - } - overlap = Some(last_started); - } - - if let Some(first_overlapping) = overlap { - return Some((range, first_overlapping)); - } - }, - } - } - - None -} - -mod redundant_pattern_match { - use super::REDUNDANT_PATTERN_MATCHING; - use clippy_utils::diagnostics::span_lint_and_then; - use clippy_utils::source::snippet; - use clippy_utils::sugg::Sugg; - use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, is_type_lang_item, match_type}; - use clippy_utils::{higher, match_def_path}; - use clippy_utils::{is_lang_ctor, is_trait_method, paths}; - use if_chain::if_chain; - use rustc_ast::ast::LitKind; - use rustc_data_structures::fx::FxHashSet; - use rustc_errors::Applicability; - use rustc_hir::LangItem::{OptionNone, PollPending}; - use rustc_hir::{ - intravisit::{walk_expr, Visitor}, - Arm, Block, Expr, ExprKind, LangItem, MatchSource, Node, Pat, PatKind, QPath, UnOp, - }; - use rustc_lint::LateContext; - use rustc_middle::ty::{self, subst::GenericArgKind, DefIdTree, Ty}; - use rustc_span::sym; - - pub fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { - if let Some(higher::IfLet { - if_else, - let_pat, - let_expr, - .. - }) = higher::IfLet::hir(cx, expr) - { - find_sugg_for_if_let(cx, expr, let_pat, let_expr, "if", if_else.is_some()); - } - if let ExprKind::Match(op, arms, MatchSource::Normal) = &expr.kind { - find_sugg_for_match(cx, expr, op, arms); - } - if let Some(higher::WhileLet { let_pat, let_expr, .. }) = higher::WhileLet::hir(expr) { - find_sugg_for_if_let(cx, expr, let_pat, let_expr, "while", false); - } - } - - /// Checks if the drop order for a type matters. Some std types implement drop solely to - /// deallocate memory. For these types, and composites containing them, changing the drop order - /// won't result in any observable side effects. - fn type_needs_ordered_drop<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { - type_needs_ordered_drop_inner(cx, ty, &mut FxHashSet::default()) - } - - fn type_needs_ordered_drop_inner<'tcx>( - cx: &LateContext<'tcx>, - ty: Ty<'tcx>, - seen: &mut FxHashSet>, - ) -> bool { - if !seen.insert(ty) { - return false; - } - if !ty.needs_drop(cx.tcx, cx.param_env) { - false - } else if !cx - .tcx - .lang_items() - .drop_trait() - .map_or(false, |id| implements_trait(cx, ty, id, &[])) - { - // This type doesn't implement drop, so no side effects here. - // Check if any component type has any. - match ty.kind() { - ty::Tuple(_) => ty.tuple_fields().any(|ty| type_needs_ordered_drop_inner(cx, ty, seen)), - ty::Array(ty, _) => type_needs_ordered_drop_inner(cx, ty, seen), - ty::Adt(adt, subs) => adt - .all_fields() - .map(|f| f.ty(cx.tcx, subs)) - .any(|ty| type_needs_ordered_drop_inner(cx, ty, seen)), - _ => true, - } - } - // Check for std types which implement drop, but only for memory allocation. - else if is_type_diagnostic_item(cx, ty, sym::Vec) - || is_type_lang_item(cx, ty, LangItem::OwnedBox) - || is_type_diagnostic_item(cx, ty, sym::Rc) - || is_type_diagnostic_item(cx, ty, sym::Arc) - || is_type_diagnostic_item(cx, ty, sym::cstring_type) - || is_type_diagnostic_item(cx, ty, sym::BTreeMap) - || is_type_diagnostic_item(cx, ty, sym::LinkedList) - || match_type(cx, ty, &paths::WEAK_RC) - || match_type(cx, ty, &paths::WEAK_ARC) - { - // Check all of the generic arguments. - if let ty::Adt(_, subs) = ty.kind() { - subs.types().any(|ty| type_needs_ordered_drop_inner(cx, ty, seen)) - } else { - true - } - } else { - true - } - } - - // Extract the generic arguments out of a type - fn try_get_generic_ty(ty: Ty<'_>, index: usize) -> Option> { - if_chain! { - if let ty::Adt(_, subs) = ty.kind(); - if let Some(sub) = subs.get(index); - if let GenericArgKind::Type(sub_ty) = sub.unpack(); - then { - Some(sub_ty) - } else { - None - } - } - } - - // Checks if there are any temporaries created in the given expression for which drop order - // matters. - fn temporaries_need_ordered_drop<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool { - struct V<'a, 'tcx> { - cx: &'a LateContext<'tcx>, - res: bool, - } - impl<'a, 'tcx> Visitor<'tcx> for V<'a, 'tcx> { - fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { - match expr.kind { - // Taking the reference of a value leaves a temporary - // e.g. In `&String::new()` the string is a temporary value. - // Remaining fields are temporary values - // e.g. In `(String::new(), 0).1` the string is a temporary value. - ExprKind::AddrOf(_, _, expr) | ExprKind::Field(expr, _) => { - if !matches!(expr.kind, ExprKind::Path(_)) { - if type_needs_ordered_drop(self.cx, self.cx.typeck_results().expr_ty(expr)) { - self.res = true; - } else { - self.visit_expr(expr); - } - } - }, - // the base type is alway taken by reference. - // e.g. In `(vec![0])[0]` the vector is a temporary value. - ExprKind::Index(base, index) => { - if !matches!(base.kind, ExprKind::Path(_)) { - if type_needs_ordered_drop(self.cx, self.cx.typeck_results().expr_ty(base)) { - self.res = true; - } else { - self.visit_expr(base); - } - } - self.visit_expr(index); - }, - // Method calls can take self by reference. - // e.g. In `String::new().len()` the string is a temporary value. - ExprKind::MethodCall(_, [self_arg, args @ ..], _) => { - if !matches!(self_arg.kind, ExprKind::Path(_)) { - let self_by_ref = self - .cx - .typeck_results() - .type_dependent_def_id(expr.hir_id) - .map_or(false, |id| self.cx.tcx.fn_sig(id).skip_binder().inputs()[0].is_ref()); - if self_by_ref - && type_needs_ordered_drop(self.cx, self.cx.typeck_results().expr_ty(self_arg)) - { - self.res = true; - } else { - self.visit_expr(self_arg); - } - } - args.iter().for_each(|arg| self.visit_expr(arg)); - }, - // Either explicitly drops values, or changes control flow. - ExprKind::DropTemps(_) - | ExprKind::Ret(_) - | ExprKind::Break(..) - | ExprKind::Yield(..) - | ExprKind::Block(Block { expr: None, .. }, _) - | ExprKind::Loop(..) => (), - - // Only consider the final expression. - ExprKind::Block(Block { expr: Some(expr), .. }, _) => self.visit_expr(expr), - - _ => walk_expr(self, expr), - } - } - } - - let mut v = V { cx, res: false }; - v.visit_expr(expr); - v.res - } - - fn find_sugg_for_if_let<'tcx>( - cx: &LateContext<'tcx>, - expr: &'tcx Expr<'_>, - let_pat: &Pat<'_>, - let_expr: &'tcx Expr<'_>, - keyword: &'static str, - has_else: bool, - ) { - // also look inside refs - // if we have &None for example, peel it so we can detect "if let None = x" - let check_pat = match let_pat.kind { - PatKind::Ref(inner, _mutability) => inner, - _ => let_pat, - }; - let op_ty = cx.typeck_results().expr_ty(let_expr); - // Determine which function should be used, and the type contained by the corresponding - // variant. - let (good_method, inner_ty) = match check_pat.kind { - PatKind::TupleStruct(ref qpath, [sub_pat], _) => { - if let PatKind::Wild = sub_pat.kind { - let res = cx.typeck_results().qpath_res(qpath, check_pat.hir_id); - let Some(id) = res.opt_def_id().and_then(|ctor_id| cx.tcx.parent(ctor_id)) else { return }; - let lang_items = cx.tcx.lang_items(); - if Some(id) == lang_items.result_ok_variant() { - ("is_ok()", try_get_generic_ty(op_ty, 0).unwrap_or(op_ty)) - } else if Some(id) == lang_items.result_err_variant() { - ("is_err()", try_get_generic_ty(op_ty, 1).unwrap_or(op_ty)) - } else if Some(id) == lang_items.option_some_variant() { - ("is_some()", op_ty) - } else if Some(id) == lang_items.poll_ready_variant() { - ("is_ready()", op_ty) - } else if match_def_path(cx, id, &paths::IPADDR_V4) { - ("is_ipv4()", op_ty) - } else if match_def_path(cx, id, &paths::IPADDR_V6) { - ("is_ipv6()", op_ty) - } else { - return; - } - } else { - return; - } - }, - PatKind::Path(ref path) => { - let method = if is_lang_ctor(cx, path, OptionNone) { - "is_none()" - } else if is_lang_ctor(cx, path, PollPending) { - "is_pending()" - } else { - return; - }; - // `None` and `Pending` don't have an inner type. - (method, cx.tcx.types.unit) - }, - _ => return, - }; - - // If this is the last expression in a block or there is an else clause then the whole - // type needs to be considered, not just the inner type of the branch being matched on. - // Note the last expression in a block is dropped after all local bindings. - let check_ty = if has_else - || (keyword == "if" && matches!(cx.tcx.hir().parent_iter(expr.hir_id).next(), Some((_, Node::Block(..))))) - { - op_ty - } else { - inner_ty - }; - - // All temporaries created in the scrutinee expression are dropped at the same time as the - // scrutinee would be, so they have to be considered as well. - // e.g. in `if let Some(x) = foo.lock().unwrap().baz.as_ref() { .. }` the lock will be held - // for the duration if body. - let needs_drop = type_needs_ordered_drop(cx, check_ty) || temporaries_need_ordered_drop(cx, let_expr); - - // check that `while_let_on_iterator` lint does not trigger - if_chain! { - if keyword == "while"; - if let ExprKind::MethodCall(method_path, _, _) = let_expr.kind; - if method_path.ident.name == sym::next; - if is_trait_method(cx, let_expr, sym::Iterator); - then { - return; - } - } - - let result_expr = match &let_expr.kind { - ExprKind::AddrOf(_, _, borrowed) => borrowed, - ExprKind::Unary(UnOp::Deref, deref) => deref, - _ => let_expr, - }; - - span_lint_and_then( - cx, - REDUNDANT_PATTERN_MATCHING, - let_pat.span, - &format!("redundant pattern matching, consider using `{}`", good_method), - |diag| { - // if/while let ... = ... { ... } - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - let expr_span = expr.span; - - // if/while let ... = ... { ... } - // ^^^ - let op_span = result_expr.span.source_callsite(); - - // if/while let ... = ... { ... } - // ^^^^^^^^^^^^^^^^^^^ - let span = expr_span.until(op_span.shrink_to_hi()); - - let app = if needs_drop { - Applicability::MaybeIncorrect - } else { - Applicability::MachineApplicable - }; - - let sugg = Sugg::hir_with_macro_callsite(cx, result_expr, "_") - .maybe_par() - .to_string(); - - diag.span_suggestion(span, "try this", format!("{} {}.{}", keyword, sugg, good_method), app); - - if needs_drop { - diag.note("this will change drop order of the result, as well as all temporaries"); - diag.note("add `#[allow(clippy::redundant_pattern_matching)]` if this is important"); - } - }, - ); - } - - fn find_sugg_for_match<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, op: &Expr<'_>, arms: &[Arm<'_>]) { - if arms.len() == 2 { - let node_pair = (&arms[0].pat.kind, &arms[1].pat.kind); - - let found_good_method = match node_pair { - ( - PatKind::TupleStruct(ref path_left, patterns_left, _), - PatKind::TupleStruct(ref path_right, patterns_right, _), - ) if patterns_left.len() == 1 && patterns_right.len() == 1 => { - if let (PatKind::Wild, PatKind::Wild) = (&patterns_left[0].kind, &patterns_right[0].kind) { - find_good_method_for_match( - cx, - arms, - path_left, - path_right, - &paths::RESULT_OK, - &paths::RESULT_ERR, - "is_ok()", - "is_err()", - ) - .or_else(|| { - find_good_method_for_match( - cx, - arms, - path_left, - path_right, - &paths::IPADDR_V4, - &paths::IPADDR_V6, - "is_ipv4()", - "is_ipv6()", - ) - }) - } else { - None - } - }, - (PatKind::TupleStruct(ref path_left, patterns, _), PatKind::Path(ref path_right)) - | (PatKind::Path(ref path_left), PatKind::TupleStruct(ref path_right, patterns, _)) - if patterns.len() == 1 => - { - if let PatKind::Wild = patterns[0].kind { - find_good_method_for_match( - cx, - arms, - path_left, - path_right, - &paths::OPTION_SOME, - &paths::OPTION_NONE, - "is_some()", - "is_none()", - ) - .or_else(|| { - find_good_method_for_match( - cx, - arms, - path_left, - path_right, - &paths::POLL_READY, - &paths::POLL_PENDING, - "is_ready()", - "is_pending()", - ) - }) - } else { - None - } - }, - _ => None, - }; - - if let Some(good_method) = found_good_method { - let span = expr.span.to(op.span); - let result_expr = match &op.kind { - ExprKind::AddrOf(_, _, borrowed) => borrowed, - _ => op, - }; - span_lint_and_then( - cx, - REDUNDANT_PATTERN_MATCHING, - expr.span, - &format!("redundant pattern matching, consider using `{}`", good_method), - |diag| { - diag.span_suggestion( - span, - "try this", - format!("{}.{}", snippet(cx, result_expr.span, "_"), good_method), - Applicability::MaybeIncorrect, // snippet - ); - }, - ); - } - } - } - - #[allow(clippy::too_many_arguments)] - fn find_good_method_for_match<'a>( - cx: &LateContext<'_>, - arms: &[Arm<'_>], - path_left: &QPath<'_>, - path_right: &QPath<'_>, - expected_left: &[&str], - expected_right: &[&str], - should_be_left: &'a str, - should_be_right: &'a str, - ) -> Option<&'a str> { - let left_id = cx - .typeck_results() - .qpath_res(path_left, arms[0].pat.hir_id) - .opt_def_id()?; - let right_id = cx - .typeck_results() - .qpath_res(path_right, arms[1].pat.hir_id) - .opt_def_id()?; - let body_node_pair = - if match_def_path(cx, left_id, expected_left) && match_def_path(cx, right_id, expected_right) { - (&(*arms[0].body).kind, &(*arms[1].body).kind) - } else if match_def_path(cx, right_id, expected_left) && match_def_path(cx, right_id, expected_right) { - (&(*arms[1].body).kind, &(*arms[0].body).kind) - } else { - return None; - }; - - match body_node_pair { - (ExprKind::Lit(ref lit_left), ExprKind::Lit(ref lit_right)) => match (&lit_left.node, &lit_right.node) { - (LitKind::Bool(true), LitKind::Bool(false)) => Some(should_be_left), - (LitKind::Bool(false), LitKind::Bool(true)) => Some(should_be_right), - _ => None, - }, - _ => None, - } - } -} - -#[test] -fn test_overlapping() { - use rustc_span::source_map::DUMMY_SP; - - let sp = |s, e| SpannedRange { - span: DUMMY_SP, - node: (s, e), - }; - - assert_eq!(None, overlapping::(&[])); - assert_eq!(None, overlapping(&[sp(1, EndBound::Included(4))])); - assert_eq!( - None, - overlapping(&[sp(1, EndBound::Included(4)), sp(5, EndBound::Included(6))]) - ); - assert_eq!( - None, - overlapping(&[ - sp(1, EndBound::Included(4)), - sp(5, EndBound::Included(6)), - sp(10, EndBound::Included(11)) - ],) - ); - assert_eq!( - Some((&sp(1, EndBound::Included(4)), &sp(3, EndBound::Included(6)))), - overlapping(&[sp(1, EndBound::Included(4)), sp(3, EndBound::Included(6))]) - ); - assert_eq!( - Some((&sp(5, EndBound::Included(6)), &sp(6, EndBound::Included(11)))), - overlapping(&[ - sp(1, EndBound::Included(4)), - sp(5, EndBound::Included(6)), - sp(6, EndBound::Included(11)) - ],) - ); -} - -/// Implementation of `MATCH_SAME_ARMS`. -fn lint_match_arms<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) { - if let ExprKind::Match(_, arms, MatchSource::Normal) = expr.kind { - let hash = |&(_, arm): &(usize, &Arm<'_>)| -> u64 { - let mut h = SpanlessHash::new(cx); - h.hash_expr(arm.body); - h.finish() - }; - - let eq = |&(lindex, lhs): &(usize, &Arm<'_>), &(rindex, rhs): &(usize, &Arm<'_>)| -> bool { - let min_index = usize::min(lindex, rindex); - let max_index = usize::max(lindex, rindex); - - let mut local_map: HirIdMap = HirIdMap::default(); - let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| { - if_chain! { - if let Some(a_id) = path_to_local(a); - if let Some(b_id) = path_to_local(b); - let entry = match local_map.entry(a_id) { - Entry::Vacant(entry) => entry, - // check if using the same bindings as before - Entry::Occupied(entry) => return *entry.get() == b_id, - }; - // the names technically don't have to match; this makes the lint more conservative - if cx.tcx.hir().name(a_id) == cx.tcx.hir().name(b_id); - if TyS::same_type(cx.typeck_results().expr_ty(a), cx.typeck_results().expr_ty(b)); - if pat_contains_local(lhs.pat, a_id); - if pat_contains_local(rhs.pat, b_id); - then { - entry.insert(b_id); - true - } else { - false - } - } - }; - // Arms with a guard are ignored, those can’t always be merged together - // This is also the case for arms in-between each there is an arm with a guard - (min_index..=max_index).all(|index| arms[index].guard.is_none()) - && SpanlessEq::new(cx) - .expr_fallback(eq_fallback) - .eq_expr(lhs.body, rhs.body) - // these checks could be removed to allow unused bindings - && bindings_eq(lhs.pat, local_map.keys().copied().collect()) - && bindings_eq(rhs.pat, local_map.values().copied().collect()) - }; - - let indexed_arms: Vec<(usize, &Arm<'_>)> = arms.iter().enumerate().collect(); - for (&(_, i), &(_, j)) in search_same(&indexed_arms, hash, eq) { - span_lint_and_then( - cx, - MATCH_SAME_ARMS, - j.body.span, - "this `match` has identical arm bodies", - |diag| { - diag.span_note(i.body.span, "same as this"); - - // Note: this does not use `span_suggestion` on purpose: - // there is no clean way - // to remove the other arm. Building a span and suggest to replace it to "" - // makes an even more confusing error message. Also in order not to make up a - // span for the whole pattern, the suggestion is only shown when there is only - // one pattern. The user should know about `|` if they are already using it… - - let lhs = snippet(cx, i.pat.span, ""); - let rhs = snippet(cx, j.pat.span, ""); - - if let PatKind::Wild = j.pat.kind { - // if the last arm is _, then i could be integrated into _ - // note that i.pat cannot be _, because that would mean that we're - // hiding all the subsequent arms, and rust won't compile - diag.span_note( - i.body.span, - &format!( - "`{}` has the same arm body as the `_` wildcard, consider removing it", - lhs - ), - ); - } else { - diag.span_help(i.pat.span, &format!("consider refactoring into `{} | {}`", lhs, rhs,)) - .help("...or consider changing the match arm bodies"); - } - }, - ); - } - } -} - -fn pat_contains_local(pat: &Pat<'_>, id: HirId) -> bool { - let mut result = false; - pat.walk_short(|p| { - result |= matches!(p.kind, PatKind::Binding(_, binding_id, ..) if binding_id == id); - !result - }); - result -} - -/// Returns true if all the bindings in the `Pat` are in `ids` and vice versa -fn bindings_eq(pat: &Pat<'_>, mut ids: HirIdSet) -> bool { - let mut result = true; - pat.each_binding_or_first(&mut |_, id, _, _| result &= ids.remove(&id)); - result && ids.is_empty() -} diff --git a/clippy_lints/src/matches/infalliable_detructuring_match.rs b/clippy_lints/src/matches/infalliable_detructuring_match.rs new file mode 100644 index 000000000000..2472acb6f6e8 --- /dev/null +++ b/clippy_lints/src/matches/infalliable_detructuring_match.rs @@ -0,0 +1,44 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{path_to_local_id, peel_blocks, strip_pat_refs}; +use rustc_errors::Applicability; +use rustc_hir::{ExprKind, Local, MatchSource, PatKind, QPath}; +use rustc_lint::LateContext; + +use super::INFALLIBLE_DESTRUCTURING_MATCH; + +pub(crate) fn check(cx: &LateContext<'_>, local: &Local<'_>) -> bool { + if_chain! { + if !local.span.from_expansion(); + if let Some(expr) = local.init; + if let ExprKind::Match(target, arms, MatchSource::Normal) = expr.kind; + if arms.len() == 1 && arms[0].guard.is_none(); + if let PatKind::TupleStruct( + QPath::Resolved(None, variant_name), args, _) = arms[0].pat.kind; + if args.len() == 1; + if let PatKind::Binding(_, arg, ..) = strip_pat_refs(&args[0]).kind; + let body = peel_blocks(arms[0].body); + if path_to_local_id(body, arg); + + then { + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + INFALLIBLE_DESTRUCTURING_MATCH, + local.span, + "you seem to be trying to use `match` to destructure a single infallible pattern. \ + Consider using `let`", + "try this", + format!( + "let {}({}) = {};", + snippet_with_applicability(cx, variant_name.span, "..", &mut applicability), + snippet_with_applicability(cx, local.pat.span, "..", &mut applicability), + snippet_with_applicability(cx, target.span, "..", &mut applicability), + ), + applicability, + ); + return true; + } + } + false +} diff --git a/clippy_lints/src/matches/match_as_ref.rs b/clippy_lints/src/matches/match_as_ref.rs new file mode 100644 index 000000000000..d914eba01716 --- /dev/null +++ b/clippy_lints/src/matches/match_as_ref.rs @@ -0,0 +1,85 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{is_lang_ctor, peel_blocks}; +use rustc_errors::Applicability; +use rustc_hir::{Arm, BindingAnnotation, Expr, ExprKind, LangItem, PatKind, QPath}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::MATCH_AS_REF; + +pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { + if arms.len() == 2 && arms[0].guard.is_none() && arms[1].guard.is_none() { + let arm_ref: Option = if is_none_arm(cx, &arms[0]) { + is_ref_some_arm(cx, &arms[1]) + } else if is_none_arm(cx, &arms[1]) { + is_ref_some_arm(cx, &arms[0]) + } else { + None + }; + if let Some(rb) = arm_ref { + let suggestion = if rb == BindingAnnotation::Ref { + "as_ref" + } else { + "as_mut" + }; + + let output_ty = cx.typeck_results().expr_ty(expr); + let input_ty = cx.typeck_results().expr_ty(ex); + + let cast = if_chain! { + if let ty::Adt(_, substs) = input_ty.kind(); + let input_ty = substs.type_at(0); + if let ty::Adt(_, substs) = output_ty.kind(); + let output_ty = substs.type_at(0); + if let ty::Ref(_, output_ty, _) = *output_ty.kind(); + if input_ty != output_ty; + then { + ".map(|x| x as _)" + } else { + "" + } + }; + + let mut applicability = Applicability::MachineApplicable; + span_lint_and_sugg( + cx, + MATCH_AS_REF, + expr.span, + &format!("use `{}()` instead", suggestion), + "try this", + format!( + "{}.{}(){}", + snippet_with_applicability(cx, ex.span, "_", &mut applicability), + suggestion, + cast, + ), + applicability, + ); + } + } +} + +// Checks if arm has the form `None => None` +fn is_none_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool { + matches!(arm.pat.kind, PatKind::Path(ref qpath) if is_lang_ctor(cx, qpath, LangItem::OptionNone)) +} + +// Checks if arm has the form `Some(ref v) => Some(v)` (checks for `ref` and `ref mut`) +fn is_ref_some_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> Option { + if_chain! { + if let PatKind::TupleStruct(ref qpath, [first_pat, ..], _) = arm.pat.kind; + if is_lang_ctor(cx, qpath, LangItem::OptionSome); + if let PatKind::Binding(rb, .., ident, _) = first_pat.kind; + if rb == BindingAnnotation::Ref || rb == BindingAnnotation::RefMut; + if let ExprKind::Call(e, args) = peel_blocks(arm.body).kind; + if let ExprKind::Path(ref some_path) = e.kind; + if is_lang_ctor(cx, some_path, LangItem::OptionSome) && args.len() == 1; + if let ExprKind::Path(QPath::Resolved(_, path2)) = args[0].kind; + if path2.segments.len() == 1 && ident.name == path2.segments[0].ident.name; + then { + return Some(rb) + } + } + None +} diff --git a/clippy_lints/src/matches/match_bool.rs b/clippy_lints/src/matches/match_bool.rs new file mode 100644 index 000000000000..90c50b994d2b --- /dev/null +++ b/clippy_lints/src/matches/match_bool.rs @@ -0,0 +1,75 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::is_unit_expr; +use clippy_utils::source::{expr_block, snippet}; +use clippy_utils::sugg::Sugg; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Arm, Expr, ExprKind, PatKind}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::MATCH_BOOL; + +pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { + // Type of expression is `bool`. + if *cx.typeck_results().expr_ty(ex).kind() == ty::Bool { + span_lint_and_then( + cx, + MATCH_BOOL, + expr.span, + "you seem to be trying to match on a boolean expression", + move |diag| { + if arms.len() == 2 { + // no guards + let exprs = if let PatKind::Lit(arm_bool) = arms[0].pat.kind { + if let ExprKind::Lit(ref lit) = arm_bool.kind { + match lit.node { + LitKind::Bool(true) => Some((&*arms[0].body, &*arms[1].body)), + LitKind::Bool(false) => Some((&*arms[1].body, &*arms[0].body)), + _ => None, + } + } else { + None + } + } else { + None + }; + + if let Some((true_expr, false_expr)) = exprs { + let sugg = match (is_unit_expr(true_expr), is_unit_expr(false_expr)) { + (false, false) => Some(format!( + "if {} {} else {}", + snippet(cx, ex.span, "b"), + expr_block(cx, true_expr, None, "..", Some(expr.span)), + expr_block(cx, false_expr, None, "..", Some(expr.span)) + )), + (false, true) => Some(format!( + "if {} {}", + snippet(cx, ex.span, "b"), + expr_block(cx, true_expr, None, "..", Some(expr.span)) + )), + (true, false) => { + let test = Sugg::hir(cx, ex, ".."); + Some(format!( + "if {} {}", + !test, + expr_block(cx, false_expr, None, "..", Some(expr.span)) + )) + }, + (true, true) => None, + }; + + if let Some(sugg) = sugg { + diag.span_suggestion( + expr.span, + "consider using an `if`/`else` expression", + sugg, + Applicability::HasPlaceholders, + ); + } + } + } + }, + ); + } +} diff --git a/clippy_lints/src/matches/match_like_matches.rs b/clippy_lints/src/matches/match_like_matches.rs new file mode 100644 index 000000000000..d605b6d73c09 --- /dev/null +++ b/clippy_lints/src/matches/match_like_matches.rs @@ -0,0 +1,166 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::{higher, is_wild}; +use rustc_ast::{Attribute, LitKind}; +use rustc_errors::Applicability; +use rustc_hir::{BorrowKind, Expr, ExprKind, Guard, MatchSource, Pat}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::source_map::Spanned; + +use super::MATCH_LIKE_MATCHES_MACRO; + +/// Lint a `match` or `if let .. { .. } else { .. }` expr that could be replaced by `matches!` +pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool { + if let Some(higher::IfLet { + let_pat, + let_expr, + if_then, + if_else: Some(if_else), + }) = higher::IfLet::hir(cx, expr) + { + return find_matches_sugg( + cx, + let_expr, + IntoIterator::into_iter([(&[][..], Some(let_pat), if_then, None), (&[][..], None, if_else, None)]), + expr, + true, + ); + } + + if let ExprKind::Match(scrut, arms, MatchSource::Normal) = expr.kind { + return find_matches_sugg( + cx, + scrut, + arms.iter().map(|arm| { + ( + cx.tcx.hir().attrs(arm.hir_id), + Some(arm.pat), + arm.body, + arm.guard.as_ref(), + ) + }), + expr, + false, + ); + } + + false +} + +/// Lint a `match` or `if let` for replacement by `matches!` +fn find_matches_sugg<'a, 'b, I>( + cx: &LateContext<'_>, + ex: &Expr<'_>, + mut iter: I, + expr: &Expr<'_>, + is_if_let: bool, +) -> bool +where + 'b: 'a, + I: Clone + + DoubleEndedIterator + + ExactSizeIterator + + Iterator< + Item = ( + &'a [Attribute], + Option<&'a Pat<'b>>, + &'a Expr<'b>, + Option<&'a Guard<'b>>, + ), + >, +{ + if_chain! { + if iter.len() >= 2; + if cx.typeck_results().expr_ty(expr).is_bool(); + if let Some((_, last_pat_opt, last_expr, _)) = iter.next_back(); + let iter_without_last = iter.clone(); + if let Some((first_attrs, _, first_expr, first_guard)) = iter.next(); + if let Some(b0) = find_bool_lit(&first_expr.kind, is_if_let); + if let Some(b1) = find_bool_lit(&last_expr.kind, is_if_let); + if b0 != b1; + if first_guard.is_none() || iter.len() == 0; + if first_attrs.is_empty(); + if iter + .all(|arm| { + find_bool_lit(&arm.2.kind, is_if_let).map_or(false, |b| b == b0) && arm.3.is_none() && arm.0.is_empty() + }); + then { + if let Some(last_pat) = last_pat_opt { + if !is_wild(last_pat) { + return false; + } + } + + // The suggestion may be incorrect, because some arms can have `cfg` attributes + // evaluated into `false` and so such arms will be stripped before. + let mut applicability = Applicability::MaybeIncorrect; + let pat = { + use itertools::Itertools as _; + iter_without_last + .filter_map(|arm| { + let pat_span = arm.1?.span; + Some(snippet_with_applicability(cx, pat_span, "..", &mut applicability)) + }) + .join(" | ") + }; + let pat_and_guard = if let Some(Guard::If(g)) = first_guard { + format!("{} if {}", pat, snippet_with_applicability(cx, g.span, "..", &mut applicability)) + } else { + pat + }; + + // strip potential borrows (#6503), but only if the type is a reference + let mut ex_new = ex; + if let ExprKind::AddrOf(BorrowKind::Ref, .., ex_inner) = ex.kind { + if let ty::Ref(..) = cx.typeck_results().expr_ty(ex_inner).kind() { + ex_new = ex_inner; + } + }; + span_lint_and_sugg( + cx, + MATCH_LIKE_MATCHES_MACRO, + expr.span, + &format!("{} expression looks like `matches!` macro", if is_if_let { "if let .. else" } else { "match" }), + "try this", + format!( + "{}matches!({}, {})", + if b0 { "" } else { "!" }, + snippet_with_applicability(cx, ex_new.span, "..", &mut applicability), + pat_and_guard, + ), + applicability, + ); + true + } else { + false + } + } +} + +/// Extract a `bool` or `{ bool }` +fn find_bool_lit(ex: &ExprKind<'_>, is_if_let: bool) -> Option { + match ex { + ExprKind::Lit(Spanned { + node: LitKind::Bool(b), .. + }) => Some(*b), + ExprKind::Block( + rustc_hir::Block { + stmts: &[], + expr: Some(exp), + .. + }, + _, + ) if is_if_let => { + if let ExprKind::Lit(Spanned { + node: LitKind::Bool(b), .. + }) = exp.kind + { + Some(b) + } else { + None + } + }, + _ => None, + } +} diff --git a/clippy_lints/src/matches/match_ref_pats.rs b/clippy_lints/src/matches/match_ref_pats.rs new file mode 100644 index 000000000000..80f964ba1b72 --- /dev/null +++ b/clippy_lints/src/matches/match_ref_pats.rs @@ -0,0 +1,66 @@ +use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; +use clippy_utils::source::snippet; +use clippy_utils::sugg::Sugg; +use core::iter::once; +use rustc_hir::{BorrowKind, Expr, ExprKind, Mutability, Pat, PatKind}; +use rustc_lint::LateContext; + +use super::MATCH_REF_PATS; + +pub(crate) fn check<'a, 'b, I>(cx: &LateContext<'_>, ex: &Expr<'_>, pats: I, expr: &Expr<'_>) +where + 'b: 'a, + I: Clone + Iterator>, +{ + if !has_multiple_ref_pats(pats.clone()) { + return; + } + + let (first_sugg, msg, title); + let span = ex.span.source_callsite(); + if let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, inner) = ex.kind { + first_sugg = once((span, Sugg::hir_with_macro_callsite(cx, inner, "..").to_string())); + msg = "try"; + title = "you don't need to add `&` to both the expression and the patterns"; + } else { + first_sugg = once((span, Sugg::hir_with_macro_callsite(cx, ex, "..").deref().to_string())); + msg = "instead of prefixing all patterns with `&`, you can dereference the expression"; + title = "you don't need to add `&` to all patterns"; + } + + let remaining_suggs = pats.filter_map(|pat| { + if let PatKind::Ref(refp, _) = pat.kind { + Some((pat.span, snippet(cx, refp.span, "..").to_string())) + } else { + None + } + }); + + span_lint_and_then(cx, MATCH_REF_PATS, expr.span, title, |diag| { + if !expr.span.from_expansion() { + multispan_sugg(diag, msg, first_sugg.chain(remaining_suggs)); + } + }); +} + +fn has_multiple_ref_pats<'a, 'b, I>(pats: I) -> bool +where + 'b: 'a, + I: Iterator>, +{ + let mut ref_count = 0; + for opt in pats.map(|pat| match pat.kind { + PatKind::Ref(..) => Some(true), // &-patterns + PatKind::Wild => Some(false), // an "anything" wildcard is also fine + _ => None, // any other pattern is not fine + }) { + if let Some(inner) = opt { + if inner { + ref_count += 1; + } + } else { + return false; + } + } + ref_count > 1 +} diff --git a/clippy_lints/src/matches/match_same_arms.rs b/clippy_lints/src/matches/match_same_arms.rs new file mode 100644 index 000000000000..7efbd540fdb4 --- /dev/null +++ b/clippy_lints/src/matches/match_same_arms.rs @@ -0,0 +1,112 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::{path_to_local, search_same, SpanlessEq, SpanlessHash}; +use rustc_hir::{Arm, Expr, ExprKind, HirId, HirIdMap, HirIdSet, MatchSource, Pat, PatKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::TyS; +use std::collections::hash_map::Entry; + +use super::MATCH_SAME_ARMS; + +pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) { + if let ExprKind::Match(_, arms, MatchSource::Normal) = expr.kind { + let hash = |&(_, arm): &(usize, &Arm<'_>)| -> u64 { + let mut h = SpanlessHash::new(cx); + h.hash_expr(arm.body); + h.finish() + }; + + let eq = |&(lindex, lhs): &(usize, &Arm<'_>), &(rindex, rhs): &(usize, &Arm<'_>)| -> bool { + let min_index = usize::min(lindex, rindex); + let max_index = usize::max(lindex, rindex); + + let mut local_map: HirIdMap = HirIdMap::default(); + let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| { + if_chain! { + if let Some(a_id) = path_to_local(a); + if let Some(b_id) = path_to_local(b); + let entry = match local_map.entry(a_id) { + Entry::Vacant(entry) => entry, + // check if using the same bindings as before + Entry::Occupied(entry) => return *entry.get() == b_id, + }; + // the names technically don't have to match; this makes the lint more conservative + if cx.tcx.hir().name(a_id) == cx.tcx.hir().name(b_id); + if TyS::same_type(cx.typeck_results().expr_ty(a), cx.typeck_results().expr_ty(b)); + if pat_contains_local(lhs.pat, a_id); + if pat_contains_local(rhs.pat, b_id); + then { + entry.insert(b_id); + true + } else { + false + } + } + }; + // Arms with a guard are ignored, those can’t always be merged together + // This is also the case for arms in-between each there is an arm with a guard + (min_index..=max_index).all(|index| arms[index].guard.is_none()) + && SpanlessEq::new(cx) + .expr_fallback(eq_fallback) + .eq_expr(lhs.body, rhs.body) + // these checks could be removed to allow unused bindings + && bindings_eq(lhs.pat, local_map.keys().copied().collect()) + && bindings_eq(rhs.pat, local_map.values().copied().collect()) + }; + + let indexed_arms: Vec<(usize, &Arm<'_>)> = arms.iter().enumerate().collect(); + for (&(_, i), &(_, j)) in search_same(&indexed_arms, hash, eq) { + span_lint_and_then( + cx, + MATCH_SAME_ARMS, + j.body.span, + "this `match` has identical arm bodies", + |diag| { + diag.span_note(i.body.span, "same as this"); + + // Note: this does not use `span_suggestion` on purpose: + // there is no clean way + // to remove the other arm. Building a span and suggest to replace it to "" + // makes an even more confusing error message. Also in order not to make up a + // span for the whole pattern, the suggestion is only shown when there is only + // one pattern. The user should know about `|` if they are already using it… + + let lhs = snippet(cx, i.pat.span, ""); + let rhs = snippet(cx, j.pat.span, ""); + + if let PatKind::Wild = j.pat.kind { + // if the last arm is _, then i could be integrated into _ + // note that i.pat cannot be _, because that would mean that we're + // hiding all the subsequent arms, and rust won't compile + diag.span_note( + i.body.span, + &format!( + "`{}` has the same arm body as the `_` wildcard, consider removing it", + lhs + ), + ); + } else { + diag.span_help(i.pat.span, &format!("consider refactoring into `{} | {}`", lhs, rhs,)) + .help("...or consider changing the match arm bodies"); + } + }, + ); + } + } +} + +fn pat_contains_local(pat: &Pat<'_>, id: HirId) -> bool { + let mut result = false; + pat.walk_short(|p| { + result |= matches!(p.kind, PatKind::Binding(_, binding_id, ..) if binding_id == id); + !result + }); + result +} + +/// Returns true if all the bindings in the `Pat` are in `ids` and vice versa +fn bindings_eq(pat: &Pat<'_>, mut ids: HirIdSet) -> bool { + let mut result = true; + pat.each_binding_or_first(&mut |_, id, _, _| result &= ids.remove(&id)); + result && ids.is_empty() +} diff --git a/clippy_lints/src/matches/match_single_binding.rs b/clippy_lints/src/matches/match_single_binding.rs new file mode 100644 index 000000000000..8ae19e03f1a6 --- /dev/null +++ b/clippy_lints/src/matches/match_single_binding.rs @@ -0,0 +1,166 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{indent_of, snippet_block, snippet_opt, snippet_with_applicability}; +use clippy_utils::sugg::Sugg; +use clippy_utils::{get_parent_expr, is_refutable, peel_blocks}; +use rustc_errors::Applicability; +use rustc_hir::{Arm, Expr, ExprKind, Local, Node, PatKind}; +use rustc_lint::LateContext; + +use super::MATCH_SINGLE_BINDING; + +#[allow(clippy::too_many_lines)] +pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], expr: &Expr<'_>) { + if expr.span.from_expansion() || arms.len() != 1 || is_refutable(cx, arms[0].pat) { + return; + } + + // HACK: + // This is a hack to deal with arms that are excluded by macros like `#[cfg]`. It is only used here + // to prevent false positives as there is currently no better way to detect if code was excluded by + // a macro. See PR #6435 + if_chain! { + if let Some(match_snippet) = snippet_opt(cx, expr.span); + if let Some(arm_snippet) = snippet_opt(cx, arms[0].span); + if let Some(ex_snippet) = snippet_opt(cx, ex.span); + let rest_snippet = match_snippet.replace(&arm_snippet, "").replace(&ex_snippet, ""); + if rest_snippet.contains("=>"); + then { + // The code it self contains another thick arrow "=>" + // -> Either another arm or a comment + return; + } + } + + let matched_vars = ex.span; + let bind_names = arms[0].pat.span; + let match_body = peel_blocks(arms[0].body); + let mut snippet_body = if match_body.span.from_expansion() { + Sugg::hir_with_macro_callsite(cx, match_body, "..").to_string() + } else { + snippet_block(cx, match_body.span, "..", Some(expr.span)).to_string() + }; + + // Do we need to add ';' to suggestion ? + match match_body.kind { + ExprKind::Block(block, _) => { + // macro + expr_ty(body) == () + if block.span.from_expansion() && cx.typeck_results().expr_ty(match_body).is_unit() { + snippet_body.push(';'); + } + }, + _ => { + // expr_ty(body) == () + if cx.typeck_results().expr_ty(match_body).is_unit() { + snippet_body.push(';'); + } + }, + } + + let mut applicability = Applicability::MaybeIncorrect; + match arms[0].pat.kind { + PatKind::Binding(..) | PatKind::Tuple(_, _) | PatKind::Struct(..) => { + // If this match is in a local (`let`) stmt + let (target_span, sugg) = if let Some(parent_let_node) = opt_parent_let(cx, ex) { + ( + parent_let_node.span, + format!( + "let {} = {};\n{}let {} = {};", + snippet_with_applicability(cx, bind_names, "..", &mut applicability), + snippet_with_applicability(cx, matched_vars, "..", &mut applicability), + " ".repeat(indent_of(cx, expr.span).unwrap_or(0)), + snippet_with_applicability(cx, parent_let_node.pat.span, "..", &mut applicability), + snippet_body + ), + ) + } else { + // If we are in closure, we need curly braces around suggestion + let mut indent = " ".repeat(indent_of(cx, ex.span).unwrap_or(0)); + let (mut cbrace_start, mut cbrace_end) = ("".to_string(), "".to_string()); + if let Some(parent_expr) = get_parent_expr(cx, expr) { + if let ExprKind::Closure(..) = parent_expr.kind { + cbrace_end = format!("\n{}}}", indent); + // Fix body indent due to the closure + indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); + cbrace_start = format!("{{\n{}", indent); + } + } + // If the parent is already an arm, and the body is another match statement, + // we need curly braces around suggestion + let parent_node_id = cx.tcx.hir().get_parent_node(expr.hir_id); + if let Node::Arm(arm) = &cx.tcx.hir().get(parent_node_id) { + if let ExprKind::Match(..) = arm.body.kind { + cbrace_end = format!("\n{}}}", indent); + // Fix body indent due to the match + indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); + cbrace_start = format!("{{\n{}", indent); + } + } + ( + expr.span, + format!( + "{}let {} = {};\n{}{}{}", + cbrace_start, + snippet_with_applicability(cx, bind_names, "..", &mut applicability), + snippet_with_applicability(cx, matched_vars, "..", &mut applicability), + indent, + snippet_body, + cbrace_end + ), + ) + }; + span_lint_and_sugg( + cx, + MATCH_SINGLE_BINDING, + target_span, + "this match could be written as a `let` statement", + "consider using `let` statement", + sugg, + applicability, + ); + }, + PatKind::Wild => { + if ex.can_have_side_effects() { + let indent = " ".repeat(indent_of(cx, expr.span).unwrap_or(0)); + let sugg = format!( + "{};\n{}{}", + snippet_with_applicability(cx, ex.span, "..", &mut applicability), + indent, + snippet_body + ); + span_lint_and_sugg( + cx, + MATCH_SINGLE_BINDING, + expr.span, + "this match could be replaced by its scrutinee and body", + "consider using the scrutinee and body instead", + sugg, + applicability, + ); + } else { + span_lint_and_sugg( + cx, + MATCH_SINGLE_BINDING, + expr.span, + "this match could be replaced by its body itself", + "consider using the match body instead", + snippet_body, + Applicability::MachineApplicable, + ); + } + }, + _ => (), + } +} + +/// Returns true if the `ex` match expression is in a local (`let`) statement +fn opt_parent_let<'a>(cx: &LateContext<'a>, ex: &Expr<'a>) -> Option<&'a Local<'a>> { + let map = &cx.tcx.hir(); + if_chain! { + if let Some(Node::Expr(parent_arm_expr)) = map.find(map.get_parent_node(ex.hir_id)); + if let Some(Node::Local(parent_let_expr)) = map.find(map.get_parent_node(parent_arm_expr.hir_id)); + then { + return Some(parent_let_expr); + } + } + None +} diff --git a/clippy_lints/src/matches/match_wild_enum.rs b/clippy_lints/src/matches/match_wild_enum.rs new file mode 100644 index 000000000000..3515286d5b4a --- /dev/null +++ b/clippy_lints/src/matches/match_wild_enum.rs @@ -0,0 +1,198 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::{is_refutable, peel_hir_pat_refs, recurse_or_patterns}; +use rustc_errors::Applicability; +use rustc_hir::def::{CtorKind, DefKind, Res}; +use rustc_hir::{Arm, Expr, PatKind, PathSegment, QPath, Ty, TyKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, VariantDef}; +use rustc_span::sym; + +use super::{MATCH_WILDCARD_FOR_SINGLE_VARIANTS, WILDCARD_ENUM_MATCH_ARM}; + +#[allow(clippy::too_many_lines)] +pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) { + let ty = cx.typeck_results().expr_ty(ex).peel_refs(); + let adt_def = match ty.kind() { + ty::Adt(adt_def, _) + if adt_def.is_enum() + && !(is_type_diagnostic_item(cx, ty, sym::Option) || is_type_diagnostic_item(cx, ty, sym::Result)) => + { + adt_def + }, + _ => return, + }; + + // First pass - check for violation, but don't do much book-keeping because this is hopefully + // the uncommon case, and the book-keeping is slightly expensive. + let mut wildcard_span = None; + let mut wildcard_ident = None; + let mut has_non_wild = false; + for arm in arms { + match peel_hir_pat_refs(arm.pat).0.kind { + PatKind::Wild => wildcard_span = Some(arm.pat.span), + PatKind::Binding(_, _, ident, None) => { + wildcard_span = Some(arm.pat.span); + wildcard_ident = Some(ident); + }, + _ => has_non_wild = true, + } + } + let wildcard_span = match wildcard_span { + Some(x) if has_non_wild => x, + _ => return, + }; + + // Accumulate the variants which should be put in place of the wildcard because they're not + // already covered. + let has_hidden = adt_def.variants.iter().any(|x| is_hidden(cx, x)); + let mut missing_variants: Vec<_> = adt_def.variants.iter().filter(|x| !is_hidden(cx, x)).collect(); + + let mut path_prefix = CommonPrefixSearcher::None; + for arm in arms { + // Guards mean that this case probably isn't exhaustively covered. Technically + // this is incorrect, as we should really check whether each variant is exhaustively + // covered by the set of guards that cover it, but that's really hard to do. + recurse_or_patterns(arm.pat, |pat| { + let path = match &peel_hir_pat_refs(pat).0.kind { + PatKind::Path(path) => { + #[allow(clippy::match_same_arms)] + let id = match cx.qpath_res(path, pat.hir_id) { + Res::Def( + DefKind::Const | DefKind::ConstParam | DefKind::AnonConst | DefKind::InlineConst, + _, + ) => return, + Res::Def(_, id) => id, + _ => return, + }; + if arm.guard.is_none() { + missing_variants.retain(|e| e.ctor_def_id != Some(id)); + } + path + }, + PatKind::TupleStruct(path, patterns, ..) => { + if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() { + if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p)) { + missing_variants.retain(|e| e.ctor_def_id != Some(id)); + } + } + path + }, + PatKind::Struct(path, patterns, ..) => { + if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() { + if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p.pat)) { + missing_variants.retain(|e| e.def_id != id); + } + } + path + }, + _ => return, + }; + match path { + QPath::Resolved(_, path) => path_prefix.with_path(path.segments), + QPath::TypeRelative( + Ty { + kind: TyKind::Path(QPath::Resolved(_, path)), + .. + }, + _, + ) => path_prefix.with_prefix(path.segments), + _ => (), + } + }); + } + + let format_suggestion = |variant: &VariantDef| { + format!( + "{}{}{}{}", + if let Some(ident) = wildcard_ident { + format!("{} @ ", ident.name) + } else { + String::new() + }, + if let CommonPrefixSearcher::Path(path_prefix) = path_prefix { + let mut s = String::new(); + for seg in path_prefix { + s.push_str(seg.ident.as_str()); + s.push_str("::"); + } + s + } else { + let mut s = cx.tcx.def_path_str(adt_def.did); + s.push_str("::"); + s + }, + variant.name, + match variant.ctor_kind { + CtorKind::Fn if variant.fields.len() == 1 => "(_)", + CtorKind::Fn => "(..)", + CtorKind::Const => "", + CtorKind::Fictive => "{ .. }", + } + ) + }; + + match missing_variants.as_slice() { + [] => (), + [x] if !adt_def.is_variant_list_non_exhaustive() && !has_hidden => span_lint_and_sugg( + cx, + MATCH_WILDCARD_FOR_SINGLE_VARIANTS, + wildcard_span, + "wildcard matches only a single variant and will also match any future added variants", + "try this", + format_suggestion(x), + Applicability::MaybeIncorrect, + ), + variants => { + let mut suggestions: Vec<_> = variants.iter().copied().map(format_suggestion).collect(); + let message = if adt_def.is_variant_list_non_exhaustive() || has_hidden { + suggestions.push("_".into()); + "wildcard matches known variants and will also match future added variants" + } else { + "wildcard match will also match any future added variants" + }; + + span_lint_and_sugg( + cx, + WILDCARD_ENUM_MATCH_ARM, + wildcard_span, + message, + "try this", + suggestions.join(" | "), + Applicability::MaybeIncorrect, + ); + }, + }; +} + +enum CommonPrefixSearcher<'a> { + None, + Path(&'a [PathSegment<'a>]), + Mixed, +} +impl<'a> CommonPrefixSearcher<'a> { + fn with_path(&mut self, path: &'a [PathSegment<'a>]) { + match path { + [path @ .., _] => self.with_prefix(path), + [] => (), + } + } + + fn with_prefix(&mut self, path: &'a [PathSegment<'a>]) { + match self { + Self::None => *self = Self::Path(path), + Self::Path(self_path) + if path + .iter() + .map(|p| p.ident.name) + .eq(self_path.iter().map(|p| p.ident.name)) => {}, + Self::Path(_) => *self = Self::Mixed, + Self::Mixed => (), + } + } +} + +fn is_hidden(cx: &LateContext<'_>, variant_def: &VariantDef) -> bool { + let attrs = cx.tcx.get_attrs(variant_def.def_id); + clippy_utils::attrs::is_doc_hidden(attrs) || clippy_utils::attrs::is_unstable(attrs) +} diff --git a/clippy_lints/src/matches/match_wild_err_arm.rs b/clippy_lints/src/matches/match_wild_err_arm.rs new file mode 100644 index 000000000000..bc16f17b6196 --- /dev/null +++ b/clippy_lints/src/matches/match_wild_err_arm.rs @@ -0,0 +1,51 @@ +use clippy_utils::diagnostics::span_lint_and_note; +use clippy_utils::macros::{is_panic, root_macro_call}; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::visitors::is_local_used; +use clippy_utils::{is_wild, peel_blocks_with_stmt}; +use rustc_hir::{Arm, Expr, PatKind}; +use rustc_lint::LateContext; +use rustc_span::symbol::{kw, sym}; + +use super::MATCH_WILD_ERR_ARM; + +pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, ex: &Expr<'tcx>, arms: &[Arm<'tcx>]) { + let ex_ty = cx.typeck_results().expr_ty(ex).peel_refs(); + if is_type_diagnostic_item(cx, ex_ty, sym::Result) { + for arm in arms { + if let PatKind::TupleStruct(ref path, inner, _) = arm.pat.kind { + let path_str = rustc_hir_pretty::to_string(rustc_hir_pretty::NO_ANN, |s| s.print_qpath(path, false)); + if path_str == "Err" { + let mut matching_wild = inner.iter().any(is_wild); + let mut ident_bind_name = kw::Underscore; + if !matching_wild { + // Looking for unused bindings (i.e.: `_e`) + for pat in inner.iter() { + if let PatKind::Binding(_, id, ident, None) = pat.kind { + if ident.as_str().starts_with('_') && !is_local_used(cx, arm.body, id) { + ident_bind_name = ident.name; + matching_wild = true; + } + } + } + } + if_chain! { + if matching_wild; + if let Some(macro_call) = root_macro_call(peel_blocks_with_stmt(arm.body).span); + if is_panic(cx, macro_call.def_id); + then { + // `Err(_)` or `Err(_e)` arm with `panic!` found + span_lint_and_note(cx, + MATCH_WILD_ERR_ARM, + arm.pat.span, + &format!("`Err({})` matches all errors", ident_bind_name), + None, + "match each error separately or use the error output, or use `.except(msg)` if the error case is unreachable", + ); + } + } + } + } + } + } +} diff --git a/clippy_lints/src/matches/mod.rs b/clippy_lints/src/matches/mod.rs new file mode 100644 index 000000000000..b5ee4561f06e --- /dev/null +++ b/clippy_lints/src/matches/mod.rs @@ -0,0 +1,646 @@ +use clippy_utils::{meets_msrv, msrvs}; +use rustc_hir::{Expr, ExprKind, Local, MatchSource, Pat}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; + +mod infalliable_detructuring_match; +mod match_as_ref; +mod match_bool; +mod match_like_matches; +mod match_ref_pats; +mod match_same_arms; +mod match_single_binding; +mod match_wild_enum; +mod match_wild_err_arm; +mod overlapping_arms; +mod redundant_pattern_match; +mod rest_pat_in_fully_bound_struct; +mod single_match; +mod wild_in_or_pats; + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches with a single arm where an `if let` + /// will usually suffice. + /// + /// ### Why is this bad? + /// Just readability – `if let` nests less than a `match`. + /// + /// ### Example + /// ```rust + /// # fn bar(stool: &str) {} + /// # let x = Some("abc"); + /// // Bad + /// match x { + /// Some(ref foo) => bar(foo), + /// _ => (), + /// } + /// + /// // Good + /// if let Some(ref foo) = x { + /// bar(foo); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SINGLE_MATCH, + style, + "a `match` statement with a single nontrivial arm (i.e., where the other arm is `_ => {}`) instead of `if let`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches with two arms where an `if let else` will + /// usually suffice. + /// + /// ### Why is this bad? + /// Just readability – `if let` nests less than a `match`. + /// + /// ### Known problems + /// Personal style preferences may differ. + /// + /// ### Example + /// Using `match`: + /// + /// ```rust + /// # fn bar(foo: &usize) {} + /// # let other_ref: usize = 1; + /// # let x: Option<&usize> = Some(&1); + /// match x { + /// Some(ref foo) => bar(foo), + /// _ => bar(&other_ref), + /// } + /// ``` + /// + /// Using `if let` with `else`: + /// + /// ```rust + /// # fn bar(foo: &usize) {} + /// # let other_ref: usize = 1; + /// # let x: Option<&usize> = Some(&1); + /// if let Some(ref foo) = x { + /// bar(foo); + /// } else { + /// bar(&other_ref); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub SINGLE_MATCH_ELSE, + pedantic, + "a `match` statement with two arms where the second arm's pattern is a placeholder instead of a specific match pattern" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches where all arms match a reference, + /// suggesting to remove the reference and deref the matched expression + /// instead. It also checks for `if let &foo = bar` blocks. + /// + /// ### Why is this bad? + /// It just makes the code less readable. That reference + /// destructuring adds nothing to the code. + /// + /// ### Example + /// ```rust,ignore + /// // Bad + /// match x { + /// &A(ref y) => foo(y), + /// &B => bar(), + /// _ => frob(&x), + /// } + /// + /// // Good + /// match *x { + /// A(ref y) => foo(y), + /// B => bar(), + /// _ => frob(x), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_REF_PATS, + style, + "a `match` or `if let` with all arms prefixed with `&` instead of deref-ing the match expression" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches where match expression is a `bool`. It + /// suggests to replace the expression with an `if...else` block. + /// + /// ### Why is this bad? + /// It makes the code less readable. + /// + /// ### Example + /// ```rust + /// # fn foo() {} + /// # fn bar() {} + /// let condition: bool = true; + /// match condition { + /// true => foo(), + /// false => bar(), + /// } + /// ``` + /// Use if/else instead: + /// ```rust + /// # fn foo() {} + /// # fn bar() {} + /// let condition: bool = true; + /// if condition { + /// foo(); + /// } else { + /// bar(); + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_BOOL, + pedantic, + "a `match` on a boolean expression instead of an `if..else` block" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for overlapping match arms. + /// + /// ### Why is this bad? + /// It is likely to be an error and if not, makes the code + /// less obvious. + /// + /// ### Example + /// ```rust + /// let x = 5; + /// match x { + /// 1..=10 => println!("1 ... 10"), + /// 5..=15 => println!("5 ... 15"), + /// _ => (), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_OVERLAPPING_ARM, + style, + "a `match` with overlapping arms" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for arm which matches all errors with `Err(_)` + /// and take drastic actions like `panic!`. + /// + /// ### Why is this bad? + /// It is generally a bad practice, similar to + /// catching all exceptions in java with `catch(Exception)` + /// + /// ### Example + /// ```rust + /// let x: Result = Ok(3); + /// match x { + /// Ok(_) => println!("ok"), + /// Err(_) => panic!("err"), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_WILD_ERR_ARM, + pedantic, + "a `match` with `Err(_)` arm and take drastic actions" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for match which is used to add a reference to an + /// `Option` value. + /// + /// ### Why is this bad? + /// Using `as_ref()` or `as_mut()` instead is shorter. + /// + /// ### Example + /// ```rust + /// let x: Option<()> = None; + /// + /// // Bad + /// let r: Option<&()> = match x { + /// None => None, + /// Some(ref v) => Some(v), + /// }; + /// + /// // Good + /// let r: Option<&()> = x.as_ref(); + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_AS_REF, + complexity, + "a `match` on an Option value instead of using `as_ref()` or `as_mut`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for wildcard enum matches using `_`. + /// + /// ### Why is this bad? + /// New enum variants added by library updates can be missed. + /// + /// ### Known problems + /// Suggested replacements may be incorrect if guards exhaustively cover some + /// variants, and also may not use correct path to enum if it's not present in the current scope. + /// + /// ### Example + /// ```rust + /// # enum Foo { A(usize), B(usize) } + /// # let x = Foo::B(1); + /// // Bad + /// match x { + /// Foo::A(_) => {}, + /// _ => {}, + /// } + /// + /// // Good + /// match x { + /// Foo::A(_) => {}, + /// Foo::B(_) => {}, + /// } + /// ``` + #[clippy::version = "1.34.0"] + pub WILDCARD_ENUM_MATCH_ARM, + restriction, + "a wildcard enum match arm using `_`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for wildcard enum matches for a single variant. + /// + /// ### Why is this bad? + /// New enum variants added by library updates can be missed. + /// + /// ### Known problems + /// Suggested replacements may not use correct path to enum + /// if it's not present in the current scope. + /// + /// ### Example + /// ```rust + /// # enum Foo { A, B, C } + /// # let x = Foo::B; + /// // Bad + /// match x { + /// Foo::A => {}, + /// Foo::B => {}, + /// _ => {}, + /// } + /// + /// // Good + /// match x { + /// Foo::A => {}, + /// Foo::B => {}, + /// Foo::C => {}, + /// } + /// ``` + #[clippy::version = "1.45.0"] + pub MATCH_WILDCARD_FOR_SINGLE_VARIANTS, + pedantic, + "a wildcard enum match for a single variant" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for wildcard pattern used with others patterns in same match arm. + /// + /// ### Why is this bad? + /// Wildcard pattern already covers any other pattern as it will match anyway. + /// It makes the code less readable, especially to spot wildcard pattern use in match arm. + /// + /// ### Example + /// ```rust + /// // Bad + /// match "foo" { + /// "a" => {}, + /// "bar" | _ => {}, + /// } + /// + /// // Good + /// match "foo" { + /// "a" => {}, + /// _ => {}, + /// } + /// ``` + #[clippy::version = "1.42.0"] + pub WILDCARD_IN_OR_PATTERNS, + complexity, + "a wildcard pattern used with others patterns in same match arm" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for matches being used to destructure a single-variant enum + /// or tuple struct where a `let` will suffice. + /// + /// ### Why is this bad? + /// Just readability – `let` doesn't nest, whereas a `match` does. + /// + /// ### Example + /// ```rust + /// enum Wrapper { + /// Data(i32), + /// } + /// + /// let wrapper = Wrapper::Data(42); + /// + /// let data = match wrapper { + /// Wrapper::Data(i) => i, + /// }; + /// ``` + /// + /// The correct use would be: + /// ```rust + /// enum Wrapper { + /// Data(i32), + /// } + /// + /// let wrapper = Wrapper::Data(42); + /// let Wrapper::Data(data) = wrapper; + /// ``` + #[clippy::version = "pre 1.29.0"] + pub INFALLIBLE_DESTRUCTURING_MATCH, + style, + "a `match` statement with a single infallible arm instead of a `let`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for useless match that binds to only one value. + /// + /// ### Why is this bad? + /// Readability and needless complexity. + /// + /// ### Known problems + /// Suggested replacements may be incorrect when `match` + /// is actually binding temporary value, bringing a 'dropped while borrowed' error. + /// + /// ### Example + /// ```rust + /// # let a = 1; + /// # let b = 2; + /// + /// // Bad + /// match (a, b) { + /// (c, d) => { + /// // useless match + /// } + /// } + /// + /// // Good + /// let (c, d) = (a, b); + /// ``` + #[clippy::version = "1.43.0"] + pub MATCH_SINGLE_BINDING, + complexity, + "a match with a single binding instead of using `let` statement" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for unnecessary '..' pattern binding on struct when all fields are explicitly matched. + /// + /// ### Why is this bad? + /// Correctness and readability. It's like having a wildcard pattern after + /// matching all enum variants explicitly. + /// + /// ### Example + /// ```rust + /// # struct A { a: i32 } + /// let a = A { a: 5 }; + /// + /// // Bad + /// match a { + /// A { a: 5, .. } => {}, + /// _ => {}, + /// } + /// + /// // Good + /// match a { + /// A { a: 5 } => {}, + /// _ => {}, + /// } + /// ``` + #[clippy::version = "1.43.0"] + pub REST_PAT_IN_FULLY_BOUND_STRUCTS, + restriction, + "a match on a struct that binds all fields but still uses the wildcard pattern" +} + +declare_clippy_lint! { + /// ### What it does + /// Lint for redundant pattern matching over `Result`, `Option`, + /// `std::task::Poll` or `std::net::IpAddr` + /// + /// ### Why is this bad? + /// It's more concise and clear to just use the proper + /// utility function + /// + /// ### Known problems + /// This will change the drop order for the matched type. Both `if let` and + /// `while let` will drop the value at the end of the block, both `if` and `while` will drop the + /// value before entering the block. For most types this change will not matter, but for a few + /// types this will not be an acceptable change (e.g. locks). See the + /// [reference](https://doc.rust-lang.org/reference/destructors.html#drop-scopes) for more about + /// drop order. + /// + /// ### Example + /// ```rust + /// # use std::task::Poll; + /// # use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + /// if let Ok(_) = Ok::(42) {} + /// if let Err(_) = Err::(42) {} + /// if let None = None::<()> {} + /// if let Some(_) = Some(42) {} + /// if let Poll::Pending = Poll::Pending::<()> {} + /// if let Poll::Ready(_) = Poll::Ready(42) {} + /// if let IpAddr::V4(_) = IpAddr::V4(Ipv4Addr::LOCALHOST) {} + /// if let IpAddr::V6(_) = IpAddr::V6(Ipv6Addr::LOCALHOST) {} + /// match Ok::(42) { + /// Ok(_) => true, + /// Err(_) => false, + /// }; + /// ``` + /// + /// The more idiomatic use would be: + /// + /// ```rust + /// # use std::task::Poll; + /// # use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + /// if Ok::(42).is_ok() {} + /// if Err::(42).is_err() {} + /// if None::<()>.is_none() {} + /// if Some(42).is_some() {} + /// if Poll::Pending::<()>.is_pending() {} + /// if Poll::Ready(42).is_ready() {} + /// if IpAddr::V4(Ipv4Addr::LOCALHOST).is_ipv4() {} + /// if IpAddr::V6(Ipv6Addr::LOCALHOST).is_ipv6() {} + /// Ok::(42).is_ok(); + /// ``` + #[clippy::version = "1.31.0"] + pub REDUNDANT_PATTERN_MATCHING, + style, + "use the proper utility function avoiding an `if let`" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `match` or `if let` expressions producing a + /// `bool` that could be written using `matches!` + /// + /// ### Why is this bad? + /// Readability and needless complexity. + /// + /// ### Known problems + /// This lint falsely triggers, if there are arms with + /// `cfg` attributes that remove an arm evaluating to `false`. + /// + /// ### Example + /// ```rust + /// let x = Some(5); + /// + /// // Bad + /// let a = match x { + /// Some(0) => true, + /// _ => false, + /// }; + /// + /// let a = if let Some(0) = x { + /// true + /// } else { + /// false + /// }; + /// + /// // Good + /// let a = matches!(x, Some(0)); + /// ``` + #[clippy::version = "1.47.0"] + pub MATCH_LIKE_MATCHES_MACRO, + style, + "a match that could be written with the matches! macro" +} + +declare_clippy_lint! { + /// ### What it does + /// Checks for `match` with identical arm bodies. + /// + /// ### Why is this bad? + /// This is probably a copy & paste error. If arm bodies + /// are the same on purpose, you can factor them + /// [using `|`](https://doc.rust-lang.org/book/patterns.html#multiple-patterns). + /// + /// ### Known problems + /// False positive possible with order dependent `match` + /// (see issue + /// [#860](https://github.com/rust-lang/rust-clippy/issues/860)). + /// + /// ### Example + /// ```rust,ignore + /// match foo { + /// Bar => bar(), + /// Quz => quz(), + /// Baz => bar(), // <= oops + /// } + /// ``` + /// + /// This should probably be + /// ```rust,ignore + /// match foo { + /// Bar => bar(), + /// Quz => quz(), + /// Baz => baz(), // <= fixed + /// } + /// ``` + /// + /// or if the original code was not a typo: + /// ```rust,ignore + /// match foo { + /// Bar | Baz => bar(), // <= shows the intent better + /// Quz => quz(), + /// } + /// ``` + #[clippy::version = "pre 1.29.0"] + pub MATCH_SAME_ARMS, + pedantic, + "`match` with identical arm bodies" +} + +#[derive(Default)] +pub struct Matches { + msrv: Option, + infallible_destructuring_match_linted: bool, +} + +impl Matches { + #[must_use] + pub fn new(msrv: Option) -> Self { + Self { + msrv, + ..Matches::default() + } + } +} + +impl_lint_pass!(Matches => [ + SINGLE_MATCH, + MATCH_REF_PATS, + MATCH_BOOL, + SINGLE_MATCH_ELSE, + MATCH_OVERLAPPING_ARM, + MATCH_WILD_ERR_ARM, + MATCH_AS_REF, + WILDCARD_ENUM_MATCH_ARM, + MATCH_WILDCARD_FOR_SINGLE_VARIANTS, + WILDCARD_IN_OR_PATTERNS, + MATCH_SINGLE_BINDING, + INFALLIBLE_DESTRUCTURING_MATCH, + REST_PAT_IN_FULLY_BOUND_STRUCTS, + REDUNDANT_PATTERN_MATCHING, + MATCH_LIKE_MATCHES_MACRO, + MATCH_SAME_ARMS, +]); + +impl<'tcx> LateLintPass<'tcx> for Matches { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if expr.span.from_expansion() { + return; + } + + redundant_pattern_match::check(cx, expr); + + if meets_msrv(self.msrv.as_ref(), &msrvs::MATCHES_MACRO) { + if !match_like_matches::check(cx, expr) { + match_same_arms::check(cx, expr); + } + } else { + match_same_arms::check(cx, expr); + } + + if let ExprKind::Match(ex, arms, MatchSource::Normal) = expr.kind { + single_match::check(cx, ex, arms, expr); + match_bool::check(cx, ex, arms, expr); + overlapping_arms::check(cx, ex, arms); + match_wild_err_arm::check(cx, ex, arms); + match_wild_enum::check(cx, ex, arms); + match_as_ref::check(cx, ex, arms, expr); + wild_in_or_pats::check(cx, arms); + + if self.infallible_destructuring_match_linted { + self.infallible_destructuring_match_linted = false; + } else { + match_single_binding::check(cx, ex, arms, expr); + } + } + if let ExprKind::Match(ex, arms, _) = expr.kind { + match_ref_pats::check(cx, ex, arms.iter().map(|el| el.pat), expr); + } + } + + fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'_>) { + self.infallible_destructuring_match_linted |= infalliable_detructuring_match::check(cx, local); + } + + fn check_pat(&mut self, cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>) { + rest_pat_in_fully_bound_struct::check(cx, pat); + } + + extract_msrv_attr!(LateContext); +} diff --git a/clippy_lints/src/matches/overlapping_arms.rs b/clippy_lints/src/matches/overlapping_arms.rs new file mode 100644 index 000000000000..7e6581266902 --- /dev/null +++ b/clippy_lints/src/matches/overlapping_arms.rs @@ -0,0 +1,181 @@ +use clippy_utils::consts::{constant, constant_full_int, miri_to_const, FullInt}; +use clippy_utils::diagnostics::span_lint_and_note; +use core::cmp::Ordering; +use rustc_hir::{Arm, Expr, PatKind, RangeEnd}; +use rustc_lint::LateContext; +use rustc_middle::ty::Ty; +use rustc_span::Span; + +use super::MATCH_OVERLAPPING_ARM; + +pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, ex: &'tcx Expr<'_>, arms: &'tcx [Arm<'_>]) { + if arms.len() >= 2 && cx.typeck_results().expr_ty(ex).is_integral() { + let ranges = all_ranges(cx, arms, cx.typeck_results().expr_ty(ex)); + if !ranges.is_empty() { + if let Some((start, end)) = overlapping(&ranges) { + span_lint_and_note( + cx, + MATCH_OVERLAPPING_ARM, + start.span, + "some ranges overlap", + Some(end.span), + "overlaps with this", + ); + } + } + } +} + +/// Gets the ranges for each range pattern arm. Applies `ty` bounds for open ranges. +fn all_ranges<'tcx>(cx: &LateContext<'tcx>, arms: &'tcx [Arm<'_>], ty: Ty<'tcx>) -> Vec> { + arms.iter() + .filter_map(|arm| { + if let Arm { pat, guard: None, .. } = *arm { + if let PatKind::Range(ref lhs, ref rhs, range_end) = pat.kind { + let lhs_const = match lhs { + Some(lhs) => constant(cx, cx.typeck_results(), lhs)?.0, + None => miri_to_const(ty.numeric_min_val(cx.tcx)?)?, + }; + let rhs_const = match rhs { + Some(rhs) => constant(cx, cx.typeck_results(), rhs)?.0, + None => miri_to_const(ty.numeric_max_val(cx.tcx)?)?, + }; + + let lhs_val = lhs_const.int_value(cx, ty)?; + let rhs_val = rhs_const.int_value(cx, ty)?; + + let rhs_bound = match range_end { + RangeEnd::Included => EndBound::Included(rhs_val), + RangeEnd::Excluded => EndBound::Excluded(rhs_val), + }; + return Some(SpannedRange { + span: pat.span, + node: (lhs_val, rhs_bound), + }); + } + + if let PatKind::Lit(value) = pat.kind { + let value = constant_full_int(cx, cx.typeck_results(), value)?; + return Some(SpannedRange { + span: pat.span, + node: (value, EndBound::Included(value)), + }); + } + } + None + }) + .collect() +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EndBound { + Included(T), + Excluded(T), +} + +#[derive(Debug, Eq, PartialEq)] +struct SpannedRange { + pub span: Span, + pub node: (T, EndBound), +} + +fn overlapping(ranges: &[SpannedRange]) -> Option<(&SpannedRange, &SpannedRange)> +where + T: Copy + Ord, +{ + #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] + enum BoundKind { + EndExcluded, + Start, + EndIncluded, + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + struct RangeBound<'a, T>(T, BoundKind, &'a SpannedRange); + + impl<'a, T: Copy + Ord> PartialOrd for RangeBound<'a, T> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl<'a, T: Copy + Ord> Ord for RangeBound<'a, T> { + fn cmp(&self, RangeBound(other_value, other_kind, _): &Self) -> Ordering { + let RangeBound(self_value, self_kind, _) = *self; + (self_value, self_kind).cmp(&(*other_value, *other_kind)) + } + } + + let mut values = Vec::with_capacity(2 * ranges.len()); + + for r @ SpannedRange { node: (start, end), .. } in ranges { + values.push(RangeBound(*start, BoundKind::Start, r)); + values.push(match end { + EndBound::Excluded(val) => RangeBound(*val, BoundKind::EndExcluded, r), + EndBound::Included(val) => RangeBound(*val, BoundKind::EndIncluded, r), + }); + } + + values.sort(); + + let mut started = vec![]; + + for RangeBound(_, kind, range) in values { + match kind { + BoundKind::Start => started.push(range), + BoundKind::EndExcluded | BoundKind::EndIncluded => { + let mut overlap = None; + + while let Some(last_started) = started.pop() { + if last_started == range { + break; + } + overlap = Some(last_started); + } + + if let Some(first_overlapping) = overlap { + return Some((range, first_overlapping)); + } + }, + } + } + + None +} + +#[test] +fn test_overlapping() { + use rustc_span::source_map::DUMMY_SP; + + let sp = |s, e| SpannedRange { + span: DUMMY_SP, + node: (s, e), + }; + + assert_eq!(None, overlapping::(&[])); + assert_eq!(None, overlapping(&[sp(1, EndBound::Included(4))])); + assert_eq!( + None, + overlapping(&[sp(1, EndBound::Included(4)), sp(5, EndBound::Included(6))]) + ); + assert_eq!( + None, + overlapping(&[ + sp(1, EndBound::Included(4)), + sp(5, EndBound::Included(6)), + sp(10, EndBound::Included(11)) + ],) + ); + assert_eq!( + Some((&sp(1, EndBound::Included(4)), &sp(3, EndBound::Included(6)))), + overlapping(&[sp(1, EndBound::Included(4)), sp(3, EndBound::Included(6))]) + ); + assert_eq!( + Some((&sp(5, EndBound::Included(6)), &sp(6, EndBound::Included(11)))), + overlapping(&[ + sp(1, EndBound::Included(4)), + sp(5, EndBound::Included(6)), + sp(6, EndBound::Included(11)) + ],) + ); +} diff --git a/clippy_lints/src/matches/redundant_pattern_match.rs b/clippy_lints/src/matches/redundant_pattern_match.rs new file mode 100644 index 000000000000..61c5fa0872f6 --- /dev/null +++ b/clippy_lints/src/matches/redundant_pattern_match.rs @@ -0,0 +1,436 @@ +use super::REDUNDANT_PATTERN_MATCHING; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::source::snippet; +use clippy_utils::sugg::Sugg; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, is_type_lang_item, match_type}; +use clippy_utils::{higher, match_def_path}; +use clippy_utils::{is_lang_ctor, is_trait_method, paths}; +use if_chain::if_chain; +use rustc_ast::ast::LitKind; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Applicability; +use rustc_hir::LangItem::{OptionNone, PollPending}; +use rustc_hir::{ + intravisit::{walk_expr, Visitor}, + Arm, Block, Expr, ExprKind, LangItem, MatchSource, Node, Pat, PatKind, QPath, UnOp, +}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, subst::GenericArgKind, DefIdTree, Ty}; +use rustc_span::sym; + +pub fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + if let Some(higher::IfLet { + if_else, + let_pat, + let_expr, + .. + }) = higher::IfLet::hir(cx, expr) + { + find_sugg_for_if_let(cx, expr, let_pat, let_expr, "if", if_else.is_some()); + } + if let ExprKind::Match(op, arms, MatchSource::Normal) = &expr.kind { + find_sugg_for_match(cx, expr, op, arms); + } + if let Some(higher::WhileLet { let_pat, let_expr, .. }) = higher::WhileLet::hir(expr) { + find_sugg_for_if_let(cx, expr, let_pat, let_expr, "while", false); + } +} + +/// Checks if the drop order for a type matters. Some std types implement drop solely to +/// deallocate memory. For these types, and composites containing them, changing the drop order +/// won't result in any observable side effects. +fn type_needs_ordered_drop<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { + type_needs_ordered_drop_inner(cx, ty, &mut FxHashSet::default()) +} + +fn type_needs_ordered_drop_inner<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, seen: &mut FxHashSet>) -> bool { + if !seen.insert(ty) { + return false; + } + if !ty.needs_drop(cx.tcx, cx.param_env) { + false + } else if !cx + .tcx + .lang_items() + .drop_trait() + .map_or(false, |id| implements_trait(cx, ty, id, &[])) + { + // This type doesn't implement drop, so no side effects here. + // Check if any component type has any. + match ty.kind() { + ty::Tuple(_) => ty.tuple_fields().any(|ty| type_needs_ordered_drop_inner(cx, ty, seen)), + ty::Array(ty, _) => type_needs_ordered_drop_inner(cx, ty, seen), + ty::Adt(adt, subs) => adt + .all_fields() + .map(|f| f.ty(cx.tcx, subs)) + .any(|ty| type_needs_ordered_drop_inner(cx, ty, seen)), + _ => true, + } + } + // Check for std types which implement drop, but only for memory allocation. + else if is_type_diagnostic_item(cx, ty, sym::Vec) + || is_type_lang_item(cx, ty, LangItem::OwnedBox) + || is_type_diagnostic_item(cx, ty, sym::Rc) + || is_type_diagnostic_item(cx, ty, sym::Arc) + || is_type_diagnostic_item(cx, ty, sym::cstring_type) + || is_type_diagnostic_item(cx, ty, sym::BTreeMap) + || is_type_diagnostic_item(cx, ty, sym::LinkedList) + || match_type(cx, ty, &paths::WEAK_RC) + || match_type(cx, ty, &paths::WEAK_ARC) + { + // Check all of the generic arguments. + if let ty::Adt(_, subs) = ty.kind() { + subs.types().any(|ty| type_needs_ordered_drop_inner(cx, ty, seen)) + } else { + true + } + } else { + true + } +} + +// Extract the generic arguments out of a type +fn try_get_generic_ty(ty: Ty<'_>, index: usize) -> Option> { + if_chain! { + if let ty::Adt(_, subs) = ty.kind(); + if let Some(sub) = subs.get(index); + if let GenericArgKind::Type(sub_ty) = sub.unpack(); + then { + Some(sub_ty) + } else { + None + } + } +} + +// Checks if there are any temporaries created in the given expression for which drop order +// matters. +fn temporaries_need_ordered_drop<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> bool { + struct V<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + res: bool, + } + impl<'a, 'tcx> Visitor<'tcx> for V<'a, 'tcx> { + fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { + match expr.kind { + // Taking the reference of a value leaves a temporary + // e.g. In `&String::new()` the string is a temporary value. + // Remaining fields are temporary values + // e.g. In `(String::new(), 0).1` the string is a temporary value. + ExprKind::AddrOf(_, _, expr) | ExprKind::Field(expr, _) => { + if !matches!(expr.kind, ExprKind::Path(_)) { + if type_needs_ordered_drop(self.cx, self.cx.typeck_results().expr_ty(expr)) { + self.res = true; + } else { + self.visit_expr(expr); + } + } + }, + // the base type is alway taken by reference. + // e.g. In `(vec![0])[0]` the vector is a temporary value. + ExprKind::Index(base, index) => { + if !matches!(base.kind, ExprKind::Path(_)) { + if type_needs_ordered_drop(self.cx, self.cx.typeck_results().expr_ty(base)) { + self.res = true; + } else { + self.visit_expr(base); + } + } + self.visit_expr(index); + }, + // Method calls can take self by reference. + // e.g. In `String::new().len()` the string is a temporary value. + ExprKind::MethodCall(_, [self_arg, args @ ..], _) => { + if !matches!(self_arg.kind, ExprKind::Path(_)) { + let self_by_ref = self + .cx + .typeck_results() + .type_dependent_def_id(expr.hir_id) + .map_or(false, |id| self.cx.tcx.fn_sig(id).skip_binder().inputs()[0].is_ref()); + if self_by_ref && type_needs_ordered_drop(self.cx, self.cx.typeck_results().expr_ty(self_arg)) { + self.res = true; + } else { + self.visit_expr(self_arg); + } + } + args.iter().for_each(|arg| self.visit_expr(arg)); + }, + // Either explicitly drops values, or changes control flow. + ExprKind::DropTemps(_) + | ExprKind::Ret(_) + | ExprKind::Break(..) + | ExprKind::Yield(..) + | ExprKind::Block(Block { expr: None, .. }, _) + | ExprKind::Loop(..) => (), + + // Only consider the final expression. + ExprKind::Block(Block { expr: Some(expr), .. }, _) => self.visit_expr(expr), + + _ => walk_expr(self, expr), + } + } + } + + let mut v = V { cx, res: false }; + v.visit_expr(expr); + v.res +} + +fn find_sugg_for_if_let<'tcx>( + cx: &LateContext<'tcx>, + expr: &'tcx Expr<'_>, + let_pat: &Pat<'_>, + let_expr: &'tcx Expr<'_>, + keyword: &'static str, + has_else: bool, +) { + // also look inside refs + // if we have &None for example, peel it so we can detect "if let None = x" + let check_pat = match let_pat.kind { + PatKind::Ref(inner, _mutability) => inner, + _ => let_pat, + }; + let op_ty = cx.typeck_results().expr_ty(let_expr); + // Determine which function should be used, and the type contained by the corresponding + // variant. + let (good_method, inner_ty) = match check_pat.kind { + PatKind::TupleStruct(ref qpath, [sub_pat], _) => { + if let PatKind::Wild = sub_pat.kind { + let res = cx.typeck_results().qpath_res(qpath, check_pat.hir_id); + let Some(id) = res.opt_def_id().and_then(|ctor_id| cx.tcx.parent(ctor_id)) else { return }; + let lang_items = cx.tcx.lang_items(); + if Some(id) == lang_items.result_ok_variant() { + ("is_ok()", try_get_generic_ty(op_ty, 0).unwrap_or(op_ty)) + } else if Some(id) == lang_items.result_err_variant() { + ("is_err()", try_get_generic_ty(op_ty, 1).unwrap_or(op_ty)) + } else if Some(id) == lang_items.option_some_variant() { + ("is_some()", op_ty) + } else if Some(id) == lang_items.poll_ready_variant() { + ("is_ready()", op_ty) + } else if match_def_path(cx, id, &paths::IPADDR_V4) { + ("is_ipv4()", op_ty) + } else if match_def_path(cx, id, &paths::IPADDR_V6) { + ("is_ipv6()", op_ty) + } else { + return; + } + } else { + return; + } + }, + PatKind::Path(ref path) => { + let method = if is_lang_ctor(cx, path, OptionNone) { + "is_none()" + } else if is_lang_ctor(cx, path, PollPending) { + "is_pending()" + } else { + return; + }; + // `None` and `Pending` don't have an inner type. + (method, cx.tcx.types.unit) + }, + _ => return, + }; + + // If this is the last expression in a block or there is an else clause then the whole + // type needs to be considered, not just the inner type of the branch being matched on. + // Note the last expression in a block is dropped after all local bindings. + let check_ty = if has_else + || (keyword == "if" && matches!(cx.tcx.hir().parent_iter(expr.hir_id).next(), Some((_, Node::Block(..))))) + { + op_ty + } else { + inner_ty + }; + + // All temporaries created in the scrutinee expression are dropped at the same time as the + // scrutinee would be, so they have to be considered as well. + // e.g. in `if let Some(x) = foo.lock().unwrap().baz.as_ref() { .. }` the lock will be held + // for the duration if body. + let needs_drop = type_needs_ordered_drop(cx, check_ty) || temporaries_need_ordered_drop(cx, let_expr); + + // check that `while_let_on_iterator` lint does not trigger + if_chain! { + if keyword == "while"; + if let ExprKind::MethodCall(method_path, _, _) = let_expr.kind; + if method_path.ident.name == sym::next; + if is_trait_method(cx, let_expr, sym::Iterator); + then { + return; + } + } + + let result_expr = match &let_expr.kind { + ExprKind::AddrOf(_, _, borrowed) => borrowed, + ExprKind::Unary(UnOp::Deref, deref) => deref, + _ => let_expr, + }; + + span_lint_and_then( + cx, + REDUNDANT_PATTERN_MATCHING, + let_pat.span, + &format!("redundant pattern matching, consider using `{}`", good_method), + |diag| { + // if/while let ... = ... { ... } + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + let expr_span = expr.span; + + // if/while let ... = ... { ... } + // ^^^ + let op_span = result_expr.span.source_callsite(); + + // if/while let ... = ... { ... } + // ^^^^^^^^^^^^^^^^^^^ + let span = expr_span.until(op_span.shrink_to_hi()); + + let app = if needs_drop { + Applicability::MaybeIncorrect + } else { + Applicability::MachineApplicable + }; + + let sugg = Sugg::hir_with_macro_callsite(cx, result_expr, "_") + .maybe_par() + .to_string(); + + diag.span_suggestion(span, "try this", format!("{} {}.{}", keyword, sugg, good_method), app); + + if needs_drop { + diag.note("this will change drop order of the result, as well as all temporaries"); + diag.note("add `#[allow(clippy::redundant_pattern_matching)]` if this is important"); + } + }, + ); +} + +fn find_sugg_for_match<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, op: &Expr<'_>, arms: &[Arm<'_>]) { + if arms.len() == 2 { + let node_pair = (&arms[0].pat.kind, &arms[1].pat.kind); + + let found_good_method = match node_pair { + ( + PatKind::TupleStruct(ref path_left, patterns_left, _), + PatKind::TupleStruct(ref path_right, patterns_right, _), + ) if patterns_left.len() == 1 && patterns_right.len() == 1 => { + if let (PatKind::Wild, PatKind::Wild) = (&patterns_left[0].kind, &patterns_right[0].kind) { + find_good_method_for_match( + cx, + arms, + path_left, + path_right, + &paths::RESULT_OK, + &paths::RESULT_ERR, + "is_ok()", + "is_err()", + ) + .or_else(|| { + find_good_method_for_match( + cx, + arms, + path_left, + path_right, + &paths::IPADDR_V4, + &paths::IPADDR_V6, + "is_ipv4()", + "is_ipv6()", + ) + }) + } else { + None + } + }, + (PatKind::TupleStruct(ref path_left, patterns, _), PatKind::Path(ref path_right)) + | (PatKind::Path(ref path_left), PatKind::TupleStruct(ref path_right, patterns, _)) + if patterns.len() == 1 => + { + if let PatKind::Wild = patterns[0].kind { + find_good_method_for_match( + cx, + arms, + path_left, + path_right, + &paths::OPTION_SOME, + &paths::OPTION_NONE, + "is_some()", + "is_none()", + ) + .or_else(|| { + find_good_method_for_match( + cx, + arms, + path_left, + path_right, + &paths::POLL_READY, + &paths::POLL_PENDING, + "is_ready()", + "is_pending()", + ) + }) + } else { + None + } + }, + _ => None, + }; + + if let Some(good_method) = found_good_method { + let span = expr.span.to(op.span); + let result_expr = match &op.kind { + ExprKind::AddrOf(_, _, borrowed) => borrowed, + _ => op, + }; + span_lint_and_then( + cx, + REDUNDANT_PATTERN_MATCHING, + expr.span, + &format!("redundant pattern matching, consider using `{}`", good_method), + |diag| { + diag.span_suggestion( + span, + "try this", + format!("{}.{}", snippet(cx, result_expr.span, "_"), good_method), + Applicability::MaybeIncorrect, // snippet + ); + }, + ); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn find_good_method_for_match<'a>( + cx: &LateContext<'_>, + arms: &[Arm<'_>], + path_left: &QPath<'_>, + path_right: &QPath<'_>, + expected_left: &[&str], + expected_right: &[&str], + should_be_left: &'a str, + should_be_right: &'a str, +) -> Option<&'a str> { + let left_id = cx + .typeck_results() + .qpath_res(path_left, arms[0].pat.hir_id) + .opt_def_id()?; + let right_id = cx + .typeck_results() + .qpath_res(path_right, arms[1].pat.hir_id) + .opt_def_id()?; + let body_node_pair = if match_def_path(cx, left_id, expected_left) && match_def_path(cx, right_id, expected_right) { + (&(*arms[0].body).kind, &(*arms[1].body).kind) + } else if match_def_path(cx, right_id, expected_left) && match_def_path(cx, right_id, expected_right) { + (&(*arms[1].body).kind, &(*arms[0].body).kind) + } else { + return None; + }; + + match body_node_pair { + (ExprKind::Lit(ref lit_left), ExprKind::Lit(ref lit_right)) => match (&lit_left.node, &lit_right.node) { + (LitKind::Bool(true), LitKind::Bool(false)) => Some(should_be_left), + (LitKind::Bool(false), LitKind::Bool(true)) => Some(should_be_right), + _ => None, + }, + _ => None, + } +} diff --git a/clippy_lints/src/matches/rest_pat_in_fully_bound_struct.rs b/clippy_lints/src/matches/rest_pat_in_fully_bound_struct.rs new file mode 100644 index 000000000000..5076239a57c4 --- /dev/null +++ b/clippy_lints/src/matches/rest_pat_in_fully_bound_struct.rs @@ -0,0 +1,29 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use rustc_hir::{Pat, PatKind, QPath}; +use rustc_lint::LateContext; +use rustc_middle::ty; + +use super::REST_PAT_IN_FULLY_BOUND_STRUCTS; + +pub(crate) fn check(cx: &LateContext<'_>, pat: &Pat<'_>) { + if_chain! { + if !pat.span.from_expansion(); + if let PatKind::Struct(QPath::Resolved(_, path), fields, true) = pat.kind; + if let Some(def_id) = path.res.opt_def_id(); + let ty = cx.tcx.type_of(def_id); + if let ty::Adt(def, _) = ty.kind(); + if def.is_struct() || def.is_union(); + if fields.len() == def.non_enum_variant().fields.len(); + + then { + span_lint_and_help( + cx, + REST_PAT_IN_FULLY_BOUND_STRUCTS, + pat.span, + "unnecessary use of `..` pattern in struct binding. All fields were already bound", + None, + "consider removing `..` from this binding", + ); + } + } +} diff --git a/clippy_lints/src/matches/single_match.rs b/clippy_lints/src/matches/single_match.rs new file mode 100644 index 000000000000..6ba279eaf122 --- /dev/null +++ b/clippy_lints/src/matches/single_match.rs @@ -0,0 +1,269 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::{expr_block, snippet}; +use clippy_utils::ty::{implements_trait, match_type, peel_mid_ty_refs}; +use clippy_utils::{ + is_lint_allowed, is_unit_expr, is_wild, paths, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs, +}; +use core::cmp::max; +use rustc_errors::Applicability; +use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, Pat, PatKind}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty, TyS}; + +use super::{MATCH_BOOL, SINGLE_MATCH, SINGLE_MATCH_ELSE}; + +#[rustfmt::skip] +pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) { + if arms.len() == 2 && arms[0].guard.is_none() && arms[1].guard.is_none() { + if expr.span.from_expansion() { + // Don't lint match expressions present in + // macro_rules! block + return; + } + if let PatKind::Or(..) = arms[0].pat.kind { + // don't lint for or patterns for now, this makes + // the lint noisy in unnecessary situations + return; + } + let els = arms[1].body; + let els = if is_unit_expr(peel_blocks(els)) { + None + } else if let ExprKind::Block(Block { stmts, expr: block_expr, .. }, _) = els.kind { + if stmts.len() == 1 && block_expr.is_none() || stmts.is_empty() && block_expr.is_some() { + // single statement/expr "else" block, don't lint + return; + } + // block with 2+ statements or 1 expr and 1+ statement + Some(els) + } else { + // not a block, don't lint + return; + }; + + let ty = cx.typeck_results().expr_ty(ex); + if *ty.kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id) { + check_single_pattern(cx, ex, arms, expr, els); + check_opt_like(cx, ex, arms, expr, ty, els); + } + } +} + +fn check_single_pattern( + cx: &LateContext<'_>, + ex: &Expr<'_>, + arms: &[Arm<'_>], + expr: &Expr<'_>, + els: Option<&Expr<'_>>, +) { + if is_wild(arms[1].pat) { + report_single_pattern(cx, ex, arms, expr, els); + } +} + +fn report_single_pattern( + cx: &LateContext<'_>, + ex: &Expr<'_>, + arms: &[Arm<'_>], + expr: &Expr<'_>, + els: Option<&Expr<'_>>, +) { + let lint = if els.is_some() { SINGLE_MATCH_ELSE } else { SINGLE_MATCH }; + let els_str = els.map_or(String::new(), |els| { + format!(" else {}", expr_block(cx, els, None, "..", Some(expr.span))) + }); + + let (pat, pat_ref_count) = peel_hir_pat_refs(arms[0].pat); + let (msg, sugg) = if_chain! { + if let PatKind::Path(_) | PatKind::Lit(_) = pat.kind; + let (ty, ty_ref_count) = peel_mid_ty_refs(cx.typeck_results().expr_ty(ex)); + if let Some(spe_trait_id) = cx.tcx.lang_items().structural_peq_trait(); + if let Some(pe_trait_id) = cx.tcx.lang_items().eq_trait(); + if ty.is_integral() || ty.is_char() || ty.is_str() + || (implements_trait(cx, ty, spe_trait_id, &[]) + && implements_trait(cx, ty, pe_trait_id, &[ty.into()])); + then { + // scrutinee derives PartialEq and the pattern is a constant. + let pat_ref_count = match pat.kind { + // string literals are already a reference. + PatKind::Lit(Expr { kind: ExprKind::Lit(lit), .. }) if lit.node.is_str() => pat_ref_count + 1, + _ => pat_ref_count, + }; + // References are only implicitly added to the pattern, so no overflow here. + // e.g. will work: match &Some(_) { Some(_) => () } + // will not: match Some(_) { &Some(_) => () } + let ref_count_diff = ty_ref_count - pat_ref_count; + + // Try to remove address of expressions first. + let (ex, removed) = peel_n_hir_expr_refs(ex, ref_count_diff); + let ref_count_diff = ref_count_diff - removed; + + let msg = "you seem to be trying to use `match` for an equality check. Consider using `if`"; + let sugg = format!( + "if {} == {}{} {}{}", + snippet(cx, ex.span, ".."), + // PartialEq for different reference counts may not exist. + "&".repeat(ref_count_diff), + snippet(cx, arms[0].pat.span, ".."), + expr_block(cx, arms[0].body, None, "..", Some(expr.span)), + els_str, + ); + (msg, sugg) + } else { + let msg = "you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`"; + let sugg = format!( + "if let {} = {} {}{}", + snippet(cx, arms[0].pat.span, ".."), + snippet(cx, ex.span, ".."), + expr_block(cx, arms[0].body, None, "..", Some(expr.span)), + els_str, + ); + (msg, sugg) + } + }; + + span_lint_and_sugg( + cx, + lint, + expr.span, + msg, + "try this", + sugg, + Applicability::HasPlaceholders, + ); +} + +fn check_opt_like<'a>( + cx: &LateContext<'a>, + ex: &Expr<'_>, + arms: &[Arm<'_>], + expr: &Expr<'_>, + ty: Ty<'a>, + els: Option<&Expr<'_>>, +) { + // list of candidate `Enum`s we know will never get any more members + let candidates = &[ + (&paths::COW, "Borrowed"), + (&paths::COW, "Cow::Borrowed"), + (&paths::COW, "Cow::Owned"), + (&paths::COW, "Owned"), + (&paths::OPTION, "None"), + (&paths::RESULT, "Err"), + (&paths::RESULT, "Ok"), + ]; + + // 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; + } + } + false + }; + if paths_and_types.iter().all(in_candidate_enum) { + report_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/clippy_lints/src/matches/wild_in_or_pats.rs b/clippy_lints/src/matches/wild_in_or_pats.rs new file mode 100644 index 000000000000..459513e65bfa --- /dev/null +++ b/clippy_lints/src/matches/wild_in_or_pats.rs @@ -0,0 +1,24 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::is_wild; +use rustc_hir::{Arm, PatKind}; +use rustc_lint::LateContext; + +use super::WILDCARD_IN_OR_PATTERNS; + +pub(crate) fn check(cx: &LateContext<'_>, arms: &[Arm<'_>]) { + for arm in arms { + if let PatKind::Or(fields) = arm.pat.kind { + // look for multiple fields in this arm that contains at least one Wild pattern + if fields.len() > 1 && fields.iter().any(is_wild) { + span_lint_and_help( + cx, + WILDCARD_IN_OR_PATTERNS, + arm.pat.span, + "wildcard pattern covers any other pattern as it will match anyway", + None, + "consider handling `_` separately", + ); + } + } + } +}