1 use clippy_utils::source::{snippet_opt, span_starts_with, walk_span_to_context};
2 use clippy_utils::{higher, in_constant, meets_msrv, msrvs};
3 use rustc_hir::{Arm, Expr, ExprKind, Local, MatchSource, Pat};
4 use rustc_lexer::{tokenize, TokenKind};
5 use rustc_lint::{LateContext, LateLintPass, LintContext};
6 use rustc_middle::lint::in_external_macro;
7 use rustc_semver::RustcVersion;
8 use rustc_session::{declare_tool_lint, impl_lint_pass};
9 use rustc_span::{Span, SpanData, SyntaxContext};
11 mod collapsible_match;
12 mod infallible_destructuring_match;
16 mod match_like_matches;
17 mod match_on_vec_items;
20 mod match_single_binding;
21 mod match_str_case_mismatch;
23 mod match_wild_err_arm;
26 mod redundant_pattern_match;
27 mod rest_pat_in_fully_bound_struct;
31 declare_clippy_lint! {
33 /// Checks for matches with a single arm where an `if let`
34 /// will usually suffice.
36 /// ### Why is this bad?
37 /// Just readability – `if let` nests less than a `match`.
41 /// # fn bar(stool: &str) {}
42 /// # let x = Some("abc");
45 /// Some(ref foo) => bar(foo),
50 /// if let Some(ref foo) = x {
54 #[clippy::version = "pre 1.29.0"]
57 "a `match` statement with a single nontrivial arm (i.e., where the other arm is `_ => {}`) instead of `if let`"
60 declare_clippy_lint! {
62 /// Checks for matches with two arms where an `if let else` will
65 /// ### Why is this bad?
66 /// Just readability – `if let` nests less than a `match`.
68 /// ### Known problems
69 /// Personal style preferences may differ.
75 /// # fn bar(foo: &usize) {}
76 /// # let other_ref: usize = 1;
77 /// # let x: Option<&usize> = Some(&1);
79 /// Some(ref foo) => bar(foo),
80 /// _ => bar(&other_ref),
84 /// Using `if let` with `else`:
87 /// # fn bar(foo: &usize) {}
88 /// # let other_ref: usize = 1;
89 /// # let x: Option<&usize> = Some(&1);
90 /// if let Some(ref foo) = x {
96 #[clippy::version = "pre 1.29.0"]
97 pub SINGLE_MATCH_ELSE,
99 "a `match` statement with two arms where the second arm's pattern is a placeholder instead of a specific match pattern"
102 declare_clippy_lint! {
104 /// Checks for matches where all arms match a reference,
105 /// suggesting to remove the reference and deref the matched expression
106 /// instead. It also checks for `if let &foo = bar` blocks.
108 /// ### Why is this bad?
109 /// It just makes the code less readable. That reference
110 /// destructuring adds nothing to the code.
116 /// &A(ref y) => foo(y),
123 /// A(ref y) => foo(y),
128 #[clippy::version = "pre 1.29.0"]
131 "a `match` or `if let` with all arms prefixed with `&` instead of deref-ing the match expression"
134 declare_clippy_lint! {
136 /// Checks for matches where match expression is a `bool`. It
137 /// suggests to replace the expression with an `if...else` block.
139 /// ### Why is this bad?
140 /// It makes the code less readable.
146 /// let condition: bool = true;
147 /// match condition {
152 /// Use if/else instead:
156 /// let condition: bool = true;
163 #[clippy::version = "pre 1.29.0"]
166 "a `match` on a boolean expression instead of an `if..else` block"
169 declare_clippy_lint! {
171 /// Checks for overlapping match arms.
173 /// ### Why is this bad?
174 /// It is likely to be an error and if not, makes the code
181 /// 1..=10 => println!("1 ... 10"),
182 /// 5..=15 => println!("5 ... 15"),
186 #[clippy::version = "pre 1.29.0"]
187 pub MATCH_OVERLAPPING_ARM,
189 "a `match` with overlapping arms"
192 declare_clippy_lint! {
194 /// Checks for arm which matches all errors with `Err(_)`
195 /// and take drastic actions like `panic!`.
197 /// ### Why is this bad?
198 /// It is generally a bad practice, similar to
199 /// catching all exceptions in java with `catch(Exception)`
203 /// let x: Result<i32, &str> = Ok(3);
205 /// Ok(_) => println!("ok"),
206 /// Err(_) => panic!("err"),
209 #[clippy::version = "pre 1.29.0"]
210 pub MATCH_WILD_ERR_ARM,
212 "a `match` with `Err(_)` arm and take drastic actions"
215 declare_clippy_lint! {
217 /// Checks for match which is used to add a reference to an
220 /// ### Why is this bad?
221 /// Using `as_ref()` or `as_mut()` instead is shorter.
225 /// let x: Option<()> = None;
228 /// let r: Option<&()> = match x {
230 /// Some(ref v) => Some(v),
234 /// let r: Option<&()> = x.as_ref();
236 #[clippy::version = "pre 1.29.0"]
239 "a `match` on an Option value instead of using `as_ref()` or `as_mut`"
242 declare_clippy_lint! {
244 /// Checks for wildcard enum matches using `_`.
246 /// ### Why is this bad?
247 /// New enum variants added by library updates can be missed.
249 /// ### Known problems
250 /// Suggested replacements may be incorrect if guards exhaustively cover some
251 /// variants, and also may not use correct path to enum if it's not present in the current scope.
255 /// # enum Foo { A(usize), B(usize) }
256 /// # let x = Foo::B(1);
269 #[clippy::version = "1.34.0"]
270 pub WILDCARD_ENUM_MATCH_ARM,
272 "a wildcard enum match arm using `_`"
275 declare_clippy_lint! {
277 /// Checks for wildcard enum matches for a single variant.
279 /// ### Why is this bad?
280 /// New enum variants added by library updates can be missed.
282 /// ### Known problems
283 /// Suggested replacements may not use correct path to enum
284 /// if it's not present in the current scope.
288 /// # enum Foo { A, B, C }
289 /// # let x = Foo::B;
304 #[clippy::version = "1.45.0"]
305 pub MATCH_WILDCARD_FOR_SINGLE_VARIANTS,
307 "a wildcard enum match for a single variant"
310 declare_clippy_lint! {
312 /// Checks for wildcard pattern used with others patterns in same match arm.
314 /// ### Why is this bad?
315 /// Wildcard pattern already covers any other pattern as it will match anyway.
316 /// It makes the code less readable, especially to spot wildcard pattern use in match arm.
332 #[clippy::version = "1.42.0"]
333 pub WILDCARD_IN_OR_PATTERNS,
335 "a wildcard pattern used with others patterns in same match arm"
338 declare_clippy_lint! {
340 /// Checks for matches being used to destructure a single-variant enum
341 /// or tuple struct where a `let` will suffice.
343 /// ### Why is this bad?
344 /// Just readability – `let` doesn't nest, whereas a `match` does.
352 /// let wrapper = Wrapper::Data(42);
354 /// let data = match wrapper {
355 /// Wrapper::Data(i) => i,
359 /// The correct use would be:
365 /// let wrapper = Wrapper::Data(42);
366 /// let Wrapper::Data(data) = wrapper;
368 #[clippy::version = "pre 1.29.0"]
369 pub INFALLIBLE_DESTRUCTURING_MATCH,
371 "a `match` statement with a single infallible arm instead of a `let`"
374 declare_clippy_lint! {
376 /// Checks for useless match that binds to only one value.
378 /// ### Why is this bad?
379 /// Readability and needless complexity.
381 /// ### Known problems
382 /// Suggested replacements may be incorrect when `match`
383 /// is actually binding temporary value, bringing a 'dropped while borrowed' error.
398 /// let (c, d) = (a, b);
400 #[clippy::version = "1.43.0"]
401 pub MATCH_SINGLE_BINDING,
403 "a match with a single binding instead of using `let` statement"
406 declare_clippy_lint! {
408 /// Checks for unnecessary '..' pattern binding on struct when all fields are explicitly matched.
410 /// ### Why is this bad?
411 /// Correctness and readability. It's like having a wildcard pattern after
412 /// matching all enum variants explicitly.
416 /// # struct A { a: i32 }
417 /// let a = A { a: 5 };
421 /// A { a: 5, .. } => {},
427 /// A { a: 5 } => {},
431 #[clippy::version = "1.43.0"]
432 pub REST_PAT_IN_FULLY_BOUND_STRUCTS,
434 "a match on a struct that binds all fields but still uses the wildcard pattern"
437 declare_clippy_lint! {
439 /// Lint for redundant pattern matching over `Result`, `Option`,
440 /// `std::task::Poll` or `std::net::IpAddr`
442 /// ### Why is this bad?
443 /// It's more concise and clear to just use the proper
446 /// ### Known problems
447 /// This will change the drop order for the matched type. Both `if let` and
448 /// `while let` will drop the value at the end of the block, both `if` and `while` will drop the
449 /// value before entering the block. For most types this change will not matter, but for a few
450 /// types this will not be an acceptable change (e.g. locks). See the
451 /// [reference](https://doc.rust-lang.org/reference/destructors.html#drop-scopes) for more about
456 /// # use std::task::Poll;
457 /// # use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
458 /// if let Ok(_) = Ok::<i32, i32>(42) {}
459 /// if let Err(_) = Err::<i32, i32>(42) {}
460 /// if let None = None::<()> {}
461 /// if let Some(_) = Some(42) {}
462 /// if let Poll::Pending = Poll::Pending::<()> {}
463 /// if let Poll::Ready(_) = Poll::Ready(42) {}
464 /// if let IpAddr::V4(_) = IpAddr::V4(Ipv4Addr::LOCALHOST) {}
465 /// if let IpAddr::V6(_) = IpAddr::V6(Ipv6Addr::LOCALHOST) {}
466 /// match Ok::<i32, i32>(42) {
472 /// The more idiomatic use would be:
475 /// # use std::task::Poll;
476 /// # use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
477 /// if Ok::<i32, i32>(42).is_ok() {}
478 /// if Err::<i32, i32>(42).is_err() {}
479 /// if None::<()>.is_none() {}
480 /// if Some(42).is_some() {}
481 /// if Poll::Pending::<()>.is_pending() {}
482 /// if Poll::Ready(42).is_ready() {}
483 /// if IpAddr::V4(Ipv4Addr::LOCALHOST).is_ipv4() {}
484 /// if IpAddr::V6(Ipv6Addr::LOCALHOST).is_ipv6() {}
485 /// Ok::<i32, i32>(42).is_ok();
487 #[clippy::version = "1.31.0"]
488 pub REDUNDANT_PATTERN_MATCHING,
490 "use the proper utility function avoiding an `if let`"
493 declare_clippy_lint! {
495 /// Checks for `match` or `if let` expressions producing a
496 /// `bool` that could be written using `matches!`
498 /// ### Why is this bad?
499 /// Readability and needless complexity.
501 /// ### Known problems
502 /// This lint falsely triggers, if there are arms with
503 /// `cfg` attributes that remove an arm evaluating to `false`.
510 /// let a = match x {
515 /// let a = if let Some(0) = x {
522 /// let a = matches!(x, Some(0));
524 #[clippy::version = "1.47.0"]
525 pub MATCH_LIKE_MATCHES_MACRO,
527 "a match that could be written with the matches! macro"
530 declare_clippy_lint! {
532 /// Checks for `match` with identical arm bodies.
534 /// ### Why is this bad?
535 /// This is probably a copy & paste error. If arm bodies
536 /// are the same on purpose, you can factor them
537 /// [using `|`](https://doc.rust-lang.org/book/patterns.html#multiple-patterns).
539 /// ### Known problems
540 /// False positive possible with order dependent `match`
542 /// [#860](https://github.com/rust-lang/rust-clippy/issues/860)).
549 /// Baz => bar(), // <= oops
553 /// This should probably be
558 /// Baz => baz(), // <= fixed
562 /// or if the original code was not a typo:
565 /// Bar | Baz => bar(), // <= shows the intent better
569 #[clippy::version = "pre 1.29.0"]
572 "`match` with identical arm bodies"
575 declare_clippy_lint! {
577 /// Checks for unnecessary `match` or match-like `if let` returns for `Option` and `Result`
578 /// when function signatures are the same.
580 /// ### Why is this bad?
581 /// This `match` block does nothing and might not be what the coder intended.
585 /// fn foo() -> Result<(), i32> {
587 /// Ok(val) => Ok(val),
588 /// Err(err) => Err(err),
592 /// fn bar() -> Option<i32> {
593 /// if let Some(val) = option {
601 /// Could be replaced as
604 /// fn foo() -> Result<(), i32> {
608 /// fn bar() -> Option<i32> {
612 #[clippy::version = "1.61.0"]
615 "`match` or match-like `if let` that are unnecessary"
618 declare_clippy_lint! {
620 /// Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
621 /// without adding any branches.
623 /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only
624 /// cases where merging would most likely make the code more readable.
626 /// ### Why is this bad?
627 /// It is unnecessarily verbose and complex.
631 /// fn func(opt: Option<Result<u64, String>>) {
632 /// let n = match opt {
633 /// Some(n) => match n {
643 /// fn func(opt: Option<Result<u64, String>>) {
644 /// let n = match opt {
645 /// Some(Ok(n)) => n,
650 #[clippy::version = "1.50.0"]
651 pub COLLAPSIBLE_MATCH,
653 "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
656 declare_clippy_lint! {
658 /// Finds patterns that reimplement `Option::unwrap_or` or `Result::unwrap_or`.
660 /// ### Why is this bad?
661 /// Concise code helps focusing on behavior instead of boilerplate.
665 /// let foo: Option<i32> = None;
674 /// let foo: Option<i32> = None;
675 /// foo.unwrap_or(1);
677 #[clippy::version = "1.49.0"]
678 pub MANUAL_UNWRAP_OR,
680 "finds patterns that can be encoded more concisely with `Option::unwrap_or` or `Result::unwrap_or`"
683 declare_clippy_lint! {
685 /// Checks for `match vec[idx]` or `match vec[n..m]`.
687 /// ### Why is this bad?
688 /// This can panic at runtime.
692 /// let arr = vec![0, 1, 2, 3];
697 /// 0 => println!("{}", 0),
698 /// 1 => println!("{}", 3),
704 /// let arr = vec![0, 1, 2, 3];
708 /// match arr.get(idx) {
709 /// Some(0) => println!("{}", 0),
710 /// Some(1) => println!("{}", 3),
714 #[clippy::version = "1.45.0"]
715 pub MATCH_ON_VEC_ITEMS,
717 "matching on vector elements can panic"
720 declare_clippy_lint! {
722 /// Checks for `match` expressions modifying the case of a string with non-compliant arms
724 /// ### Why is this bad?
725 /// The arm is unreachable, which is likely a mistake
729 /// # let text = "Foo";
730 /// match &*text.to_ascii_lowercase() {
738 /// # let text = "Foo";
739 /// match &*text.to_ascii_lowercase() {
745 #[clippy::version = "1.58.0"]
746 pub MATCH_STR_CASE_MISMATCH,
748 "creation of a case altering match expression with non-compliant arms"
753 msrv: Option<RustcVersion>,
754 infallible_destructuring_match_linted: bool,
759 pub fn new(msrv: Option<RustcVersion>) -> Self {
767 impl_lint_pass!(Matches => [
772 MATCH_OVERLAPPING_ARM,
775 WILDCARD_ENUM_MATCH_ARM,
776 MATCH_WILDCARD_FOR_SINGLE_VARIANTS,
777 WILDCARD_IN_OR_PATTERNS,
778 MATCH_SINGLE_BINDING,
779 INFALLIBLE_DESTRUCTURING_MATCH,
780 REST_PAT_IN_FULLY_BOUND_STRUCTS,
781 REDUNDANT_PATTERN_MATCHING,
782 MATCH_LIKE_MATCHES_MACRO,
788 MATCH_STR_CASE_MISMATCH,
791 impl<'tcx> LateLintPass<'tcx> for Matches {
792 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
793 if in_external_macro(cx.sess(), expr.span) {
796 let from_expansion = expr.span.from_expansion();
798 if let ExprKind::Match(ex, arms, source) = expr.kind {
799 if !span_starts_with(cx, expr.span, "match") {
803 collapsible_match::check_match(cx, arms);
805 // These don't depend on a relationship between multiple arms
806 match_wild_err_arm::check(cx, ex, arms);
807 wild_in_or_pats::check(cx, arms);
810 if !from_expansion && !contains_cfg_arm(cx, expr, ex, arms) {
811 if source == MatchSource::Normal {
812 if !(meets_msrv(self.msrv, msrvs::MATCHES_MACRO)
813 && match_like_matches::check_match(cx, expr, ex, arms))
815 match_same_arms::check(cx, arms);
818 redundant_pattern_match::check_match(cx, expr, ex, arms);
819 single_match::check(cx, ex, arms, expr);
820 match_bool::check(cx, ex, arms, expr);
821 overlapping_arms::check(cx, ex, arms);
822 match_wild_enum::check(cx, ex, arms);
823 match_as_ref::check(cx, ex, arms, expr);
824 needless_match::check_match(cx, ex, arms, expr);
825 match_on_vec_items::check(cx, ex);
826 match_str_case_mismatch::check(cx, ex, arms);
828 if !in_constant(cx, expr.hir_id) {
829 manual_unwrap_or::check(cx, expr, ex, arms);
832 if self.infallible_destructuring_match_linted {
833 self.infallible_destructuring_match_linted = false;
835 match_single_binding::check(cx, ex, arms, expr);
838 match_ref_pats::check(cx, ex, arms.iter().map(|el| el.pat), expr);
840 } else if let Some(if_let) = higher::IfLet::hir(cx, expr) {
841 collapsible_match::check_if_let(cx, if_let.let_pat, if_let.if_then, if_let.if_else);
843 if let Some(else_expr) = if_let.if_else {
844 if meets_msrv(self.msrv, msrvs::MATCHES_MACRO) {
845 match_like_matches::check_if_let(
855 redundant_pattern_match::check_if_let(
860 if_let.if_else.is_some(),
862 needless_match::check_if_let(cx, expr, &if_let);
864 } else if !from_expansion {
865 redundant_pattern_match::check(cx, expr);
869 fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'_>) {
870 self.infallible_destructuring_match_linted |= infallible_destructuring_match::check(cx, local);
873 fn check_pat(&mut self, cx: &LateContext<'tcx>, pat: &'tcx Pat<'_>) {
874 rest_pat_in_fully_bound_struct::check(cx, pat);
877 extract_msrv_attr!(LateContext);
880 /// Checks if there are any arms with a `#[cfg(..)]` attribute.
881 fn contains_cfg_arm(cx: &LateContext<'_>, e: &Expr<'_>, scrutinee: &Expr<'_>, arms: &[Arm<'_>]) -> bool {
882 let Some(scrutinee_span) = walk_span_to_context(scrutinee.span, SyntaxContext::root()) else {
883 // Shouldn't happen, but treat this as though a `cfg` attribute were found
887 let start = scrutinee_span.hi();
888 let mut arm_spans = arms.iter().map(|arm| {
889 let data = arm.span.data();
890 (data.ctxt == SyntaxContext::root()).then(|| (data.lo, data.hi))
892 let end = e.span.hi();
894 // Walk through all the non-code space before each match arm. The space trailing the final arm is
895 // handled after the `try_fold` e.g.
898 // _________^- everything between the scrutinee and arm1
900 //|---^___________^ everything before arm2
901 //| #[cfg(feature = "enabled")]
902 //| arm2 => some_code(),
903 //|---^____________________^ everything before arm3
904 //| // some comment about arm3
905 //| arm3 => some_code(),
906 //|---^____________________^ everything after arm3
907 //| #[cfg(feature = "disabled")]
908 //| arm4 = some_code(),
911 let found = arm_spans.try_fold(start, |start, range| {
912 let Some((end, next_start)) = range else {
913 // Shouldn't happen as macros can't expand to match arms, but treat this as though a `cfg` attribute were
917 let span = SpanData {
920 ctxt: SyntaxContext::root(),
924 (!span_contains_cfg(cx, span)).then(|| next_start).ok_or(())
928 let span = SpanData {
931 ctxt: SyntaxContext::root(),
935 span_contains_cfg(cx, span)
941 /// Checks if the given span contains a `#[cfg(..)]` attribute
942 fn span_contains_cfg(cx: &LateContext<'_>, s: Span) -> bool {
943 let Some(snip) = snippet_opt(cx, s) else {
944 // Assume true. This would require either an invalid span, or one which crosses file boundaries.
947 let mut pos = 0usize;
948 let mut iter = tokenize(&snip).map(|t| {
954 // Search for the token sequence [`#`, `[`, `cfg`]
955 while iter.any(|(t, _)| matches!(t, TokenKind::Pound)) {
956 let mut iter = iter.by_ref().skip_while(|(t, _)| {
959 TokenKind::Whitespace | TokenKind::LineComment { .. } | TokenKind::BlockComment { .. }
962 if matches!(iter.next(), Some((TokenKind::OpenBracket, _)))
963 && matches!(iter.next(), Some((TokenKind::Ident, range)) if &snip[range.clone()] == "cfg")