about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2020-12-03 09:23:48 +0000
committerbors <bors@rust-lang.org>2020-12-03 09:23:48 +0000
commit249b6fee9120efb6d6bb8f559524c478b2ab4b74 (patch)
tree7c8821786ade5fbc492042c8aa70274a3f8cb719
parent4785da6e37ae0a85f71fe200e5a2aa82f4fec85f (diff)
parentc9da8667566a37cd2808892009cd770ea0ae4a79 (diff)
downloadrust-249b6fee9120efb6d6bb8f559524c478b2ab4b74.tar.gz
rust-249b6fee9120efb6d6bb8f559524c478b2ab4b74.zip
Auto merge of #6415 - flip1995:rollup-fz7872l, r=flip1995
Rollup of 4 pull requests

Successful merges:

 - #6308 (add `internal-lints` feature to enable clippys internal lints (off by default))
 - #6395 (switch Version/VersionReq usages to RustcVersion )
 - #6402 (Add Collapsible match lint)
 - #6407 (CONTRIBUTING: update bors queue url from buildbot2.rlo to bors.rlo)

Failed merges:

r? `@ghost`
`@rustbot` modify labels: rollup

changelog: rollup
-rw-r--r--.github/workflows/clippy_bors.yml12
-rw-r--r--CHANGELOG.md1
-rw-r--r--CONTRIBUTING.md9
-rw-r--r--Cargo.toml5
-rw-r--r--clippy_dev/src/lib.rs32
-rw-r--r--clippy_dev/src/update_lints.rs2
-rw-r--r--clippy_lints/Cargo.toml3
-rw-r--r--clippy_lints/src/collapsible_match.rs172
-rw-r--r--clippy_lints/src/default.rs3
-rw-r--r--clippy_lints/src/if_let_some_result.rs3
-rw-r--r--clippy_lints/src/implicit_return.rs3
-rw-r--r--clippy_lints/src/implicit_saturating_sub.rs3
-rw-r--r--clippy_lints/src/large_const_arrays.rs3
-rw-r--r--clippy_lints/src/large_stack_arrays.rs3
-rw-r--r--clippy_lints/src/let_if_seq.rs53
-rw-r--r--clippy_lints/src/lib.rs75
-rw-r--r--clippy_lints/src/loops.rs35
-rw-r--r--clippy_lints/src/manual_non_exhaustive.rs14
-rw-r--r--clippy_lints/src/manual_strip.rs17
-rw-r--r--clippy_lints/src/matches.rs38
-rw-r--r--clippy_lints/src/methods/manual_saturating_arithmetic.rs3
-rw-r--r--clippy_lints/src/methods/mod.rs16
-rw-r--r--clippy_lints/src/needless_bool.rs11
-rw-r--r--clippy_lints/src/question_mark.rs3
-rw-r--r--clippy_lints/src/strings.rs3
-rw-r--r--clippy_lints/src/trait_bounds.rs3
-rw-r--r--clippy_lints/src/transmuting_null.rs3
-rw-r--r--clippy_lints/src/types.rs6
-rw-r--r--clippy_lints/src/utils/diagnostics.rs4
-rw-r--r--clippy_lints/src/utils/higher.rs3
-rw-r--r--clippy_lints/src/utils/hir_utils.rs28
-rw-r--r--clippy_lints/src/utils/mod.rs11
-rw-r--r--clippy_lints/src/utils/paths.rs4
-rw-r--r--clippy_lints/src/utils/visitors.rs55
-rw-r--r--tests/compile-test.rs15
-rw-r--r--tests/dogfood.rs16
-rw-r--r--tests/ui-internal/collapsible_span_lint_calls.fixed (renamed from tests/ui/collapsible_span_lint_calls.fixed)0
-rw-r--r--tests/ui-internal/collapsible_span_lint_calls.rs (renamed from tests/ui/collapsible_span_lint_calls.rs)0
-rw-r--r--tests/ui-internal/collapsible_span_lint_calls.stderr (renamed from tests/ui/collapsible_span_lint_calls.stderr)0
-rw-r--r--tests/ui-internal/custom_ice_message.rs (renamed from tests/ui/custom_ice_message.rs)0
-rw-r--r--tests/ui-internal/custom_ice_message.stderr (renamed from tests/ui/custom_ice_message.stderr)0
-rw-r--r--tests/ui-internal/default_lint.rs (renamed from tests/ui/default_lint.rs)0
-rw-r--r--tests/ui-internal/default_lint.stderr (renamed from tests/ui/default_lint.stderr)0
-rw-r--r--tests/ui-internal/invalid_paths.rs (renamed from tests/ui/invalid_paths.rs)0
-rw-r--r--tests/ui-internal/invalid_paths.stderr (renamed from tests/ui/invalid_paths.stderr)0
-rw-r--r--tests/ui-internal/lint_without_lint_pass.rs (renamed from tests/ui/lint_without_lint_pass.rs)0
-rw-r--r--tests/ui-internal/lint_without_lint_pass.stderr (renamed from tests/ui/lint_without_lint_pass.stderr)0
-rw-r--r--tests/ui-internal/match_type_on_diag_item.rs (renamed from tests/ui/match_type_on_diag_item.rs)0
-rw-r--r--tests/ui-internal/match_type_on_diag_item.stderr (renamed from tests/ui/match_type_on_diag_item.stderr)0
-rw-r--r--tests/ui-internal/outer_expn_data.fixed (renamed from tests/ui/outer_expn_data.fixed)0
-rw-r--r--tests/ui-internal/outer_expn_data.rs (renamed from tests/ui/outer_expn_data.rs)0
-rw-r--r--tests/ui-internal/outer_expn_data.stderr (renamed from tests/ui/outer_expn_data.stderr)0
-rw-r--r--tests/ui/collapsible_match.rs239
-rw-r--r--tests/ui/collapsible_match.stderr179
-rw-r--r--tests/ui/collapsible_match2.rs53
-rw-r--r--tests/ui/collapsible_match2.stderr61
-rw-r--r--tests/ui/min_rust_version_attr.rs38
-rw-r--r--tests/ui/min_rust_version_attr.stderr37
-rw-r--r--tests/ui/min_rust_version_no_patch.rs2
59 files changed, 1041 insertions, 238 deletions
diff --git a/.github/workflows/clippy_bors.yml b/.github/workflows/clippy_bors.yml
index 7509d90c6c2..784463fe0df 100644
--- a/.github/workflows/clippy_bors.yml
+++ b/.github/workflows/clippy_bors.yml
@@ -128,14 +128,14 @@ jobs:
         SYSROOT=$(rustc --print sysroot)
         echo "$SYSROOT/bin" >> $GITHUB_PATH
 
-    - name: Build
-      run: cargo build --features deny-warnings
+    - name: Build with internal lints
+      run: cargo build --features deny-warnings,internal-lints
 
-    - name: Test
-      run: cargo test --features deny-warnings
+    - name: Test with internal lints
+      run: cargo test --features deny-warnings,internal-lints
 
-    - name: Test clippy_lints
-      run: cargo test --features deny-warnings
+    - name: Test clippy_lints with internal lints
+      run: cargo test --features deny-warnings,internal-lints
       working-directory: clippy_lints
 
     - name: Test rustc_tools_util
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e76a781f13b..e65e7cc639f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1770,6 +1770,7 @@ Released 2018-09-13
 [`cmp_owned`]: https://rust-lang.github.io/rust-clippy/master/index.html#cmp_owned
 [`cognitive_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#cognitive_complexity
 [`collapsible_if`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
+[`collapsible_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_match
 [`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
 [`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
 [`copy_iterator`]: https://rust-lang.github.io/rust-clippy/master/index.html#copy_iterator
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a8e2123656e..f8c26e2d456 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,11 +14,16 @@ All contributors are expected to follow the [Rust Code of Conduct].
 
 - [Contributing to Clippy](#contributing-to-clippy)
   - [Getting started](#getting-started)
+    - [High level approach](#high-level-approach)
     - [Finding something to fix/improve](#finding-something-to-fiximprove)
   - [Writing code](#writing-code)
   - [Getting code-completion for rustc internals to work](#getting-code-completion-for-rustc-internals-to-work)
   - [How Clippy works](#how-clippy-works)
   - [Fixing build failures caused by Rust](#fixing-build-failures-caused-by-rust)
+    - [Patching git-subtree to work with big repos](#patching-git-subtree-to-work-with-big-repos)
+    - [Performing the sync](#performing-the-sync)
+    - [Syncing back changes in Clippy to [`rust-lang/rust`]](#syncing-back-changes-in-clippy-to-rust-langrust)
+    - [Defining remotes](#defining-remotes)
   - [Issue and PR triage](#issue-and-pr-triage)
   - [Bors and Homu](#bors-and-homu)
   - [Contributions](#contributions)
@@ -320,8 +325,8 @@ commands [here][homu_instructions].
 [l-crash]: https://github.com/rust-lang/rust-clippy/labels/L-crash
 [l-bug]: https://github.com/rust-lang/rust-clippy/labels/L-bug
 [homu]: https://github.com/rust-lang/homu
-[homu_instructions]: https://buildbot2.rust-lang.org/homu/
-[homu_queue]: https://buildbot2.rust-lang.org/homu/queue/clippy
+[homu_instructions]: https://bors.rust-lang.org/
+[homu_queue]: https://bors.rust-lang.org/queue/clippy
 
 ## Contributions
 
diff --git a/Cargo.toml b/Cargo.toml
index 1ddcd18598d..a765390c603 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,7 +32,7 @@ path = "src/driver.rs"
 clippy_lints = { version = "0.0.212", path = "clippy_lints" }
 # end automatic update
 semver = "0.11"
-rustc_tools_util = { version = "0.2.0", path = "rustc_tools_util"}
+rustc_tools_util = { version = "0.2.0", path = "rustc_tools_util" }
 tempfile = { version = "3.1.0", optional = true }
 
 [dev-dependencies]
@@ -49,8 +49,9 @@ derive-new = "0.5"
 rustc-workspace-hack = "1.0.0"
 
 [build-dependencies]
-rustc_tools_util = { version = "0.2.0", path = "rustc_tools_util"}
+rustc_tools_util = { version = "0.2.0", path = "rustc_tools_util" }
 
 [features]
 deny-warnings = []
 integration = ["tempfile"]
+internal-lints = ["clippy_lints/internal-lints"]
diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs
index 43cb2954b74..f51c45e9eb5 100644
--- a/clippy_dev/src/lib.rs
+++ b/clippy_dev/src/lib.rs
@@ -146,16 +146,30 @@ pub fn gen_deprecated<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String>
 }
 
 #[must_use]
-pub fn gen_register_lint_list<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String> {
-    let pre = "    store.register_lints(&[".to_string();
-    let post = "    ]);".to_string();
-    let mut inner = lints
+pub fn gen_register_lint_list<'a>(
+    internal_lints: impl Iterator<Item = &'a Lint>,
+    usable_lints: impl Iterator<Item = &'a Lint>,
+) -> Vec<String> {
+    let header = "    store.register_lints(&[".to_string();
+    let footer = "    ]);".to_string();
+    let internal_lints = internal_lints
+        .sorted_by_key(|l| format!("        &{}::{},", l.module, l.name.to_uppercase()))
+        .map(|l| {
+            format!(
+                "        #[cfg(feature = \"internal-lints\")]\n        &{}::{},",
+                l.module,
+                l.name.to_uppercase()
+            )
+        });
+    let other_lints = usable_lints
+        .sorted_by_key(|l| format!("        &{}::{},", l.module, l.name.to_uppercase()))
         .map(|l| format!("        &{}::{},", l.module, l.name.to_uppercase()))
-        .sorted()
-        .collect::<Vec<String>>();
-    inner.insert(0, pre);
-    inner.push(post);
-    inner
+        .sorted();
+    let mut lint_list = vec![header];
+    lint_list.extend(internal_lints);
+    lint_list.extend(other_lints);
+    lint_list.push(footer);
+    lint_list
 }
 
 /// Gathers all files in `src/clippy_lints` and gathers all lints inside
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index fcf093f8835..edf6c5f57a4 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -68,7 +68,7 @@ pub fn run(update_mode: UpdateMode) {
         "end register lints",
         false,
         update_mode == UpdateMode::Change,
-        || gen_register_lint_list(usable_lints.iter().chain(internal_lints.iter())),
+        || gen_register_lint_list(internal_lints.iter(), usable_lints.iter()),
     )
     .changed;
 
diff --git a/clippy_lints/Cargo.toml b/clippy_lints/Cargo.toml
index d9471d25197..7697eba650a 100644
--- a/clippy_lints/Cargo.toml
+++ b/clippy_lints/Cargo.toml
@@ -28,6 +28,7 @@ smallvec = { version = "1", features = ["union"] }
 toml = "0.5.3"
 unicode-normalization = "0.1"
 semver = "0.11"
+rustc-semver="1.1.0"
 # NOTE: cargo requires serde feat in its url dep
 # see <https://github.com/rust-lang/rust/pull/63587#issuecomment-522343864>
 url = { version =  "2.1.0", features = ["serde"] }
@@ -36,3 +37,5 @@ syn = { version = "1", features = ["full"] }
 
 [features]
 deny-warnings = []
+# build clippy with internal lints enabled, off by default
+internal-lints = []
diff --git a/clippy_lints/src/collapsible_match.rs b/clippy_lints/src/collapsible_match.rs
new file mode 100644
index 00000000000..a34ba2d00a8
--- /dev/null
+++ b/clippy_lints/src/collapsible_match.rs
@@ -0,0 +1,172 @@
+use crate::utils::visitors::LocalUsedVisitor;
+use crate::utils::{span_lint_and_then, SpanlessEq};
+use if_chain::if_chain;
+use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
+use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, Pat, PatKind, QPath, StmtKind};
+use rustc_lint::{LateContext, LateLintPass};
+use rustc_middle::ty::{DefIdTree, TyCtxt};
+use rustc_session::{declare_lint_pass, declare_tool_lint};
+use rustc_span::{MultiSpan, Span};
+
+declare_clippy_lint! {
+    /// **What it does:** Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
+    /// without adding any branches.
+    ///
+    /// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only
+    /// cases where merging would most likely make the code more readable.
+    ///
+    /// **Why is this bad?** It is unnecessarily verbose and complex.
+    ///
+    /// **Known problems:** None.
+    ///
+    /// **Example:**
+    ///
+    /// ```rust
+    /// fn func(opt: Option<Result<u64, String>>) {
+    ///     let n = match opt {
+    ///         Some(n) => match n {
+    ///             Ok(n) => n,
+    ///             _ => return,
+    ///         }
+    ///         None => return,
+    ///     };
+    /// }
+    /// ```
+    /// Use instead:
+    /// ```rust
+    /// fn func(opt: Option<Result<u64, String>>) {
+    ///     let n = match opt {
+    ///         Some(Ok(n)) => n,
+    ///         _ => return,
+    ///     };
+    /// }
+    /// ```
+    pub COLLAPSIBLE_MATCH,
+    style,
+    "Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
+}
+
+declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
+
+impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
+    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
+        if let ExprKind::Match(_expr, arms, _source) = expr.kind {
+            if let Some(wild_arm) = arms.iter().rfind(|arm| arm_is_wild_like(arm, cx.tcx)) {
+                for arm in arms {
+                    check_arm(arm, wild_arm, cx);
+                }
+            }
+        }
+    }
+}
+
+fn check_arm(arm: &Arm<'_>, wild_outer_arm: &Arm<'_>, cx: &LateContext<'_>) {
+    if_chain! {
+        let expr = strip_singleton_blocks(arm.body);
+        if let ExprKind::Match(expr_in, arms_inner, _) = expr.kind;
+        // the outer arm pattern and the inner match
+        if expr_in.span.ctxt() == arm.pat.span.ctxt();
+        // there must be no more than two arms in the inner match for this lint
+        if arms_inner.len() == 2;
+        // no if guards on the inner match
+        if arms_inner.iter().all(|arm| arm.guard.is_none());
+        // match expression must be a local binding
+        // match <local> { .. }
+        if let ExprKind::Path(QPath::Resolved(None, path)) = expr_in.kind;
+        if let Res::Local(binding_id) = path.res;
+        // one of the branches must be "wild-like"
+        if let Some(wild_inner_arm_idx) = arms_inner.iter().rposition(|arm_inner| arm_is_wild_like(arm_inner, cx.tcx));
+        let (wild_inner_arm, non_wild_inner_arm) =
+            (&arms_inner[wild_inner_arm_idx], &arms_inner[1 - wild_inner_arm_idx]);
+        if !pat_contains_or(non_wild_inner_arm.pat);
+        // the binding must come from the pattern of the containing match arm
+        // ..<local>.. => match <local> { .. }
+        if let Some(binding_span) = find_pat_binding(arm.pat, binding_id);
+        // the "wild-like" branches must be equal
+        if SpanlessEq::new(cx).eq_expr(wild_inner_arm.body, wild_outer_arm.body);
+        // the binding must not be used in the if guard
+        if !matches!(arm.guard, Some(Guard::If(guard)) if LocalUsedVisitor::new(binding_id).check_expr(guard));
+        // ...or anywhere in the inner match
+        if !arms_inner.iter().any(|arm| LocalUsedVisitor::new(binding_id).check_arm(arm));
+        then {
+            span_lint_and_then(
+                cx,
+                COLLAPSIBLE_MATCH,
+                expr.span,
+                "Unnecessary nested match",
+                |diag| {
+                    let mut help_span = MultiSpan::from_spans(vec![binding_span, non_wild_inner_arm.pat.span]);
+                    help_span.push_span_label(binding_span, "Replace this binding".into());
+                    help_span.push_span_label(non_wild_inner_arm.pat.span, "with this pattern".into());
+                    diag.span_help(help_span, "The outer pattern can be modified to include the inner pattern.");
+                },
+            );
+        }
+    }
+}
+
+fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
+    while let ExprKind::Block(block, _) = expr.kind {
+        match (block.stmts, block.expr) {
+            ([stmt], None) => match stmt.kind {
+                StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e,
+                _ => break,
+            },
+            ([], Some(e)) => expr = e,
+            _ => break,
+        }
+    }
+    expr
+}
+
+/// A "wild-like" pattern is wild ("_") or `None`.
+/// For this lint to apply, both the outer and inner match expressions
+/// must have "wild-like" branches that can be combined.
+fn arm_is_wild_like(arm: &Arm<'_>, tcx: TyCtxt<'_>) -> bool {
+    if arm.guard.is_some() {
+        return false;
+    }
+    match arm.pat.kind {
+        PatKind::Binding(..) | PatKind::Wild => true,
+        PatKind::Path(QPath::Resolved(None, path)) if is_none_ctor(path.res, tcx) => true,
+        _ => false,
+    }
+}
+
+fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
+    let mut span = None;
+    pat.walk_short(|p| match &p.kind {
+        // ignore OR patterns
+        PatKind::Or(_) => false,
+        PatKind::Binding(_bm, _, _ident, _) => {
+            let found = p.hir_id == hir_id;
+            if found {
+                span = Some(p.span);
+            }
+            !found
+        },
+        _ => true,
+    });
+    span
+}
+
+fn pat_contains_or(pat: &Pat<'_>) -> bool {
+    let mut result = false;
+    pat.walk(|p| {
+        let is_or = matches!(p.kind, PatKind::Or(_));
+        result |= is_or;
+        !is_or
+    });
+    result
+}
+
+fn is_none_ctor(res: Res, tcx: TyCtxt<'_>) -> bool {
+    if let Some(none_id) = tcx.lang_items().option_none_variant() {
+        if let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = res {
+            if let Some(variant_id) = tcx.parent(id) {
+                return variant_id == none_id;
+            }
+        }
+    }
+    false
+}
diff --git a/clippy_lints/src/default.rs b/clippy_lints/src/default.rs
index 612c5355338..f69f6f1412a 100644
--- a/clippy_lints/src/default.rs
+++ b/clippy_lints/src/default.rs
@@ -280,8 +280,7 @@ fn field_reassigned_by_stmt<'tcx>(this: &Stmt<'tcx>, binding_name: Symbol) -> Op
         // only take assignments to fields where the left-hand side field is a field of
         // the same binding as the previous statement
         if let ExprKind::Field(ref binding, field_ident) = assign_lhs.kind;
-        if let ExprKind::Path(ref qpath) = binding.kind;
-        if let QPath::Resolved(_, path) = qpath;
+        if let ExprKind::Path(QPath::Resolved(_, path)) = binding.kind;
         if let Some(second_binding_name) = path.segments.last();
         if second_binding_name.ident.name == binding_name;
         then {
diff --git a/clippy_lints/src/if_let_some_result.rs b/clippy_lints/src/if_let_some_result.rs
index e0a1f4c5ca4..1194bd7e55e 100644
--- a/clippy_lints/src/if_let_some_result.rs
+++ b/clippy_lints/src/if_let_some_result.rs
@@ -41,8 +41,7 @@ declare_lint_pass!(OkIfLet => [IF_LET_SOME_RESULT]);
 impl<'tcx> LateLintPass<'tcx> for OkIfLet {
     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
         if_chain! { //begin checking variables
-            if let ExprKind::Match(ref op, ref body, source) = expr.kind; //test if expr is a match
-            if let MatchSource::IfLetDesugar { .. } = source; //test if it is an If Let
+            if let ExprKind::Match(ref op, ref body, MatchSource::IfLetDesugar { .. }) = expr.kind; //test if expr is if let
             if let ExprKind::MethodCall(_, ok_span, ref result_types, _) = op.kind; //check is expr.ok() has type Result<T,E>.ok(, _)
             if let PatKind::TupleStruct(QPath::Resolved(_, ref x), ref y, _)  = body[0].pat.kind; //get operation
             if method_chain_args(op, &["ok"]).is_some(); //test to see if using ok() methoduse std::marker::Sized;
diff --git a/clippy_lints/src/implicit_return.rs b/clippy_lints/src/implicit_return.rs
index ed7f3b9293d..03e95c9e27f 100644
--- a/clippy_lints/src/implicit_return.rs
+++ b/clippy_lints/src/implicit_return.rs
@@ -68,8 +68,7 @@ fn expr_match(cx: &LateContext<'_>, expr: &Expr<'_>) {
                 if_chain! {
                     if let StmtKind::Semi(expr, ..) = &stmt.kind;
                     // make sure it's a break, otherwise we want to skip
-                    if let ExprKind::Break(.., break_expr) = &expr.kind;
-                    if let Some(break_expr) = break_expr;
+                    if let ExprKind::Break(.., Some(break_expr)) = &expr.kind;
                     then {
                             lint(cx, expr.span, break_expr.span, LINT_BREAK);
                     }
diff --git a/clippy_lints/src/implicit_saturating_sub.rs b/clippy_lints/src/implicit_saturating_sub.rs
index b57fe8dc426..3a01acd8fdc 100644
--- a/clippy_lints/src/implicit_saturating_sub.rs
+++ b/clippy_lints/src/implicit_saturating_sub.rs
@@ -59,8 +59,7 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub {
             if let Some(target) = subtracts_one(cx, e);
 
             // Extracting out the variable name
-            if let ExprKind::Path(ref assign_path) = target.kind;
-            if let QPath::Resolved(_, ref ares_path) = assign_path;
+            if let ExprKind::Path(QPath::Resolved(_, ref ares_path)) = target.kind;
 
             then {
                 // Handle symmetric conditions in the if statement
diff --git a/clippy_lints/src/large_const_arrays.rs b/clippy_lints/src/large_const_arrays.rs
index 025ff86da39..a76595ed089 100644
--- a/clippy_lints/src/large_const_arrays.rs
+++ b/clippy_lints/src/large_const_arrays.rs
@@ -52,8 +52,7 @@ impl<'tcx> LateLintPass<'tcx> for LargeConstArrays {
             if let ItemKind::Const(hir_ty, _) = &item.kind;
             let ty = hir_ty_to_ty(cx.tcx, hir_ty);
             if let ty::Array(element_type, cst) = ty.kind();
-            if let ConstKind::Value(val) = cst.val;
-            if let ConstValue::Scalar(element_count) = val;
+            if let ConstKind::Value(ConstValue::Scalar(element_count)) = cst.val;
             if let Ok(element_count) = element_count.to_machine_usize(&cx.tcx);
             if let Ok(element_size) = cx.layout_of(element_type).map(|l| l.size.bytes());
             if self.maximum_allowed_size < element_count * element_size;
diff --git a/clippy_lints/src/large_stack_arrays.rs b/clippy_lints/src/large_stack_arrays.rs
index 9fd3780e14e..9a448ab1256 100644
--- a/clippy_lints/src/large_stack_arrays.rs
+++ b/clippy_lints/src/large_stack_arrays.rs
@@ -43,8 +43,7 @@ impl<'tcx> LateLintPass<'tcx> for LargeStackArrays {
         if_chain! {
             if let ExprKind::Repeat(_, _) = expr.kind;
             if let ty::Array(element_type, cst) = cx.typeck_results().expr_ty(expr).kind();
-            if let ConstKind::Value(val) = cst.val;
-            if let ConstValue::Scalar(element_count) = val;
+            if let ConstKind::Value(ConstValue::Scalar(element_count)) = cst.val;
             if let Ok(element_count) = element_count.to_machine_usize(&cx.tcx);
             if let Ok(element_size) = cx.layout_of(element_type).map(|l| l.size.bytes());
             if self.maximum_allowed_size < element_count * element_size;
diff --git a/clippy_lints/src/let_if_seq.rs b/clippy_lints/src/let_if_seq.rs
index 8243b0a29bc..0d2d95324c4 100644
--- a/clippy_lints/src/let_if_seq.rs
+++ b/clippy_lints/src/let_if_seq.rs
@@ -1,12 +1,11 @@
+use crate::utils::visitors::LocalUsedVisitor;
 use crate::utils::{higher, qpath_res, snippet, span_lint_and_then};
 use if_chain::if_chain;
 use rustc_errors::Applicability;
 use rustc_hir as hir;
 use rustc_hir::def::Res;
-use rustc_hir::intravisit;
 use rustc_hir::BindingAnnotation;
 use rustc_lint::{LateContext, LateLintPass};
-use rustc_middle::hir::map::Map;
 use rustc_session::{declare_lint_pass, declare_tool_lint};
 
 declare_clippy_lint! {
@@ -66,10 +65,10 @@ impl<'tcx> LateLintPass<'tcx> for LetIfSeq {
                 if let hir::PatKind::Binding(mode, canonical_id, ident, None) = local.pat.kind;
                 if let hir::StmtKind::Expr(ref if_) = expr.kind;
                 if let Some((ref cond, ref then, ref else_)) = higher::if_block(&if_);
-                if !used_in_expr(cx, canonical_id, cond);
+                if !LocalUsedVisitor::new(canonical_id).check_expr(cond);
                 if let hir::ExprKind::Block(ref then, _) = then.kind;
                 if let Some(value) = check_assign(cx, canonical_id, &*then);
-                if !used_in_expr(cx, canonical_id, value);
+                if !LocalUsedVisitor::new(canonical_id).check_expr(value);
                 then {
                     let span = stmt.span.to(if_.span);
 
@@ -136,32 +135,6 @@ impl<'tcx> LateLintPass<'tcx> for LetIfSeq {
     }
 }
 
-struct UsedVisitor<'a, 'tcx> {
-    cx: &'a LateContext<'tcx>,
-    id: hir::HirId,
-    used: bool,
-}
-
-impl<'a, 'tcx> intravisit::Visitor<'tcx> for UsedVisitor<'a, 'tcx> {
-    type Map = Map<'tcx>;
-
-    fn visit_expr(&mut self, expr: &'tcx hir::Expr<'_>) {
-        if_chain! {
-            if let hir::ExprKind::Path(ref qpath) = expr.kind;
-            if let Res::Local(local_id) = qpath_res(self.cx, qpath, expr.hir_id);
-            if self.id == local_id;
-            then {
-                self.used = true;
-                return;
-            }
-        }
-        intravisit::walk_expr(self, expr);
-    }
-    fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap<Self::Map> {
-        intravisit::NestedVisitorMap::None
-    }
-}
-
 fn check_assign<'tcx>(
     cx: &LateContext<'tcx>,
     decl: hir::HirId,
@@ -176,18 +149,10 @@ fn check_assign<'tcx>(
         if let Res::Local(local_id) = qpath_res(cx, qpath, var.hir_id);
         if decl == local_id;
         then {
-            let mut v = UsedVisitor {
-                cx,
-                id: decl,
-                used: false,
-            };
-
-            for s in block.stmts.iter().take(block.stmts.len()-1) {
-                intravisit::walk_stmt(&mut v, s);
+            let mut v = LocalUsedVisitor::new(decl);
 
-                if v.used {
-                    return None;
-                }
+            if block.stmts.iter().take(block.stmts.len()-1).any(|stmt| v.check_stmt(stmt)) {
+                return None;
             }
 
             return Some(value);
@@ -196,9 +161,3 @@ fn check_assign<'tcx>(
 
     None
 }
-
-fn used_in_expr<'tcx>(cx: &LateContext<'tcx>, id: hir::HirId, expr: &'tcx hir::Expr<'_>) -> bool {
-    let mut v = UsedVisitor { cx, id, used: false };
-    intravisit::walk_expr(&mut v, expr);
-    v.used
-}
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index 6eb5f6a7f48..167e5b6b87f 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -172,6 +172,7 @@ mod cargo_common_metadata;
 mod checked_conversions;
 mod cognitive_complexity;
 mod collapsible_if;
+mod collapsible_match;
 mod comparison_chain;
 mod copies;
 mod copy_iterator;
@@ -498,6 +499,24 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
 
     // begin register lints, do not remove this comment, it’s used in `update_lints`
     store.register_lints(&[
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::CLIPPY_LINTS_INTERNAL,
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::COLLAPSIBLE_SPAN_LINT_CALLS,
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::COMPILER_LINT_FUNCTIONS,
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::DEFAULT_LINT,
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::INVALID_PATHS,
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::LINT_WITHOUT_LINT_PASS,
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::MATCH_TYPE_ON_DIAGNOSTIC_ITEM,
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::OUTER_EXPN_EXPN_DATA,
+        #[cfg(feature = "internal-lints")]
+        &utils::internal_lints::PRODUCE_ICE,
         &approx_const::APPROX_CONSTANT,
         &arithmetic::FLOAT_ARITHMETIC,
         &arithmetic::INTEGER_ARITHMETIC,
@@ -531,6 +550,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         &checked_conversions::CHECKED_CONVERSIONS,
         &cognitive_complexity::COGNITIVE_COMPLEXITY,
         &collapsible_if::COLLAPSIBLE_IF,
+        &collapsible_match::COLLAPSIBLE_MATCH,
         &comparison_chain::COMPARISON_CHAIN,
         &copies::IFS_SAME_COND,
         &copies::IF_SAME_THEN_ELSE,
@@ -904,15 +924,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         &unwrap_in_result::UNWRAP_IN_RESULT,
         &use_self::USE_SELF,
         &useless_conversion::USELESS_CONVERSION,
-        &utils::internal_lints::CLIPPY_LINTS_INTERNAL,
-        &utils::internal_lints::COLLAPSIBLE_SPAN_LINT_CALLS,
-        &utils::internal_lints::COMPILER_LINT_FUNCTIONS,
-        &utils::internal_lints::DEFAULT_LINT,
-        &utils::internal_lints::INVALID_PATHS,
-        &utils::internal_lints::LINT_WITHOUT_LINT_PASS,
-        &utils::internal_lints::MATCH_TYPE_ON_DIAGNOSTIC_ITEM,
-        &utils::internal_lints::OUTER_EXPN_EXPN_DATA,
-        &utils::internal_lints::PRODUCE_ICE,
         &vec::USELESS_VEC,
         &vec_resize_to_zero::VEC_RESIZE_TO_ZERO,
         &verbose_file_reads::VERBOSE_FILE_READS,
@@ -930,14 +941,23 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         &zero_div_zero::ZERO_DIVIDED_BY_ZERO,
     ]);
     // end register lints, do not remove this comment, it’s used in `update_lints`
+
+    // all the internal lints
+    #[cfg(feature = "internal-lints")]
+    {
+        store.register_early_pass(|| box utils::internal_lints::ClippyLintsInternal);
+        store.register_early_pass(|| box utils::internal_lints::ProduceIce);
+        store.register_late_pass(|| box utils::inspector::DeepCodeInspector);
+        store.register_late_pass(|| box utils::internal_lints::CollapsibleCalls);
+        store.register_late_pass(|| box utils::internal_lints::CompilerLintFunctions::new());
+        store.register_late_pass(|| box utils::internal_lints::InvalidPaths);
+        store.register_late_pass(|| box utils::internal_lints::LintWithoutLintPass::default());
+        store.register_late_pass(|| box utils::internal_lints::MatchTypeOnDiagItem);
+        store.register_late_pass(|| box utils::internal_lints::OuterExpnDataPass);
+    }
+    store.register_late_pass(|| box utils::author::Author);
     store.register_late_pass(|| box await_holding_invalid::AwaitHolding);
     store.register_late_pass(|| box serde_api::SerdeAPI);
-    store.register_late_pass(|| box utils::internal_lints::CompilerLintFunctions::new());
-    store.register_late_pass(|| box utils::internal_lints::LintWithoutLintPass::default());
-    store.register_late_pass(|| box utils::internal_lints::OuterExpnDataPass);
-    store.register_late_pass(|| box utils::internal_lints::InvalidPaths);
-    store.register_late_pass(|| box utils::inspector::DeepCodeInspector);
-    store.register_late_pass(|| box utils::author::Author);
     let vec_box_size_threshold = conf.vec_box_size_threshold;
     store.register_late_pass(move || box types::Types::new(vec_box_size_threshold));
     store.register_late_pass(|| box booleans::NonminimalBool);
@@ -960,28 +980,24 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_late_pass(|| box len_zero::LenZero);
     store.register_late_pass(|| box attrs::Attributes);
     store.register_late_pass(|| box blocks_in_if_conditions::BlocksInIfConditions);
+    store.register_late_pass(|| box collapsible_match::CollapsibleMatch);
     store.register_late_pass(|| box unicode::Unicode);
     store.register_late_pass(|| box unit_return_expecting_ord::UnitReturnExpectingOrd);
     store.register_late_pass(|| box strings::StringAdd);
     store.register_late_pass(|| box implicit_return::ImplicitReturn);
     store.register_late_pass(|| box implicit_saturating_sub::ImplicitSaturatingSub);
 
-    let parsed_msrv = conf.msrv.as_ref().and_then(|s| {
+    let msrv = conf.msrv.as_ref().and_then(|s| {
         parse_msrv(s, None, None).or_else(|| {
             sess.err(&format!("error reading Clippy's configuration file. `{}` is not a valid Rust version", s));
             None
         })
     });
 
-    let msrv = parsed_msrv.clone();
-    store.register_late_pass(move || box methods::Methods::new(msrv.clone()));
-    let msrv = parsed_msrv.clone();
-    store.register_late_pass(move || box matches::Matches::new(msrv.clone()));
-    let msrv = parsed_msrv.clone();
-    store.register_early_pass(move || box manual_non_exhaustive::ManualNonExhaustive::new(msrv.clone()));
-    let msrv = parsed_msrv;
-    store.register_late_pass(move || box manual_strip::ManualStrip::new(msrv.clone()));
-
+    store.register_late_pass(move || box methods::Methods::new(msrv));
+    store.register_late_pass(move || box matches::Matches::new(msrv));
+    store.register_early_pass(move || box manual_non_exhaustive::ManualNonExhaustive::new(msrv));
+    store.register_late_pass(move || box manual_strip::ManualStrip::new(msrv));
     store.register_late_pass(|| box map_clone::MapClone);
     store.register_late_pass(|| box map_err_ignore::MapErrIgnore);
     store.register_late_pass(|| box shadow::Shadow);
@@ -1122,7 +1138,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_early_pass(|| box literal_representation::LiteralDigitGrouping);
     let literal_representation_threshold = conf.literal_representation_threshold;
     store.register_early_pass(move || box literal_representation::DecimalLiteralRepresentation::new(literal_representation_threshold));
-    store.register_early_pass(|| box utils::internal_lints::ClippyLintsInternal);
     let enum_variant_name_threshold = conf.enum_variant_name_threshold;
     store.register_early_pass(move || box enum_variants::EnumVariantNames::new(enum_variant_name_threshold));
     store.register_early_pass(|| box tabs_in_doc_comments::TabsInDocComments);
@@ -1136,7 +1151,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_late_pass(move || box large_const_arrays::LargeConstArrays::new(array_size_threshold));
     store.register_late_pass(|| box floating_point_arithmetic::FloatingPointArithmetic);
     store.register_early_pass(|| box as_conversions::AsConversions);
-    store.register_early_pass(|| box utils::internal_lints::ProduceIce);
     store.register_late_pass(|| box let_underscore::LetUnderscore);
     store.register_late_pass(|| box atomic_ordering::AtomicOrdering);
     store.register_early_pass(|| box single_component_path_imports::SingleComponentPathImports);
@@ -1152,7 +1166,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_late_pass(|| box dereference::Dereferencing);
     store.register_late_pass(|| box option_if_let_else::OptionIfLetElse);
     store.register_late_pass(|| box future_not_send::FutureNotSend);
-    store.register_late_pass(|| box utils::internal_lints::CollapsibleCalls);
     store.register_late_pass(|| box if_let_mutex::IfLetMutex);
     store.register_late_pass(|| box mut_mutex_lock::MutMutexLock);
     store.register_late_pass(|| box match_on_vec_items::MatchOnVecItems);
@@ -1160,7 +1173,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_early_pass(|| box redundant_field_names::RedundantFieldNames);
     store.register_late_pass(|| box vec_resize_to_zero::VecResizeToZero);
     store.register_late_pass(|| box panic_in_result_fn::PanicInResultFn);
-
     let single_char_binding_names_threshold = conf.single_char_binding_names_threshold;
     store.register_early_pass(move || box non_expressive_names::NonExpressiveNames {
         single_char_binding_names_threshold,
@@ -1177,7 +1189,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_late_pass(|| box manual_ok_or::ManualOkOr);
     store.register_late_pass(|| box float_equality_without_abs::FloatEqualityWithoutAbs);
     store.register_late_pass(|| box async_yields_async::AsyncYieldsAsync);
-    store.register_late_pass(|| box utils::internal_lints::MatchTypeOnDiagItem);
     let disallowed_methods = conf.disallowed_methods.iter().cloned().collect::<FxHashSet<_>>();
     store.register_late_pass(move || box disallowed_method::DisallowedMethod::new(&disallowed_methods));
     store.register_early_pass(|| box asm_syntax::InlineAsmX86AttSyntax);
@@ -1186,7 +1197,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_late_pass(|| box strings::StrToString);
     store.register_late_pass(|| box strings::StringToString);
 
-
     store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
         LintId::of(&arithmetic::FLOAT_ARITHMETIC),
         LintId::of(&arithmetic::INTEGER_ARITHMETIC),
@@ -1318,6 +1328,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         LintId::of(&wildcard_imports::WILDCARD_IMPORTS),
     ]);
 
+    #[cfg(feature = "internal-lints")]
     store.register_group(true, "clippy::internal", Some("clippy_internal"), vec![
         LintId::of(&utils::internal_lints::CLIPPY_LINTS_INTERNAL),
         LintId::of(&utils::internal_lints::COLLAPSIBLE_SPAN_LINT_CALLS),
@@ -1351,6 +1362,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         LintId::of(&booleans::NONMINIMAL_BOOL),
         LintId::of(&bytecount::NAIVE_BYTECOUNT),
         LintId::of(&collapsible_if::COLLAPSIBLE_IF),
+        LintId::of(&collapsible_match::COLLAPSIBLE_MATCH),
         LintId::of(&comparison_chain::COMPARISON_CHAIN),
         LintId::of(&copies::IFS_SAME_COND),
         LintId::of(&copies::IF_SAME_THEN_ELSE),
@@ -1617,6 +1629,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         LintId::of(&blacklisted_name::BLACKLISTED_NAME),
         LintId::of(&blocks_in_if_conditions::BLOCKS_IN_IF_CONDITIONS),
         LintId::of(&collapsible_if::COLLAPSIBLE_IF),
+        LintId::of(&collapsible_match::COLLAPSIBLE_MATCH),
         LintId::of(&comparison_chain::COMPARISON_CHAIN),
         LintId::of(&default::FIELD_REASSIGN_WITH_DEFAULT),
         LintId::of(&doc::MISSING_SAFETY_DOC),
diff --git a/clippy_lints/src/loops.rs b/clippy_lints/src/loops.rs
index 143cbea5537..400148ab81d 100644
--- a/clippy_lints/src/loops.rs
+++ b/clippy_lints/src/loops.rs
@@ -2,6 +2,7 @@ use crate::consts::constant;
 use crate::utils::paths;
 use crate::utils::sugg::Sugg;
 use crate::utils::usage::{is_unused, mutated_variables};
+use crate::utils::visitors::LocalUsedVisitor;
 use crate::utils::{
     contains_name, get_enclosing_block, get_parent_expr, get_trait_def_id, has_iter_method, higher, implements_trait,
     indent_of, is_in_panic_handler, is_integer_const, is_no_std_crate, is_refutable, is_type_diagnostic_item,
@@ -1919,8 +1920,7 @@ fn check_for_single_element_loop<'tcx>(
     if_chain! {
         if let ExprKind::AddrOf(BorrowKind::Ref, _, ref arg_expr) = arg.kind;
         if let PatKind::Binding(.., target, _) = pat.kind;
-        if let ExprKind::Array(ref arg_expr_list) = arg_expr.kind;
-        if let [arg_expression] = arg_expr_list;
+        if let ExprKind::Array([arg_expression]) = arg_expr.kind;
         if let ExprKind::Path(ref list_item) = arg_expression.kind;
         if let Some(list_item_name) = single_segment_path(list_item).map(|ps| ps.ident.name);
         if let ExprKind::Block(ref block, _) = body.kind;
@@ -2025,8 +2025,7 @@ fn check_for_mutability(cx: &LateContext<'_>, bound: &Expr<'_>) -> Option<HirId>
                 let node_str = cx.tcx.hir().get(hir_id);
                 if_chain! {
                     if let Node::Binding(pat) = node_str;
-                    if let PatKind::Binding(bind_ann, ..) = pat.kind;
-                    if let BindingAnnotation::Mutable = bind_ann;
+                    if let PatKind::Binding(BindingAnnotation::Mutable, ..) = pat.kind;
                     then {
                         return Some(hir_id);
                     }
@@ -2071,28 +2070,6 @@ fn pat_is_wild<'tcx>(pat: &'tcx PatKind<'_>, body: &'tcx Expr<'_>) -> bool {
     }
 }
 
-struct LocalUsedVisitor<'a, 'tcx> {
-    cx: &'a LateContext<'tcx>,
-    local: HirId,
-    used: bool,
-}
-
-impl<'a, 'tcx> Visitor<'tcx> for LocalUsedVisitor<'a, 'tcx> {
-    type Map = Map<'tcx>;
-
-    fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
-        if same_var(self.cx, expr, self.local) {
-            self.used = true;
-        } else {
-            walk_expr(self, expr);
-        }
-    }
-
-    fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
-        NestedVisitorMap::None
-    }
-}
-
 struct VarVisitor<'a, 'tcx> {
     /// context reference
     cx: &'a LateContext<'tcx>,
@@ -2126,11 +2103,7 @@ impl<'a, 'tcx> VarVisitor<'a, 'tcx> {
             then {
                 let index_used_directly = same_var(self.cx, idx, self.var);
                 let indexed_indirectly = {
-                    let mut used_visitor = LocalUsedVisitor {
-                        cx: self.cx,
-                        local: self.var,
-                        used: false,
-                    };
+                    let mut used_visitor = LocalUsedVisitor::new(self.var);
                     walk_expr(&mut used_visitor, idx);
                     used_visitor.used
                 };
diff --git a/clippy_lints/src/manual_non_exhaustive.rs b/clippy_lints/src/manual_non_exhaustive.rs
index 703e6feeca5..91849e74887 100644
--- a/clippy_lints/src/manual_non_exhaustive.rs
+++ b/clippy_lints/src/manual_non_exhaustive.rs
@@ -4,17 +4,11 @@ use rustc_ast::ast::{Attribute, Item, ItemKind, StructField, Variant, VariantDat
 use rustc_attr as attr;
 use rustc_errors::Applicability;
 use rustc_lint::{EarlyContext, EarlyLintPass};
+use rustc_semver::RustcVersion;
 use rustc_session::{declare_tool_lint, impl_lint_pass};
 use rustc_span::{sym, Span};
-use semver::{Version, VersionReq};
 
-const MANUAL_NON_EXHAUSTIVE_MSRV: Version = Version {
-    major: 1,
-    minor: 40,
-    patch: 0,
-    pre: Vec::new(),
-    build: Vec::new(),
-};
+const MANUAL_NON_EXHAUSTIVE_MSRV: RustcVersion = RustcVersion::new(1, 40, 0);
 
 declare_clippy_lint! {
     /// **What it does:** Checks for manual implementations of the non-exhaustive pattern.
@@ -66,12 +60,12 @@ declare_clippy_lint! {
 
 #[derive(Clone)]
 pub struct ManualNonExhaustive {
-    msrv: Option<VersionReq>,
+    msrv: Option<RustcVersion>,
 }
 
 impl ManualNonExhaustive {
     #[must_use]
-    pub fn new(msrv: Option<VersionReq>) -> Self {
+    pub fn new(msrv: Option<RustcVersion>) -> Self {
         Self { msrv }
     }
 }
diff --git a/clippy_lints/src/manual_strip.rs b/clippy_lints/src/manual_strip.rs
index e17e3adb94f..3c4368a3545 100644
--- a/clippy_lints/src/manual_strip.rs
+++ b/clippy_lints/src/manual_strip.rs
@@ -13,18 +13,12 @@ use rustc_hir::{BorrowKind, Expr, ExprKind};
 use rustc_lint::{LateContext, LateLintPass, LintContext};
 use rustc_middle::hir::map::Map;
 use rustc_middle::ty;
+use rustc_semver::RustcVersion;
 use rustc_session::{declare_tool_lint, impl_lint_pass};
 use rustc_span::source_map::Spanned;
 use rustc_span::Span;
-use semver::{Version, VersionReq};
 
-const MANUAL_STRIP_MSRV: Version = Version {
-    major: 1,
-    minor: 45,
-    patch: 0,
-    pre: Vec::new(),
-    build: Vec::new(),
-};
+const MANUAL_STRIP_MSRV: RustcVersion = RustcVersion::new(1, 45, 0);
 
 declare_clippy_lint! {
     /// **What it does:**
@@ -61,12 +55,12 @@ declare_clippy_lint! {
 }
 
 pub struct ManualStrip {
-    msrv: Option<VersionReq>,
+    msrv: Option<RustcVersion>,
 }
 
 impl ManualStrip {
     #[must_use]
-    pub fn new(msrv: Option<VersionReq>) -> Self {
+    pub fn new(msrv: Option<RustcVersion>) -> Self {
         Self { msrv }
     }
 }
@@ -225,8 +219,7 @@ fn find_stripping<'tcx>(
                 if is_ref_str(self.cx, ex);
                 let unref = peel_ref(ex);
                 if let ExprKind::Index(indexed, index) = &unref.kind;
-                if let Some(range) = higher::range(index);
-                if let higher::Range { start, end, .. } = range;
+                if let Some(higher::Range { start, end, .. }) = higher::range(index);
                 if let ExprKind::Path(path) = &indexed.kind;
                 if qpath_res(self.cx, path, ex.hir_id) == self.target;
                 then {
diff --git a/clippy_lints/src/matches.rs b/clippy_lints/src/matches.rs
index 7d64fa6c262..274d20cfa80 100644
--- a/clippy_lints/src/matches.rs
+++ b/clippy_lints/src/matches.rs
@@ -20,10 +20,10 @@ use rustc_hir::{
 use rustc_lint::{LateContext, LateLintPass, LintContext};
 use rustc_middle::lint::in_external_macro;
 use rustc_middle::ty::{self, Ty, TyS};
+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};
-use semver::{Version, VersionReq};
 use std::cmp::Ordering;
 use std::collections::hash_map::Entry;
 use std::collections::Bound;
@@ -535,13 +535,13 @@ declare_clippy_lint! {
 
 #[derive(Default)]
 pub struct Matches {
-    msrv: Option<VersionReq>,
+    msrv: Option<RustcVersion>,
     infallible_destructuring_match_linted: bool,
 }
 
 impl Matches {
     #[must_use]
-    pub fn new(msrv: Option<VersionReq>) -> Self {
+    pub fn new(msrv: Option<RustcVersion>) -> Self {
         Self {
             msrv,
             ..Matches::default()
@@ -568,13 +568,7 @@ impl_lint_pass!(Matches => [
     MATCH_SAME_ARMS,
 ]);
 
-const MATCH_LIKE_MATCHES_MACRO_MSRV: Version = Version {
-    major: 1,
-    minor: 42,
-    patch: 0,
-    pre: Vec::new(),
-    build: Vec::new(),
-};
+const MATCH_LIKE_MATCHES_MACRO_MSRV: RustcVersion = RustcVersion::new(1, 42, 0);
 
 impl<'tcx> LateLintPass<'tcx> for Matches {
     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
@@ -652,8 +646,7 @@ impl<'tcx> LateLintPass<'tcx> for Matches {
         if_chain! {
             if !in_external_macro(cx.sess(), pat.span);
             if !in_macro(pat.span);
-            if let PatKind::Struct(ref qpath, fields, true) = pat.kind;
-            if let QPath::Resolved(_, ref path) = qpath;
+            if let PatKind::Struct(QPath::Resolved(_, ref 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();
@@ -962,16 +955,14 @@ fn check_wild_enum_match(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>])
                 if let QPath::Resolved(_, p) = path {
                     missing_variants.retain(|e| e.ctor_def_id != Some(p.res.def_id()));
                 }
-            } else if let PatKind::TupleStruct(ref path, ref patterns, ..) = arm.pat.kind {
-                if let QPath::Resolved(_, p) = path {
-                    // Some simple checks for exhaustive patterns.
-                    // There is a room for improvements to detect more cases,
-                    // but it can be more expensive to do so.
-                    let is_pattern_exhaustive =
-                        |pat: &&Pat<'_>| matches!(pat.kind, PatKind::Wild | PatKind::Binding(.., None));
-                    if patterns.iter().all(is_pattern_exhaustive) {
-                        missing_variants.retain(|e| e.ctor_def_id != Some(p.res.def_id()));
-                    }
+            } else if let PatKind::TupleStruct(QPath::Resolved(_, p), ref patterns, ..) = arm.pat.kind {
+                // Some simple checks for exhaustive patterns.
+                // There is a room for improvements to detect more cases,
+                // but it can be more expensive to do so.
+                let is_pattern_exhaustive =
+                    |pat: &&Pat<'_>| matches!(pat.kind, PatKind::Wild | PatKind::Binding(.., None));
+                if patterns.iter().all(is_pattern_exhaustive) {
+                    missing_variants.retain(|e| e.ctor_def_id != Some(p.res.def_id()));
                 }
             }
         }
@@ -1446,8 +1437,7 @@ fn is_ref_some_arm(arm: &Arm<'_>) -> Option<BindingAnnotation> {
         if let ExprKind::Call(ref e, ref args) = remove_blocks(&arm.body).kind;
         if let ExprKind::Path(ref some_path) = e.kind;
         if match_qpath(some_path, &paths::OPTION_SOME) && args.len() == 1;
-        if let ExprKind::Path(ref qpath) = args[0].kind;
-        if let &QPath::Resolved(_, ref path2) = qpath;
+        if let ExprKind::Path(QPath::Resolved(_, ref path2)) = args[0].kind;
         if path2.segments.len() == 1 && ident.name == path2.segments[0].ident.name;
         then {
             return Some(rb)
diff --git a/clippy_lints/src/methods/manual_saturating_arithmetic.rs b/clippy_lints/src/methods/manual_saturating_arithmetic.rs
index 40a62575861..44c974b9d98 100644
--- a/clippy_lints/src/methods/manual_saturating_arithmetic.rs
+++ b/clippy_lints/src/methods/manual_saturating_arithmetic.rs
@@ -90,8 +90,7 @@ fn is_min_or_max<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>) -> Option<M
     if_chain! {
         if let hir::ExprKind::Call(func, args) = &expr.kind;
         if args.is_empty();
-        if let hir::ExprKind::Path(path) = &func.kind;
-        if let hir::QPath::TypeRelative(_, segment) = path;
+        if let hir::ExprKind::Path(hir::QPath::TypeRelative(_, segment)) = &func.kind;
         then {
             match &*segment.ident.as_str() {
                 "max_value" => return Some(MinMax::Max),
diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs
index 1476408e0fb..8002c27a5e9 100644
--- a/clippy_lints/src/methods/mod.rs
+++ b/clippy_lints/src/methods/mod.rs
@@ -18,6 +18,7 @@ use rustc_hir::{TraitItem, TraitItemKind};
 use rustc_lint::{LateContext, LateLintPass, Lint, LintContext};
 use rustc_middle::lint::in_external_macro;
 use rustc_middle::ty::{self, TraitRef, Ty, TyS};
+use rustc_semver::RustcVersion;
 use rustc_session::{declare_tool_lint, impl_lint_pass};
 use rustc_span::source_map::Span;
 use rustc_span::symbol::{sym, SymbolStr};
@@ -33,7 +34,6 @@ use crate::utils::{
     snippet_with_macro_callsite, span_lint, span_lint_and_help, span_lint_and_sugg, span_lint_and_then, sugg,
     walk_ptrs_ty_depth, SpanlessEq,
 };
-use semver::{Version, VersionReq};
 
 declare_clippy_lint! {
     /// **What it does:** Checks for `.unwrap()` calls on `Option`s and on `Result`s.
@@ -1405,12 +1405,12 @@ declare_clippy_lint! {
 }
 
 pub struct Methods {
-    msrv: Option<VersionReq>,
+    msrv: Option<RustcVersion>,
 }
 
 impl Methods {
     #[must_use]
-    pub fn new(msrv: Option<VersionReq>) -> Self {
+    pub fn new(msrv: Option<RustcVersion>) -> Self {
         Self { msrv }
     }
 }
@@ -3470,13 +3470,7 @@ fn lint_suspicious_map(cx: &LateContext<'_>, expr: &hir::Expr<'_>) {
     );
 }
 
-const OPTION_AS_REF_DEREF_MSRV: Version = Version {
-    major: 1,
-    minor: 40,
-    patch: 0,
-    pre: Vec::new(),
-    build: Vec::new(),
-};
+const OPTION_AS_REF_DEREF_MSRV: RustcVersion = RustcVersion::new(1, 40, 0);
 
 /// lint use of `_.as_ref().map(Deref::deref)` for `Option`s
 fn lint_option_as_ref_deref<'tcx>(
@@ -3485,7 +3479,7 @@ fn lint_option_as_ref_deref<'tcx>(
     as_ref_args: &[hir::Expr<'_>],
     map_args: &[hir::Expr<'_>],
     is_mut: bool,
-    msrv: Option<&VersionReq>,
+    msrv: Option<&RustcVersion>,
 ) {
     if !meets_msrv(msrv, &OPTION_AS_REF_DEREF_MSRV) {
         return;
diff --git a/clippy_lints/src/needless_bool.rs b/clippy_lints/src/needless_bool.rs
index a799a644e97..42f97b2ac49 100644
--- a/clippy_lints/src/needless_bool.rs
+++ b/clippy_lints/src/needless_bool.rs
@@ -6,7 +6,6 @@ use crate::utils::sugg::Sugg;
 use crate::utils::{
     higher, is_expn_of, parent_node_is_if_expr, snippet_with_applicability, span_lint, span_lint_and_sugg,
 };
-use if_chain::if_chain;
 use rustc_ast::ast::LitKind;
 use rustc_errors::Applicability;
 use rustc_hir::{BinOpKind, Block, Expr, ExprKind, StmtKind, UnOp};
@@ -198,13 +197,9 @@ struct ExpressionInfoWithSpan {
 }
 
 fn is_unary_not(e: &Expr<'_>) -> (bool, Span) {
-    if_chain! {
-        if let ExprKind::Unary(unop, operand) = e.kind;
-        if let UnOp::UnNot = unop;
-        then {
-            return (true, operand.span);
-        }
-    };
+    if let ExprKind::Unary(UnOp::UnNot, operand) = e.kind {
+        return (true, operand.span);
+    }
     (false, e.span)
 }
 
diff --git a/clippy_lints/src/question_mark.rs b/clippy_lints/src/question_mark.rs
index d9b280b7a85..b91233ac582 100644
--- a/clippy_lints/src/question_mark.rs
+++ b/clippy_lints/src/question_mark.rs
@@ -176,8 +176,7 @@ impl QuestionMark {
             if block.stmts.len() == 1;
             if let Some(expr) = block.stmts.iter().last();
             if let StmtKind::Semi(ref expr) = expr.kind;
-            if let ExprKind::Ret(ret_expr) = expr.kind;
-            if let Some(ret_expr) = ret_expr;
+            if let ExprKind::Ret(Some(ret_expr)) = expr.kind;
 
             then {
                 return Some(ret_expr);
diff --git a/clippy_lints/src/strings.rs b/clippy_lints/src/strings.rs
index 42c45be3b45..77e79073378 100644
--- a/clippy_lints/src/strings.rs
+++ b/clippy_lints/src/strings.rs
@@ -222,8 +222,7 @@ impl<'tcx> LateLintPass<'tcx> for StringLitAsBytes {
             if method_names[0] == sym!(as_bytes);
 
             // Check for slicer
-            if let ExprKind::Struct(ref path, _, _) = right.kind;
-            if let QPath::LangItem(LangItem::Range, _) = path;
+            if let ExprKind::Struct(QPath::LangItem(LangItem::Range, _), _, _) = right.kind;
 
             then {
                 let mut applicability = Applicability::MachineApplicable;
diff --git a/clippy_lints/src/trait_bounds.rs b/clippy_lints/src/trait_bounds.rs
index d4acf8df46d..daff5f81e8c 100644
--- a/clippy_lints/src/trait_bounds.rs
+++ b/clippy_lints/src/trait_bounds.rs
@@ -168,8 +168,7 @@ fn check_trait_bound_duplication(cx: &LateContext<'_>, gen: &'_ Generics<'_>) {
         if_chain! {
             if let WherePredicate::BoundPredicate(ref bound_predicate) = predicate;
             if !in_macro(bound_predicate.span);
-            if let TyKind::Path(ref path) = bound_predicate.bounded_ty.kind;
-            if let QPath::Resolved(_, Path { ref segments, .. }) = path;
+            if let TyKind::Path(QPath::Resolved(_, Path { ref segments, .. })) = bound_predicate.bounded_ty.kind;
             if let Some(segment) = segments.first();
             if let Some(trait_resolutions_direct) = map.get(&segment.ident);
             then {
diff --git a/clippy_lints/src/transmuting_null.rs b/clippy_lints/src/transmuting_null.rs
index d60306336c6..6b171a0fa1a 100644
--- a/clippy_lints/src/transmuting_null.rs
+++ b/clippy_lints/src/transmuting_null.rs
@@ -48,8 +48,7 @@ impl<'tcx> LateLintPass<'tcx> for TransmutingNull {
                 if_chain! {
                     if let ExprKind::Path(ref _qpath) = args[0].kind;
                     let x = const_eval_context.expr(&args[0]);
-                    if let Some(constant) = x;
-                    if let Constant::RawPtr(0) = constant;
+                    if let Some(Constant::RawPtr(0)) = x;
                     then {
                         span_lint(cx, TRANSMUTING_NULL, expr.span, LINT_MSG)
                     }
diff --git a/clippy_lints/src/types.rs b/clippy_lints/src/types.rs
index c8bdc5a71e6..74ba53e6a9a 100644
--- a/clippy_lints/src/types.rs
+++ b/clippy_lints/src/types.rs
@@ -738,8 +738,7 @@ fn is_any_trait(t: &hir::Ty<'_>) -> bool {
 fn get_bounds_if_impl_trait<'tcx>(cx: &LateContext<'tcx>, qpath: &QPath<'_>, id: HirId) -> Option<GenericBounds<'tcx>> {
     if_chain! {
         if let Some(did) = qpath_res(cx, qpath, id).opt_def_id();
-        if let Some(node) = cx.tcx.hir().get_if_local(did);
-        if let Node::GenericParam(generic_param) = node;
+        if let Some(Node::GenericParam(generic_param)) = cx.tcx.hir().get_if_local(did);
         if let GenericParamKind::Type { synthetic, .. } = generic_param.kind;
         if synthetic == Some(SyntheticTyParamKind::ImplTrait);
         then {
@@ -1470,8 +1469,7 @@ fn check_loss_of_sign(cx: &LateContext<'_>, expr: &Expr<'_>, op: &Expr<'_>, cast
     // don't lint for positive constants
     let const_val = constant(cx, &cx.typeck_results(), op);
     if_chain! {
-        if let Some((const_val, _)) = const_val;
-        if let Constant::Int(n) = const_val;
+        if let Some((Constant::Int(n), _)) = const_val;
         if let ty::Int(ity) = *cast_from.kind();
         if sext(cx.tcx, n, ity) >= 0;
         then {
diff --git a/clippy_lints/src/utils/diagnostics.rs b/clippy_lints/src/utils/diagnostics.rs
index 0a58231558e..a7a6b5855b7 100644
--- a/clippy_lints/src/utils/diagnostics.rs
+++ b/clippy_lints/src/utils/diagnostics.rs
@@ -186,7 +186,9 @@ pub fn span_lint_hir_and_then(
 ///     |
 ///     = note: `-D fold-any` implied by `-D warnings`
 /// ```
-#[allow(clippy::collapsible_span_lint_calls)]
+
+#[allow(clippy::unknown_clippy_lints)]
+#[cfg_attr(feature = "internal-lints", allow(clippy::collapsible_span_lint_calls))]
 pub fn span_lint_and_sugg<'a, T: LintContext>(
     cx: &'a T,
     lint: &'static Lint,
diff --git a/clippy_lints/src/utils/higher.rs b/clippy_lints/src/utils/higher.rs
index 6d7c5058b4f..01ffac5b559 100644
--- a/clippy_lints/src/utils/higher.rs
+++ b/clippy_lints/src/utils/higher.rs
@@ -162,8 +162,7 @@ pub fn while_loop<'tcx>(expr: &'tcx hir::Expr<'tcx>) -> Option<(&'tcx hir::Expr<
         if let hir::Block { expr: Some(expr), .. } = &**block;
         if let hir::ExprKind::Match(cond, arms, hir::MatchSource::WhileDesugar) = &expr.kind;
         if let hir::ExprKind::DropTemps(cond) = &cond.kind;
-        if let [arm, ..] = &arms[..];
-        if let hir::Arm { body, .. } = arm;
+        if let [hir::Arm { body, .. }, ..] = &arms[..];
         then {
             return Some((cond, body));
         }
diff --git a/clippy_lints/src/utils/hir_utils.rs b/clippy_lints/src/utils/hir_utils.rs
index e4ad105c351..d847d22275e 100644
--- a/clippy_lints/src/utils/hir_utils.rs
+++ b/clippy_lints/src/utils/hir_utils.rs
@@ -81,7 +81,7 @@ impl<'a, 'tcx> SpanlessEq<'a, 'tcx> {
             }
         }
 
-        match (&left.kind, &right.kind) {
+        match (&reduce_exprkind(&left.kind), &reduce_exprkind(&right.kind)) {
             (&ExprKind::AddrOf(lb, l_mut, ref le), &ExprKind::AddrOf(rb, r_mut, ref re)) => {
                 lb == rb && l_mut == r_mut && self.eq_expr(le, re)
             },
@@ -306,6 +306,32 @@ impl<'a, 'tcx> SpanlessEq<'a, 'tcx> {
     }
 }
 
+/// Some simple reductions like `{ return }` => `return`
+fn reduce_exprkind<'hir>(kind: &'hir ExprKind<'hir>) -> &ExprKind<'hir> {
+    if let ExprKind::Block(block, _) = kind {
+        match (block.stmts, block.expr) {
+            // `{}` => `()`
+            ([], None) => &ExprKind::Tup(&[]),
+            ([], Some(expr)) => match expr.kind {
+                // `{ return .. }` => `return ..`
+                ExprKind::Ret(..) => &expr.kind,
+                _ => kind,
+            },
+            ([stmt], None) => match stmt.kind {
+                StmtKind::Expr(expr) | StmtKind::Semi(expr) => match expr.kind {
+                    // `{ return ..; }` => `return ..`
+                    ExprKind::Ret(..) => &expr.kind,
+                    _ => kind,
+                },
+                _ => kind,
+            },
+            _ => kind,
+        }
+    } else {
+        kind
+    }
+}
+
 fn swap_binop<'a>(
     binop: BinOpKind,
     lhs: &'a Expr<'a>,
diff --git a/clippy_lints/src/utils/mod.rs b/clippy_lints/src/utils/mod.rs
index 850abc3bae7..3a6b64c90e8 100644
--- a/clippy_lints/src/utils/mod.rs
+++ b/clippy_lints/src/utils/mod.rs
@@ -14,6 +14,7 @@ pub mod eager_or_lazy;
 pub mod higher;
 mod hir_utils;
 pub mod inspector;
+#[cfg(feature = "internal-lints")]
 pub mod internal_lints;
 pub mod numeric_literal;
 pub mod paths;
@@ -51,6 +52,7 @@ use rustc_lint::{LateContext, Level, Lint, LintContext};
 use rustc_middle::hir::map::Map;
 use rustc_middle::ty::subst::{GenericArg, GenericArgKind};
 use rustc_middle::ty::{self, layout::IntegerExt, Ty, TyCtxt, TypeFoldable};
+use rustc_semver::RustcVersion;
 use rustc_session::Session;
 use rustc_span::hygiene::{ExpnKind, MacroKind};
 use rustc_span::source_map::original_sp;
@@ -59,13 +61,12 @@ use rustc_span::symbol::{self, kw, Symbol};
 use rustc_span::{BytePos, Pos, Span, DUMMY_SP};
 use rustc_target::abi::Integer;
 use rustc_trait_selection::traits::query::normalize::AtExt;
-use semver::{Version, VersionReq};
 use smallvec::SmallVec;
 
 use crate::consts::{constant, Constant};
 
-pub fn parse_msrv(msrv: &str, sess: Option<&Session>, span: Option<Span>) -> Option<VersionReq> {
-    if let Ok(version) = VersionReq::parse(msrv) {
+pub fn parse_msrv(msrv: &str, sess: Option<&Session>, span: Option<Span>) -> Option<RustcVersion> {
+    if let Ok(version) = RustcVersion::parse(msrv) {
         return Some(version);
     } else if let Some(sess) = sess {
         if let Some(span) = span {
@@ -75,8 +76,8 @@ pub fn parse_msrv(msrv: &str, sess: Option<&Session>, span: Option<Span>) -> Opt
     None
 }
 
-pub fn meets_msrv(msrv: Option<&VersionReq>, lint_msrv: &Version) -> bool {
-    msrv.map_or(true, |msrv| !msrv.matches(lint_msrv))
+pub fn meets_msrv(msrv: Option<&RustcVersion>, lint_msrv: &RustcVersion) -> bool {
+    msrv.map_or(true, |msrv| msrv.meets(*lint_msrv))
 }
 
 macro_rules! extract_msrv_attr {
diff --git a/clippy_lints/src/utils/paths.rs b/clippy_lints/src/utils/paths.rs
index 61aeabb7ba7..16e6a016c9e 100644
--- a/clippy_lints/src/utils/paths.rs
+++ b/clippy_lints/src/utils/paths.rs
@@ -31,6 +31,7 @@ pub const DISPLAY_TRAIT: [&str; 3] = ["core", "fmt", "Display"];
 pub const DOUBLE_ENDED_ITERATOR: [&str; 4] = ["core", "iter", "traits", "DoubleEndedIterator"];
 pub const DROP: [&str; 3] = ["core", "mem", "drop"];
 pub const DURATION: [&str; 3] = ["core", "time", "Duration"];
+#[cfg(feature = "internal-lints")]
 pub const EARLY_CONTEXT: [&str; 2] = ["rustc_lint", "EarlyContext"];
 pub const EXIT: [&str; 3] = ["std", "process", "exit"];
 pub const F32_EPSILON: [&str; 4] = ["core", "f32", "<impl f32>", "EPSILON"];
@@ -61,8 +62,10 @@ pub const IO_WRITE: [&str; 3] = ["std", "io", "Write"];
 pub const IPADDR_V4: [&str; 4] = ["std", "net", "IpAddr", "V4"];
 pub const IPADDR_V6: [&str; 4] = ["std", "net", "IpAddr", "V6"];
 pub const ITERATOR: [&str; 5] = ["core", "iter", "traits", "iterator", "Iterator"];
+#[cfg(feature = "internal-lints")]
 pub const LATE_CONTEXT: [&str; 2] = ["rustc_lint", "LateContext"];
 pub const LINKED_LIST: [&str; 4] = ["alloc", "collections", "linked_list", "LinkedList"];
+#[cfg(feature = "internal-lints")]
 pub const LINT: [&str; 2] = ["rustc_lint_defs", "Lint"];
 pub const MEM_DISCRIMINANT: [&str; 3] = ["core", "mem", "discriminant"];
 pub const MEM_FORGET: [&str; 3] = ["core", "mem", "forget"];
@@ -133,6 +136,7 @@ pub const STR_ENDS_WITH: [&str; 4] = ["core", "str", "<impl str>", "ends_with"];
 pub const STR_FROM_UTF8: [&str; 4] = ["core", "str", "converts", "from_utf8"];
 pub const STR_LEN: [&str; 4] = ["core", "str", "<impl str>", "len"];
 pub const STR_STARTS_WITH: [&str; 4] = ["core", "str", "<impl str>", "starts_with"];
+#[cfg(feature = "internal-lints")]
 pub const SYNTAX_CONTEXT: [&str; 3] = ["rustc_span", "hygiene", "SyntaxContext"];
 pub const TO_OWNED: [&str; 3] = ["alloc", "borrow", "ToOwned"];
 pub const TO_OWNED_METHOD: [&str; 4] = ["alloc", "borrow", "ToOwned", "to_owned"];
diff --git a/clippy_lints/src/utils/visitors.rs b/clippy_lints/src/utils/visitors.rs
index b0837b6c43e..28b3e79d7a6 100644
--- a/clippy_lints/src/utils/visitors.rs
+++ b/clippy_lints/src/utils/visitors.rs
@@ -1,5 +1,7 @@
 use rustc_hir as hir;
-use rustc_hir::intravisit::{self, Visitor};
+use rustc_hir::def::Res;
+use rustc_hir::intravisit::{self, walk_expr, NestedVisitorMap, Visitor};
+use rustc_hir::{Arm, Expr, ExprKind, HirId, QPath, Stmt};
 use rustc_lint::LateContext;
 use rustc_middle::hir::map::Map;
 
@@ -123,3 +125,54 @@ where
         !ret_finder.failed
     }
 }
+
+pub struct LocalUsedVisitor {
+    pub local_hir_id: HirId,
+    pub used: bool,
+}
+
+impl LocalUsedVisitor {
+    pub fn new(local_hir_id: HirId) -> Self {
+        Self {
+            local_hir_id,
+            used: false,
+        }
+    }
+
+    fn check<T>(&mut self, t: T, visit: fn(&mut Self, T)) -> bool {
+        visit(self, t);
+        std::mem::replace(&mut self.used, false)
+    }
+
+    pub fn check_arm(&mut self, arm: &Arm<'_>) -> bool {
+        self.check(arm, Self::visit_arm)
+    }
+
+    pub fn check_expr(&mut self, expr: &Expr<'_>) -> bool {
+        self.check(expr, Self::visit_expr)
+    }
+
+    pub fn check_stmt(&mut self, stmt: &Stmt<'_>) -> bool {
+        self.check(stmt, Self::visit_stmt)
+    }
+}
+
+impl<'v> Visitor<'v> for LocalUsedVisitor {
+    type Map = Map<'v>;
+
+    fn visit_expr(&mut self, expr: &'v Expr<'v>) {
+        if let ExprKind::Path(QPath::Resolved(None, path)) = expr.kind {
+            if let Res::Local(id) = path.res {
+                if id == self.local_hir_id {
+                    self.used = true;
+                    return;
+                }
+            }
+        }
+        walk_expr(self, expr);
+    }
+
+    fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
+        NestedVisitorMap::None
+    }
+}
diff --git a/tests/compile-test.rs b/tests/compile-test.rs
index 0e8f7683103..ec3af94b9ca 100644
--- a/tests/compile-test.rs
+++ b/tests/compile-test.rs
@@ -12,6 +12,9 @@ use std::path::{Path, PathBuf};
 
 mod cargo;
 
+// whether to run internal tests or not
+const RUN_INTERNAL_TESTS: bool = cfg!(feature = "internal-lints");
+
 fn host_lib() -> PathBuf {
     option_env!("HOST_LIBS").map_or(cargo::CARGO_TARGET_DIR.join(env!("PROFILE")), PathBuf::from)
 }
@@ -96,6 +99,16 @@ fn run_mode(cfg: &mut compiletest::Config) {
     compiletest::run_tests(&cfg);
 }
 
+fn run_internal_tests(cfg: &mut compiletest::Config) {
+    // only run internal tests with the internal-tests feature
+    if !RUN_INTERNAL_TESTS {
+        return;
+    }
+    cfg.mode = TestMode::Ui;
+    cfg.src_base = Path::new("tests").join("ui-internal");
+    compiletest::run_tests(&cfg);
+}
+
 fn run_ui_toml(config: &mut compiletest::Config) {
     fn run_tests(config: &compiletest::Config, mut tests: Vec<tester::TestDescAndFn>) -> Result<bool, io::Error> {
         let mut result = true;
@@ -199,7 +212,6 @@ fn run_ui_cargo(config: &mut compiletest::Config) {
                         Some("main.rs") => {},
                         _ => continue,
                     }
-
                     let paths = compiletest::common::TestPaths {
                         file: file_path,
                         base: config.src_base.clone(),
@@ -253,4 +265,5 @@ fn compile_test() {
     run_mode(&mut config);
     run_ui_toml(&mut config);
     run_ui_cargo(&mut config);
+    run_internal_tests(&mut config);
 }
diff --git a/tests/dogfood.rs b/tests/dogfood.rs
index 48e0478f169..a6163a83d76 100644
--- a/tests/dogfood.rs
+++ b/tests/dogfood.rs
@@ -18,7 +18,8 @@ fn dogfood_clippy() {
     }
     let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
 
-    let output = Command::new(&*CLIPPY_PATH)
+    let mut command = Command::new(&*CLIPPY_PATH);
+    command
         .current_dir(root_dir)
         .env("CLIPPY_DOGFOOD", "1")
         .env("CARGO_INCREMENTAL", "0")
@@ -27,11 +28,16 @@ fn dogfood_clippy() {
         .arg("--all-features")
         .arg("--")
         .args(&["-D", "clippy::all"])
-        .args(&["-D", "clippy::internal"])
         .args(&["-D", "clippy::pedantic"])
-        .arg("-Cdebuginfo=0") // disable debuginfo to generate less data in the target dir
-        .output()
-        .unwrap();
+        .arg("-Cdebuginfo=0"); // disable debuginfo to generate less data in the target dir
+
+    // internal lints only exist if we build with the internal-lints feature
+    if cfg!(feature = "internal-lints") {
+        command.args(&["-D", "clippy::internal"]);
+    }
+
+    let output = command.output().unwrap();
+
     println!("status: {}", output.status);
     println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
     println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
diff --git a/tests/ui/collapsible_span_lint_calls.fixed b/tests/ui-internal/collapsible_span_lint_calls.fixed
index e588c23345e..e588c23345e 100644
--- a/tests/ui/collapsible_span_lint_calls.fixed
+++ b/tests/ui-internal/collapsible_span_lint_calls.fixed
diff --git a/tests/ui/collapsible_span_lint_calls.rs b/tests/ui-internal/collapsible_span_lint_calls.rs
index d5dd3bb562b..d5dd3bb562b 100644
--- a/tests/ui/collapsible_span_lint_calls.rs
+++ b/tests/ui-internal/collapsible_span_lint_calls.rs
diff --git a/tests/ui/collapsible_span_lint_calls.stderr b/tests/ui-internal/collapsible_span_lint_calls.stderr
index 874d4a9f255..874d4a9f255 100644
--- a/tests/ui/collapsible_span_lint_calls.stderr
+++ b/tests/ui-internal/collapsible_span_lint_calls.stderr
diff --git a/tests/ui/custom_ice_message.rs b/tests/ui-internal/custom_ice_message.rs
index 5b30c9d5721..5b30c9d5721 100644
--- a/tests/ui/custom_ice_message.rs
+++ b/tests/ui-internal/custom_ice_message.rs
diff --git a/tests/ui/custom_ice_message.stderr b/tests/ui-internal/custom_ice_message.stderr
index a1b8e2ee162..a1b8e2ee162 100644
--- a/tests/ui/custom_ice_message.stderr
+++ b/tests/ui-internal/custom_ice_message.stderr
diff --git a/tests/ui/default_lint.rs b/tests/ui-internal/default_lint.rs
index 053faae02ce..053faae02ce 100644
--- a/tests/ui/default_lint.rs
+++ b/tests/ui-internal/default_lint.rs
diff --git a/tests/ui/default_lint.stderr b/tests/ui-internal/default_lint.stderr
index 5c5836a7d29..5c5836a7d29 100644
--- a/tests/ui/default_lint.stderr
+++ b/tests/ui-internal/default_lint.stderr
diff --git a/tests/ui/invalid_paths.rs b/tests/ui-internal/invalid_paths.rs
index 01e28ae5e9d..01e28ae5e9d 100644
--- a/tests/ui/invalid_paths.rs
+++ b/tests/ui-internal/invalid_paths.rs
diff --git a/tests/ui/invalid_paths.stderr b/tests/ui-internal/invalid_paths.stderr
index bd69d661b71..bd69d661b71 100644
--- a/tests/ui/invalid_paths.stderr
+++ b/tests/ui-internal/invalid_paths.stderr
diff --git a/tests/ui/lint_without_lint_pass.rs b/tests/ui-internal/lint_without_lint_pass.rs
index beaef79a340..beaef79a340 100644
--- a/tests/ui/lint_without_lint_pass.rs
+++ b/tests/ui-internal/lint_without_lint_pass.rs
diff --git a/tests/ui/lint_without_lint_pass.stderr b/tests/ui-internal/lint_without_lint_pass.stderr
index 1257dae96d7..1257dae96d7 100644
--- a/tests/ui/lint_without_lint_pass.stderr
+++ b/tests/ui-internal/lint_without_lint_pass.stderr
diff --git a/tests/ui/match_type_on_diag_item.rs b/tests/ui-internal/match_type_on_diag_item.rs
index fe950b0aa7c..fe950b0aa7c 100644
--- a/tests/ui/match_type_on_diag_item.rs
+++ b/tests/ui-internal/match_type_on_diag_item.rs
diff --git a/tests/ui/match_type_on_diag_item.stderr b/tests/ui-internal/match_type_on_diag_item.stderr
index 82465dbaf6e..82465dbaf6e 100644
--- a/tests/ui/match_type_on_diag_item.stderr
+++ b/tests/ui-internal/match_type_on_diag_item.stderr
diff --git a/tests/ui/outer_expn_data.fixed b/tests/ui-internal/outer_expn_data.fixed
index b0b3498f057..b0b3498f057 100644
--- a/tests/ui/outer_expn_data.fixed
+++ b/tests/ui-internal/outer_expn_data.fixed
diff --git a/tests/ui/outer_expn_data.rs b/tests/ui-internal/outer_expn_data.rs
index 55a3fed00d0..55a3fed00d0 100644
--- a/tests/ui/outer_expn_data.rs
+++ b/tests/ui-internal/outer_expn_data.rs
diff --git a/tests/ui/outer_expn_data.stderr b/tests/ui-internal/outer_expn_data.stderr
index 56b6ce1f78e..56b6ce1f78e 100644
--- a/tests/ui/outer_expn_data.stderr
+++ b/tests/ui-internal/outer_expn_data.stderr
diff --git a/tests/ui/collapsible_match.rs b/tests/ui/collapsible_match.rs
new file mode 100644
index 00000000000..a83e6c77b12
--- /dev/null
+++ b/tests/ui/collapsible_match.rs
@@ -0,0 +1,239 @@
+#![warn(clippy::collapsible_match)]
+#![allow(clippy::needless_return, clippy::no_effect, clippy::single_match)]
+
+fn lint_cases(opt_opt: Option<Option<u32>>, res_opt: Result<Option<u32>, String>) {
+    // match without block
+    match res_opt {
+        Ok(val) => match val {
+            Some(n) => foo(n),
+            _ => return,
+        },
+        _ => return,
+    }
+
+    // match with block
+    match res_opt {
+        Ok(val) => match val {
+            Some(n) => foo(n),
+            _ => return,
+        },
+        _ => return,
+    }
+
+    // if let, if let
+    if let Ok(val) = res_opt {
+        if let Some(n) = val {
+            take(n);
+        }
+    }
+
+    // if let else, if let else
+    if let Ok(val) = res_opt {
+        if let Some(n) = val {
+            take(n);
+        } else {
+            return;
+        }
+    } else {
+        return;
+    }
+
+    // if let, match
+    if let Ok(val) = res_opt {
+        match val {
+            Some(n) => foo(n),
+            _ => (),
+        }
+    }
+
+    // match, if let
+    match res_opt {
+        Ok(val) => {
+            if let Some(n) = val {
+                take(n);
+            }
+        },
+        _ => {},
+    }
+
+    // if let else, match
+    if let Ok(val) = res_opt {
+        match val {
+            Some(n) => foo(n),
+            _ => return,
+        }
+    } else {
+        return;
+    }
+
+    // match, if let else
+    match res_opt {
+        Ok(val) => {
+            if let Some(n) = val {
+                take(n);
+            } else {
+                return;
+            }
+        },
+        _ => return,
+    }
+
+    // None in inner match same as outer wild branch
+    match res_opt {
+        Ok(val) => match val {
+            Some(n) => foo(n),
+            None => return,
+        },
+        _ => return,
+    }
+
+    // None in outer match same as inner wild branch
+    match opt_opt {
+        Some(val) => match val {
+            Some(n) => foo(n),
+            _ => return,
+        },
+        None => return,
+    }
+}
+
+fn negative_cases(res_opt: Result<Option<u32>, String>, res_res: Result<Result<u32, String>, String>) {
+    // no wild pattern in outer match
+    match res_opt {
+        Ok(val) => match val {
+            Some(n) => foo(n),
+            _ => return,
+        },
+        Err(_) => return,
+    }
+
+    // inner branch is not wild or None
+    match res_res {
+        Ok(val) => match val {
+            Ok(n) => foo(n),
+            Err(_) => return,
+        },
+        _ => return,
+    }
+
+    // statement before inner match
+    match res_opt {
+        Ok(val) => {
+            "hi buddy";
+            match val {
+                Some(n) => foo(n),
+                _ => return,
+            }
+        },
+        _ => return,
+    }
+
+    // statement after inner match
+    match res_opt {
+        Ok(val) => {
+            match val {
+                Some(n) => foo(n),
+                _ => return,
+            }
+            "hi buddy";
+        },
+        _ => return,
+    }
+
+    // wild branches do not match
+    match res_opt {
+        Ok(val) => match val {
+            Some(n) => foo(n),
+            _ => {
+                "sup";
+                return;
+            },
+        },
+        _ => return,
+    }
+
+    // binding used in if guard
+    match res_opt {
+        Ok(val) if val.is_some() => match val {
+            Some(n) => foo(n),
+            _ => return,
+        },
+        _ => return,
+    }
+
+    // binding used in inner match body
+    match res_opt {
+        Ok(val) => match val {
+            Some(_) => take(val),
+            _ => return,
+        },
+        _ => return,
+    }
+
+    // if guard on inner match
+    {
+        match res_opt {
+            Ok(val) => match val {
+                Some(n) if make() => foo(n),
+                _ => return,
+            },
+            _ => return,
+        }
+        match res_opt {
+            Ok(val) => match val {
+                _ => make(),
+                _ if make() => return,
+            },
+            _ => return,
+        }
+    }
+
+    // differing macro contexts
+    {
+        macro_rules! mac {
+            ($val:ident) => {
+                match $val {
+                    Some(n) => foo(n),
+                    _ => return,
+                }
+            };
+        }
+        match res_opt {
+            Ok(val) => mac!(val),
+            _ => return,
+        }
+    }
+
+    // OR pattern
+    enum E<T> {
+        A(T),
+        B(T),
+        C(T),
+    };
+    match make::<E<Option<u32>>>() {
+        E::A(val) | E::B(val) => match val {
+            Some(n) => foo(n),
+            _ => return,
+        },
+        _ => return,
+    }
+    match make::<Option<E<u32>>>() {
+        Some(val) => match val {
+            E::A(val) | E::B(val) => foo(val),
+            _ => return,
+        },
+        _ => return,
+    }
+}
+
+fn make<T>() -> T {
+    unimplemented!()
+}
+
+fn foo<T, U>(t: T) -> U {
+    unimplemented!()
+}
+
+fn take<T>(t: T) {}
+
+fn main() {}
diff --git a/tests/ui/collapsible_match.stderr b/tests/ui/collapsible_match.stderr
new file mode 100644
index 00000000000..63ac6a1613d
--- /dev/null
+++ b/tests/ui/collapsible_match.stderr
@@ -0,0 +1,179 @@
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:7:20
+   |
+LL |           Ok(val) => match val {
+   |  ____________________^
+LL | |             Some(n) => foo(n),
+LL | |             _ => return,
+LL | |         },
+   | |_________^
+   |
+   = note: `-D clippy::collapsible-match` implied by `-D warnings`
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:7:12
+   |
+LL |         Ok(val) => match val {
+   |            ^^^ Replace this binding
+LL |             Some(n) => foo(n),
+   |             ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:16:20
+   |
+LL |           Ok(val) => match val {
+   |  ____________________^
+LL | |             Some(n) => foo(n),
+LL | |             _ => return,
+LL | |         },
+   | |_________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:16:12
+   |
+LL |         Ok(val) => match val {
+   |            ^^^ Replace this binding
+LL |             Some(n) => foo(n),
+   |             ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:25:9
+   |
+LL | /         if let Some(n) = val {
+LL | |             take(n);
+LL | |         }
+   | |_________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:24:15
+   |
+LL |     if let Ok(val) = res_opt {
+   |               ^^^ Replace this binding
+LL |         if let Some(n) = val {
+   |                ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:32:9
+   |
+LL | /         if let Some(n) = val {
+LL | |             take(n);
+LL | |         } else {
+LL | |             return;
+LL | |         }
+   | |_________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:31:15
+   |
+LL |     if let Ok(val) = res_opt {
+   |               ^^^ Replace this binding
+LL |         if let Some(n) = val {
+   |                ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:43:9
+   |
+LL | /         match val {
+LL | |             Some(n) => foo(n),
+LL | |             _ => (),
+LL | |         }
+   | |_________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:42:15
+   |
+LL |     if let Ok(val) = res_opt {
+   |               ^^^ Replace this binding
+LL |         match val {
+LL |             Some(n) => foo(n),
+   |             ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:52:13
+   |
+LL | /             if let Some(n) = val {
+LL | |                 take(n);
+LL | |             }
+   | |_____________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:51:12
+   |
+LL |         Ok(val) => {
+   |            ^^^ Replace this binding
+LL |             if let Some(n) = val {
+   |                    ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:61:9
+   |
+LL | /         match val {
+LL | |             Some(n) => foo(n),
+LL | |             _ => return,
+LL | |         }
+   | |_________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:60:15
+   |
+LL |     if let Ok(val) = res_opt {
+   |               ^^^ Replace this binding
+LL |         match val {
+LL |             Some(n) => foo(n),
+   |             ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:72:13
+   |
+LL | /             if let Some(n) = val {
+LL | |                 take(n);
+LL | |             } else {
+LL | |                 return;
+LL | |             }
+   | |_____________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:71:12
+   |
+LL |         Ok(val) => {
+   |            ^^^ Replace this binding
+LL |             if let Some(n) = val {
+   |                    ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:83:20
+   |
+LL |           Ok(val) => match val {
+   |  ____________________^
+LL | |             Some(n) => foo(n),
+LL | |             None => return,
+LL | |         },
+   | |_________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:83:12
+   |
+LL |         Ok(val) => match val {
+   |            ^^^ Replace this binding
+LL |             Some(n) => foo(n),
+   |             ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match.rs:92:22
+   |
+LL |           Some(val) => match val {
+   |  ______________________^
+LL | |             Some(n) => foo(n),
+LL | |             _ => return,
+LL | |         },
+   | |_________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match.rs:92:14
+   |
+LL |         Some(val) => match val {
+   |              ^^^ Replace this binding
+LL |             Some(n) => foo(n),
+   |             ^^^^^^^ with this pattern
+
+error: aborting due to 10 previous errors
+
diff --git a/tests/ui/collapsible_match2.rs b/tests/ui/collapsible_match2.rs
new file mode 100644
index 00000000000..d571ac4ab69
--- /dev/null
+++ b/tests/ui/collapsible_match2.rs
@@ -0,0 +1,53 @@
+#![warn(clippy::collapsible_match)]
+#![allow(clippy::needless_return, clippy::no_effect, clippy::single_match)]
+
+fn lint_cases(opt_opt: Option<Option<u32>>, res_opt: Result<Option<u32>, String>) {
+    // if guards on outer match
+    {
+        match res_opt {
+            Ok(val) if make() => match val {
+                Some(n) => foo(n),
+                _ => return,
+            },
+            _ => return,
+        }
+        match res_opt {
+            Ok(val) => match val {
+                Some(n) => foo(n),
+                _ => return,
+            },
+            _ if make() => return,
+            _ => return,
+        }
+    }
+
+    // macro
+    {
+        macro_rules! mac {
+            ($outer:expr => $pat:pat, $e:expr => $inner_pat:pat, $then:expr) => {
+                match $outer {
+                    $pat => match $e {
+                        $inner_pat => $then,
+                        _ => return,
+                    },
+                    _ => return,
+                }
+            };
+        }
+        // Lint this since the patterns are not defined by the macro.
+        // Allows the lint to work on if_chain! for example.
+        // Fixing the lint requires knowledge of the specific macro, but we optimistically assume that
+        // there is still a better way to write this.
+        mac!(res_opt => Ok(val), val => Some(n), foo(n));
+    }
+}
+
+fn make<T>() -> T {
+    unimplemented!()
+}
+
+fn foo<T, U>(t: T) -> U {
+    unimplemented!()
+}
+
+fn main() {}
diff --git a/tests/ui/collapsible_match2.stderr b/tests/ui/collapsible_match2.stderr
new file mode 100644
index 00000000000..490d82d12cd
--- /dev/null
+++ b/tests/ui/collapsible_match2.stderr
@@ -0,0 +1,61 @@
+error: Unnecessary nested match
+  --> $DIR/collapsible_match2.rs:8:34
+   |
+LL |               Ok(val) if make() => match val {
+   |  __________________________________^
+LL | |                 Some(n) => foo(n),
+LL | |                 _ => return,
+LL | |             },
+   | |_____________^
+   |
+   = note: `-D clippy::collapsible-match` implied by `-D warnings`
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match2.rs:8:16
+   |
+LL |             Ok(val) if make() => match val {
+   |                ^^^ Replace this binding
+LL |                 Some(n) => foo(n),
+   |                 ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match2.rs:15:24
+   |
+LL |               Ok(val) => match val {
+   |  ________________________^
+LL | |                 Some(n) => foo(n),
+LL | |                 _ => return,
+LL | |             },
+   | |_____________^
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match2.rs:15:16
+   |
+LL |             Ok(val) => match val {
+   |                ^^^ Replace this binding
+LL |                 Some(n) => foo(n),
+   |                 ^^^^^^^ with this pattern
+
+error: Unnecessary nested match
+  --> $DIR/collapsible_match2.rs:29:29
+   |
+LL |                       $pat => match $e {
+   |  _____________________________^
+LL | |                         $inner_pat => $then,
+LL | |                         _ => return,
+LL | |                     },
+   | |_____________________^
+...
+LL |           mac!(res_opt => Ok(val), val => Some(n), foo(n));
+   |           ------------------------------------------------- in this macro invocation
+   |
+help: The outer pattern can be modified to include the inner pattern.
+  --> $DIR/collapsible_match2.rs:41:28
+   |
+LL |         mac!(res_opt => Ok(val), val => Some(n), foo(n));
+   |                            ^^^          ^^^^^^^ with this pattern
+   |                            |
+   |                            Replace this binding
+   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: aborting due to 3 previous errors
+
diff --git a/tests/ui/min_rust_version_attr.rs b/tests/ui/min_rust_version_attr.rs
index 8ed483a3ac6..1026cc40d3b 100644
--- a/tests/ui/min_rust_version_attr.rs
+++ b/tests/ui/min_rust_version_attr.rs
@@ -35,7 +35,7 @@ fn match_same_arms2() {
     };
 }
 
-fn manual_strip_msrv() {
+pub fn manual_strip_msrv() {
     let s = "hello, world!";
     if s.starts_with("hello, ") {
         assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!");
@@ -49,3 +49,39 @@ fn main() {
     match_same_arms2();
     manual_strip_msrv();
 }
+
+mod meets_msrv {
+    #![feature(custom_inner_attributes)]
+    #![clippy::msrv = "1.45.0"]
+
+    fn main() {
+        let s = "hello, world!";
+        if s.starts_with("hello, ") {
+            assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!");
+        }
+    }
+}
+
+mod just_under_msrv {
+    #![feature(custom_inner_attributes)]
+    #![clippy::msrv = "1.46.0"]
+
+    fn main() {
+        let s = "hello, world!";
+        if s.starts_with("hello, ") {
+            assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!");
+        }
+    }
+}
+
+mod just_above_msrv {
+    #![feature(custom_inner_attributes)]
+    #![clippy::msrv = "1.44.0"]
+
+    fn main() {
+        let s = "hello, world!";
+        if s.starts_with("hello, ") {
+            assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!");
+        }
+    }
+}
diff --git a/tests/ui/min_rust_version_attr.stderr b/tests/ui/min_rust_version_attr.stderr
new file mode 100644
index 00000000000..3e1af046e7a
--- /dev/null
+++ b/tests/ui/min_rust_version_attr.stderr
@@ -0,0 +1,37 @@
+error: stripping a prefix manually
+  --> $DIR/min_rust_version_attr.rs:60:24
+   |
+LL |             assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!");
+   |                        ^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: `-D clippy::manual-strip` implied by `-D warnings`
+note: the prefix was tested here
+  --> $DIR/min_rust_version_attr.rs:59:9
+   |
+LL |         if s.starts_with("hello, ") {
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+help: try using the `strip_prefix` method
+   |
+LL |         if let Some(<stripped>) = s.strip_prefix("hello, ") {
+LL |             assert_eq!(<stripped>.to_uppercase(), "WORLD!");
+   |
+
+error: stripping a prefix manually
+  --> $DIR/min_rust_version_attr.rs:72:24
+   |
+LL |             assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!");
+   |                        ^^^^^^^^^^^^^^^^^^^^
+   |
+note: the prefix was tested here
+  --> $DIR/min_rust_version_attr.rs:71:9
+   |
+LL |         if s.starts_with("hello, ") {
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+help: try using the `strip_prefix` method
+   |
+LL |         if let Some(<stripped>) = s.strip_prefix("hello, ") {
+LL |             assert_eq!(<stripped>.to_uppercase(), "WORLD!");
+   |
+
+error: aborting due to 2 previous errors
+
diff --git a/tests/ui/min_rust_version_no_patch.rs b/tests/ui/min_rust_version_no_patch.rs
index 515fe8f95e9..98fffe1e351 100644
--- a/tests/ui/min_rust_version_no_patch.rs
+++ b/tests/ui/min_rust_version_no_patch.rs
@@ -1,6 +1,6 @@
 #![allow(clippy::redundant_clone)]
 #![feature(custom_inner_attributes)]
-#![clippy::msrv = "^1.0"]
+#![clippy::msrv = "1.0"]
 
 fn manual_strip_msrv() {
     let s = "hello, world!";