about summary refs log tree commit diff
diff options
context:
space:
mode:
authorAlex Macleod <alex@macleod.io>2024-08-05 14:08:04 +0000
committerAlex Macleod <alex@macleod.io>2024-08-12 20:24:46 +0000
commit182cd5f2785ae77f3a05cf75ee301ed8850c8d0a (patch)
tree29162880d225a6b9afed03ce405bb10844f9d39d
parent88271075267a3d7172f07777cc8c3d5a6ba2f7a2 (diff)
downloadrust-182cd5f2785ae77f3a05cf75ee301ed8850c8d0a.tar.gz
rust-182cd5f2785ae77f3a05cf75ee301ed8850c8d0a.zip
Replace the metadata collector with tests
-rw-r--r--.cargo/config.toml6
-rw-r--r--.github/workflows/clippy_bors.yml5
-rw-r--r--Cargo.toml4
-rw-r--r--book/src/development/adding_lints.md2
-rw-r--r--book/src/lint_configuration.md4
-rw-r--r--clippy_dev/src/update_lints.rs2
-rw-r--r--clippy_lints/src/declared_lints.rs4
-rw-r--r--clippy_lints/src/lib.rs48
-rw-r--r--clippy_lints/src/utils/internal_lints.rs1
-rw-r--r--clippy_lints/src/utils/internal_lints/metadata_collector.rs1080
-rw-r--r--declare_clippy_lint/src/lib.rs25
-rw-r--r--tests/compile-test.rs382
-rw-r--r--tests/config-metadata.rs76
-rw-r--r--util/gh-pages/index.html6
-rw-r--r--util/gh-pages/script.js9
15 files changed, 448 insertions, 1206 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index ce07290d1e1..d9c635df5dc 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,10 +1,10 @@
 [alias]
+bless = "test --config env.RUSTC_BLESS='1'"
 uitest = "test --test compile-test"
-uibless = "test --test compile-test -- -- --bless"
-bless = "test -- -- --bless"
+uibless = "bless --test compile-test"
 dev = "run --package clippy_dev --bin clippy_dev --manifest-path clippy_dev/Cargo.toml --"
 lintcheck = "run --package lintcheck --bin lintcheck --manifest-path lintcheck/Cargo.toml  -- "
-collect-metadata = "test --test dogfood --features internal -- collect_metadata"
+collect-metadata = "test --test compile-test --config env.COLLECT_METADATA='1'"
 
 [build]
 # -Zbinary-dep-depinfo allows us to track which rlib files to use for compiling UI tests
diff --git a/.github/workflows/clippy_bors.yml b/.github/workflows/clippy_bors.yml
index 10e18e84c89..2aa13313fa5 100644
--- a/.github/workflows/clippy_bors.yml
+++ b/.github/workflows/clippy_bors.yml
@@ -136,11 +136,6 @@ jobs:
     - name: Test metadata collection
       run: cargo collect-metadata
 
-    - name: Test lint_configuration.md is up-to-date
-      run: |
-        echo "run \`cargo collect-metadata\` if this fails"
-        git update-index --refresh
-
   integration_build:
     needs: changelog
     runs-on: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index 78409c7a09e..b48b881097f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,8 +30,11 @@ color-print = "0.3.4"
 anstream = "0.6.0"
 
 [dev-dependencies]
+cargo_metadata = "0.18.1"
 ui_test = "0.25"
 regex = "1.5.5"
+serde = { version = "1.0.145", features = ["derive"] }
+serde_json = "1.0.122"
 toml = "0.7.3"
 walkdir = "2.3"
 filetime = "0.2.9"
@@ -41,7 +44,6 @@ itertools = "0.12"
 clippy_utils = { path = "clippy_utils" }
 if_chain = "1.0"
 quote = "1.0.25"
-serde = { version = "1.0.145", features = ["derive"] }
 syn = { version = "2.0", features = ["full"] }
 futures = "0.3"
 parking_lot = "0.12"
diff --git a/book/src/development/adding_lints.md b/book/src/development/adding_lints.md
index a71d94daca7..963e02e5c16 100644
--- a/book/src/development/adding_lints.md
+++ b/book/src/development/adding_lints.md
@@ -739,7 +739,7 @@ for some users. Adding a configuration is done in the following steps:
 
 5. Update [Lint Configuration](../lint_configuration.md)
 
-   Run `cargo collect-metadata` to generate documentation changes for the book.
+   Run `cargo bless --test config-metadata` to generate documentation changes for the book.
 
 [`clippy_config::conf`]: https://github.com/rust-lang/rust-clippy/blob/master/clippy_config/src/conf.rs
 [`clippy_lints` lib file]: https://github.com/rust-lang/rust-clippy/blob/master/clippy_lints/src/lib.rs
diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md
index e3d550b1466..4f23afe2e11 100644
--- a/book/src/lint_configuration.md
+++ b/book/src/lint_configuration.md
@@ -1,5 +1,5 @@
 <!--
-This file is generated by `cargo collect-metadata`.
+This file is generated by `cargo bless --test config-metadata`.
 Please use that command to update the file and do not edit it by hand.
 -->
 
@@ -949,5 +949,3 @@ Whether to also emit warnings for unsafe blocks with metavariable expansions in
 ---
 **Affected lints:**
 * [`macro_metavars_in_unsafe`](https://rust-lang.github.io/rust-clippy/master/index.html#macro_metavars_in_unsafe)
-
-
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index 15578d69c3a..8dbda1c634c 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -604,7 +604,7 @@ fn gen_declared_lints<'a>(
     details.sort_unstable();
 
     let mut output = GENERATED_FILE_COMMENT.to_string();
-    output.push_str("pub(crate) static LINTS: &[&crate::LintInfo] = &[\n");
+    output.push_str("pub static LINTS: &[&crate::LintInfo] = &[\n");
 
     for (is_public, module_name, lint_name) in details {
         if !is_public {
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 3fb083dd833..8754a4dff87 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -2,7 +2,7 @@
 // Use that command to update this file and do not edit by hand.
 // Manual edits will be overwritten.
 
-pub(crate) static LINTS: &[&crate::LintInfo] = &[
+pub static LINTS: &[&crate::LintInfo] = &[
     #[cfg(feature = "internal")]
     crate::utils::internal_lints::almost_standard_lint_formulation::ALMOST_STANDARD_LINT_FORMULATION_INFO,
     #[cfg(feature = "internal")]
@@ -22,8 +22,6 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
     #[cfg(feature = "internal")]
     crate::utils::internal_lints::lint_without_lint_pass::MISSING_CLIPPY_VERSION_ATTRIBUTE_INFO,
     #[cfg(feature = "internal")]
-    crate::utils::internal_lints::metadata_collector::METADATA_COLLECTOR_INFO,
-    #[cfg(feature = "internal")]
     crate::utils::internal_lints::msrv_attr_impl::MISSING_MSRV_ATTR_IMPL_INFO,
     #[cfg(feature = "internal")]
     crate::utils::internal_lints::outer_expn_data_pass::OUTER_EXPN_EXPN_DATA_INFO,
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index ce13a9afef5..2ac06b360be 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -66,8 +66,8 @@ extern crate declare_clippy_lint;
 #[cfg_attr(feature = "internal", allow(clippy::missing_clippy_version_attribute))]
 mod utils;
 
-mod declared_lints;
-mod deprecated_lints;
+pub mod declared_lints;
+pub mod deprecated_lints;
 
 // begin lints modules, do not remove this comment, it’s used in `update_lints`
 mod absolute_paths;
@@ -440,7 +440,7 @@ impl RegistrationGroups {
     }
 }
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug)]
 pub(crate) enum LintCategory {
     Cargo,
     Complexity,
@@ -479,11 +479,39 @@ impl LintCategory {
     }
 }
 
-pub(crate) struct LintInfo {
+pub struct LintInfo {
     /// Double reference to maintain pointer equality
-    lint: &'static &'static Lint,
+    pub lint: &'static &'static Lint,
     category: LintCategory,
-    explanation: &'static str,
+    pub explanation: &'static str,
+    /// e.g. `clippy_lints/src/absolute_paths.rs#43`
+    pub location: &'static str,
+    pub version: Option<&'static str>,
+}
+
+impl LintInfo {
+    /// Returns the lint name in lowercase without the `clippy::` prefix
+    #[allow(clippy::missing_panics_doc)]
+    pub fn name_lower(&self) -> String {
+        self.lint.name.strip_prefix("clippy::").unwrap().to_ascii_lowercase()
+    }
+
+    /// Returns the name of the lint's category in lowercase (`style`, `pedantic`)
+    pub fn category_str(&self) -> &'static str {
+        match self.category {
+            Cargo => "cargo",
+            Complexity => "complexity",
+            Correctness => "correctness",
+            Nursery => "nursery",
+            Pedantic => "pedantic",
+            Perf => "perf",
+            Restriction => "restriction",
+            Style => "style",
+            Suspicious => "suspicious",
+            #[cfg(feature = "internal")]
+            Internal => "internal",
+        }
+    }
 }
 
 pub fn explain(name: &str) -> i32 {
@@ -538,14 +566,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
         store.register_removed(name, reason);
     }
 
-    #[cfg(feature = "internal")]
-    {
-        if std::env::var("ENABLE_METADATA_COLLECTION").eq(&Ok("1".to_string())) {
-            store.register_late_pass(|_| Box::new(utils::internal_lints::metadata_collector::MetadataCollector::new()));
-            return;
-        }
-    }
-
     let format_args_storage = FormatArgsStorage::default();
     let format_args = format_args_storage.clone();
     store.register_early_pass(move || {
diff --git a/clippy_lints/src/utils/internal_lints.rs b/clippy_lints/src/utils/internal_lints.rs
index 1d294c2944b..f662c7651f6 100644
--- a/clippy_lints/src/utils/internal_lints.rs
+++ b/clippy_lints/src/utils/internal_lints.rs
@@ -3,7 +3,6 @@ pub mod collapsible_calls;
 pub mod interning_defined_symbol;
 pub mod invalid_paths;
 pub mod lint_without_lint_pass;
-pub mod metadata_collector;
 pub mod msrv_attr_impl;
 pub mod outer_expn_data_pass;
 pub mod produce_ice;
diff --git a/clippy_lints/src/utils/internal_lints/metadata_collector.rs b/clippy_lints/src/utils/internal_lints/metadata_collector.rs
deleted file mode 100644
index c73ec262637..00000000000
--- a/clippy_lints/src/utils/internal_lints/metadata_collector.rs
+++ /dev/null
@@ -1,1080 +0,0 @@
-//! This lint is used to collect metadata about clippy lints. This metadata is exported as a json
-//! file and then used to generate the [clippy lint list](https://rust-lang.github.io/rust-clippy/master/index.html)
-//!
-//! This module and therefore the entire lint is guarded by a feature flag called `internal`
-//!
-//! The module transforms all lint names to ascii lowercase to ensure that we don't have mismatches
-//! during any comparison or mapping. (Please take care of this, it's not fun to spend time on such
-//! a simple mistake)
-
-use crate::utils::internal_lints::lint_without_lint_pass::{extract_clippy_version_value, is_lint_ref_type};
-use clippy_config::{get_configuration_metadata, ClippyConfiguration};
-
-use clippy_utils::diagnostics::span_lint;
-use clippy_utils::ty::{match_type, walk_ptrs_ty_depth};
-use clippy_utils::{last_path_segment, match_function_call, match_path, paths};
-use itertools::Itertools;
-use rustc_ast as ast;
-use rustc_data_structures::fx::FxHashMap;
-use rustc_hir::def::DefKind;
-use rustc_hir::intravisit::Visitor;
-use rustc_hir::{self as hir, intravisit, Closure, ExprKind, Item, ItemKind, Mutability, QPath};
-use rustc_lint::{unerased_lint_store, CheckLintNameResult, LateContext, LateLintPass, LintContext, LintId};
-use rustc_middle::hir::nested_filter;
-use rustc_session::impl_lint_pass;
-use rustc_span::symbol::Ident;
-use rustc_span::{sym, Loc, Span, Symbol};
-use serde::ser::SerializeStruct;
-use serde::{Serialize, Serializer};
-use std::collections::{BTreeSet, BinaryHeap};
-use std::fmt::Write as _;
-use std::fs::{self, File};
-use std::io::prelude::*;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-use std::{env, fmt};
-
-/// This is the json output file of the lint collector.
-const JSON_OUTPUT_FILE: &str = "../util/gh-pages/lints.json";
-/// This is the markdown output file of the lint collector.
-const MARKDOWN_OUTPUT_FILE: &str = "../book/src/lint_configuration.md";
-/// These groups will be ignored by the lint group matcher. This is useful for collections like
-/// `clippy::all`
-const IGNORED_LINT_GROUPS: [&str; 1] = ["clippy::all"];
-/// Lints within this group will be excluded from the collection. These groups
-/// have to be defined without the `clippy::` prefix.
-const EXCLUDED_LINT_GROUPS: [&str; 1] = ["internal"];
-/// Collected deprecated lint will be assigned to this group in the JSON output
-const DEPRECATED_LINT_GROUP_STR: &str = "deprecated";
-/// This is the lint level for deprecated lints that will be displayed in the lint list
-const DEPRECATED_LINT_LEVEL: &str = "none";
-/// This array holds Clippy's lint groups with their corresponding default lint level. The
-/// lint level for deprecated lints is set in `DEPRECATED_LINT_LEVEL`.
-const DEFAULT_LINT_LEVELS: &[(&str, &str)] = &[
-    ("correctness", "deny"),
-    ("suspicious", "warn"),
-    ("restriction", "allow"),
-    ("style", "warn"),
-    ("pedantic", "allow"),
-    ("complexity", "warn"),
-    ("perf", "warn"),
-    ("cargo", "allow"),
-    ("nursery", "allow"),
-];
-/// This prefix is in front of the lint groups in the lint store. The prefix will be trimmed
-/// to only keep the actual lint group in the output.
-const CLIPPY_LINT_GROUP_PREFIX: &str = "clippy::";
-const LINT_EMISSION_FUNCTIONS: [&[&str]; 7] = [
-    &["clippy_utils", "diagnostics", "span_lint"],
-    &["clippy_utils", "diagnostics", "span_lint_and_help"],
-    &["clippy_utils", "diagnostics", "span_lint_and_note"],
-    &["clippy_utils", "diagnostics", "span_lint_hir"],
-    &["clippy_utils", "diagnostics", "span_lint_and_sugg"],
-    &["clippy_utils", "diagnostics", "span_lint_and_then"],
-    &["clippy_utils", "diagnostics", "span_lint_hir_and_then"],
-];
-const SUGGESTION_DIAG_METHODS: [(&str, bool); 9] = [
-    ("span_suggestion", false),
-    ("span_suggestion_short", false),
-    ("span_suggestion_verbose", false),
-    ("span_suggestion_hidden", false),
-    ("tool_only_span_suggestion", false),
-    ("multipart_suggestion", true),
-    ("multipart_suggestions", true),
-    ("tool_only_multipart_suggestion", true),
-    ("span_suggestions", true),
-];
-
-/// The index of the applicability name of `paths::APPLICABILITY_VALUES`
-const APPLICABILITY_NAME_INDEX: usize = 2;
-/// This applicability will be set for unresolved applicability values.
-const APPLICABILITY_UNRESOLVED_STR: &str = "Unresolved";
-/// The version that will be displayed if none has been defined
-const VERSION_DEFAULT_STR: &str = "Unknown";
-
-const CHANGELOG_PATH: &str = "../CHANGELOG.md";
-
-declare_clippy_lint! {
-    /// ### What it does
-    /// Collects metadata about clippy lints for the website.
-    ///
-    /// This lint will be used to report problems of syntax parsing. You should hopefully never
-    /// see this but never say never I guess ^^
-    ///
-    /// ### Why is this bad?
-    /// This is not a bad thing but definitely a hacky way to do it. See
-    /// issue [#4310](https://github.com/rust-lang/rust-clippy/issues/4310) for a discussion
-    /// about the implementation.
-    ///
-    /// ### Known problems
-    /// Hopefully none. It would be pretty uncool to have a problem here :)
-    ///
-    /// ### Example output
-    /// ```json,ignore
-    /// {
-    ///     "id": "metadata_collector",
-    ///     "id_span": {
-    ///         "path": "clippy_lints/src/utils/internal_lints/metadata_collector.rs",
-    ///         "line": 1
-    ///     },
-    ///     "group": "clippy::internal",
-    ///     "docs": " ### What it does\nCollects metadata about clippy lints for the website. [...] "
-    /// }
-    /// ```
-    #[clippy::version = "1.56.0"]
-    pub METADATA_COLLECTOR,
-    internal,
-    "A busy bee collection metadata about lints"
-}
-
-impl_lint_pass!(MetadataCollector => [METADATA_COLLECTOR]);
-
-#[allow(clippy::module_name_repetitions)]
-#[derive(Debug, Clone)]
-pub struct MetadataCollector {
-    /// All collected lints
-    ///
-    /// We use a Heap here to have the lints added in alphabetic order in the export
-    lints: BinaryHeap<LintMetadata>,
-    applicability_info: FxHashMap<String, ApplicabilityInfo>,
-    config: Vec<ClippyConfiguration>,
-    clippy_project_root: PathBuf,
-}
-
-impl MetadataCollector {
-    pub fn new() -> Self {
-        Self {
-            lints: BinaryHeap::<LintMetadata>::default(),
-            applicability_info: FxHashMap::<String, ApplicabilityInfo>::default(),
-            config: get_configuration_metadata(),
-            clippy_project_root: env::current_dir()
-                .expect("failed to get current dir")
-                .ancestors()
-                .nth(1)
-                .expect("failed to get project root")
-                .to_path_buf(),
-        }
-    }
-
-    fn get_lint_configs(&self, lint_name: &str) -> Option<String> {
-        self.config
-            .iter()
-            .filter(|config| config.lints.iter().any(|&lint| lint == lint_name))
-            .map(ToString::to_string)
-            .reduce(|acc, x| acc + "\n\n" + &x)
-            .map(|configurations| {
-                format!(
-                    r#"
-### Configuration
-This lint has the following configuration variables:
-
-{configurations}
-"#
-                )
-            })
-    }
-
-    fn configs_to_markdown(&self, map_fn: fn(&ClippyConfiguration) -> String) -> String {
-        self.config
-            .iter()
-            .filter(|config| config.deprecation_reason.is_none())
-            .filter(|config| !config.lints.is_empty())
-            .map(map_fn)
-            .join("\n")
-    }
-
-    fn get_markdown_docs(&self) -> String {
-        format!(
-            r#"# Lint Configuration Options
-
-The following list shows each configuration option, along with a description, its default value, an example
-and lints affected.
-
----
-
-{}"#,
-            self.configs_to_markdown(ClippyConfiguration::to_markdown_paragraph),
-        )
-    }
-}
-
-impl Drop for MetadataCollector {
-    /// You might ask: How hacky is this?
-    /// My answer:     YES
-    fn drop(&mut self) {
-        // The metadata collector gets dropped twice, this makes sure that we only write
-        // when the list is full
-        if self.lints.is_empty() {
-            return;
-        }
-
-        let mut applicability_info = std::mem::take(&mut self.applicability_info);
-
-        // Add deprecated lints
-        self.lints.extend(
-            crate::deprecated_lints::DEPRECATED
-                .iter()
-                .zip(crate::deprecated_lints::DEPRECATED_VERSION)
-                .filter_map(|((lint, reason), version)| LintMetadata::new_deprecated(lint, reason, version)),
-        );
-        // Mapping the final data
-        let mut lints = std::mem::take(&mut self.lints).into_sorted_vec();
-        for x in &mut lints {
-            x.applicability = Some(applicability_info.remove(&x.id).unwrap_or_default());
-            replace_produces(&x.id, &mut x.docs, &self.clippy_project_root);
-        }
-
-        collect_renames(&mut lints);
-
-        // Outputting json
-        fs::write(JSON_OUTPUT_FILE, serde_json::to_string_pretty(&lints).unwrap()).unwrap();
-
-        // Outputting markdown
-        let mut file = File::create(MARKDOWN_OUTPUT_FILE).unwrap();
-        writeln!(
-            file,
-            "<!--
-This file is generated by `cargo collect-metadata`.
-Please use that command to update the file and do not edit it by hand.
--->
-
-{}",
-            self.get_markdown_docs(),
-        )
-        .unwrap();
-
-        // Write configuration links to CHANGELOG.md
-        let changelog = fs::read_to_string(CHANGELOG_PATH).unwrap();
-        let mut changelog_file = File::create(CHANGELOG_PATH).unwrap();
-        let position = changelog
-            .find("<!-- begin autogenerated links to configuration documentation -->")
-            .unwrap();
-        writeln!(
-            changelog_file,
-            "{}<!-- begin autogenerated links to configuration documentation -->\n{}\n<!-- end autogenerated links to configuration documentation -->",
-            &changelog[..position],
-            self.configs_to_markdown(ClippyConfiguration::to_markdown_link)
-        )
-        .unwrap();
-    }
-}
-
-#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
-struct LintMetadata {
-    id: String,
-    id_span: Option<SerializableSpan>,
-    group: String,
-    level: String,
-    docs: String,
-    version: String,
-    /// This field is only used in the output and will only be
-    /// mapped shortly before the actual output.
-    applicability: Option<ApplicabilityInfo>,
-    /// All the past names of lints which have been renamed.
-    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
-    former_ids: BTreeSet<String>,
-}
-
-impl LintMetadata {
-    fn new(
-        id: String,
-        id_span: SerializableSpan,
-        group: String,
-        level: &'static str,
-        version: String,
-        docs: String,
-    ) -> Self {
-        Self {
-            id,
-            id_span: Some(id_span),
-            group,
-            level: level.to_string(),
-            version,
-            docs,
-            applicability: None,
-            former_ids: BTreeSet::new(),
-        }
-    }
-
-    fn new_deprecated(name: &str, reason: &str, version: &str) -> Option<Self> {
-        // The reason starts with a lowercase letter and end without a period.
-        // This needs to be fixed for the website.
-        let mut reason = reason.to_owned();
-        if let Some(reason) = reason.get_mut(0..1) {
-            reason.make_ascii_uppercase();
-        }
-        name.strip_prefix("clippy::").map(|name| Self {
-            id: name.into(),
-            id_span: None,
-            group: DEPRECATED_LINT_GROUP_STR.into(),
-            level: DEPRECATED_LINT_LEVEL.into(),
-            version: version.into(),
-            docs: format!(
-                "### What it does\n\n\
-                Nothing. This lint has been deprecated\n\n\
-                ### Deprecation reason\n\n{reason}.\n",
-            ),
-            applicability: None,
-            former_ids: BTreeSet::new(),
-        })
-    }
-}
-
-fn replace_produces(lint_name: &str, docs: &mut String, clippy_project_root: &Path) {
-    let mut doc_lines = docs.lines().map(ToString::to_string).collect::<Vec<_>>();
-    let mut lines = doc_lines.iter_mut();
-
-    'outer: loop {
-        // Find the start of the example
-
-        // ```rust
-        loop {
-            match lines.next() {
-                Some(line) if line.trim_start().starts_with("```rust") => {
-                    if line.contains("ignore") || line.contains("no_run") {
-                        // A {{produces}} marker may have been put on a ignored code block by mistake,
-                        // just seek to the end of the code block and continue checking.
-                        if lines.any(|line| line.trim_start().starts_with("```")) {
-                            continue;
-                        }
-
-                        panic!("lint `{lint_name}` has an unterminated code block")
-                    }
-
-                    break;
-                },
-                Some(line) if line.trim_start() == "{{produces}}" => {
-                    panic!("lint `{lint_name}` has marker {{{{produces}}}} with an ignored or missing code block")
-                },
-                Some(line) => {
-                    let line = line.trim();
-                    // These are the two most common markers of the corrections section
-                    if line.eq_ignore_ascii_case("Use instead:") || line.eq_ignore_ascii_case("Could be written as:") {
-                        break 'outer;
-                    }
-                },
-                None => break 'outer,
-            }
-        }
-
-        // Collect the example
-        let mut example = Vec::new();
-        loop {
-            match lines.next() {
-                Some(line) if line.trim_start() == "```" => break,
-                Some(line) => example.push(line),
-                None => panic!("lint `{lint_name}` has an unterminated code block"),
-            }
-        }
-
-        // Find the {{produces}} and attempt to generate the output
-        loop {
-            match lines.next() {
-                Some(line) if line.is_empty() => {},
-                Some(line) if line.trim() == "{{produces}}" => {
-                    let output = get_lint_output(lint_name, &example, clippy_project_root);
-                    line.replace_range(
-                        ..,
-                        &format!(
-                            "<details>\
-                            <summary>Produces</summary>\n\
-                            \n\
-                            ```text\n\
-                            {output}\n\
-                            ```\n\
-                        </details>"
-                        ),
-                    );
-
-                    break;
-                },
-                // No {{produces}}, we can move on to the next example
-                Some(_) => break,
-                None => break 'outer,
-            }
-        }
-    }
-
-    *docs = cleanup_docs(&doc_lines);
-}
-
-fn get_lint_output(lint_name: &str, example: &[&mut String], clippy_project_root: &Path) -> String {
-    let dir = tempfile::tempdir().unwrap_or_else(|e| panic!("failed to create temp dir: {e}"));
-    let file = dir.path().join("lint_example.rs");
-
-    let mut source = String::new();
-    let unhidden = example
-        .iter()
-        .map(|line| line.trim_start().strip_prefix("# ").unwrap_or(line));
-
-    // Get any attributes
-    let mut lines = unhidden.peekable();
-    while let Some(line) = lines.peek() {
-        if line.starts_with("#!") {
-            source.push_str(line);
-            source.push('\n');
-            lines.next();
-        } else {
-            break;
-        }
-    }
-
-    let needs_main = !example.iter().any(|line| line.contains("fn main"));
-    if needs_main {
-        source.push_str("fn main() {\n");
-    }
-
-    for line in lines {
-        source.push_str(line);
-        source.push('\n');
-    }
-
-    if needs_main {
-        source.push_str("}\n");
-    }
-
-    if let Err(e) = fs::write(&file, &source) {
-        panic!("failed to write to `{}`: {e}", file.as_path().to_string_lossy());
-    }
-
-    let prefixed_name = format!("{CLIPPY_LINT_GROUP_PREFIX}{lint_name}");
-
-    let mut cmd = Command::new(env::var("CARGO").unwrap_or("cargo".into()));
-
-    cmd.current_dir(clippy_project_root)
-        .env("CARGO_INCREMENTAL", "0")
-        .env("CLIPPY_ARGS", "")
-        .env("CLIPPY_DISABLE_DOCS_LINKS", "1")
-        // We need to disable this to enable all lints
-        .env("ENABLE_METADATA_COLLECTION", "0")
-        .args(["run", "--bin", "clippy-driver"])
-        .args(["--target-dir", "./clippy_lints/target"])
-        .args(["--", "--error-format=json"])
-        .args(["--edition", "2021"])
-        .arg("-Cdebuginfo=0")
-        .args(["-A", "clippy::all"])
-        .args(["-W", &prefixed_name])
-        .args(["-L", "./target/debug"])
-        .args(["-Z", "no-codegen"]);
-
-    let output = cmd
-        .arg(file.as_path())
-        .output()
-        .unwrap_or_else(|e| panic!("failed to run `{cmd:?}`: {e}"));
-
-    let tmp_file_path = file.to_string_lossy();
-    let stderr = std::str::from_utf8(&output.stderr).unwrap();
-    let msgs = stderr
-        .lines()
-        .filter(|line| line.starts_with('{'))
-        .map(|line| serde_json::from_str(line).unwrap())
-        .collect::<Vec<serde_json::Value>>();
-
-    let mut rendered = String::new();
-    let iter = msgs
-        .iter()
-        .filter(|msg| matches!(&msg["code"]["code"], serde_json::Value::String(s) if s == &prefixed_name));
-
-    for message in iter {
-        let rendered_part = message["rendered"].as_str().expect("rendered field should exist");
-        rendered.push_str(rendered_part);
-    }
-
-    if rendered.is_empty() {
-        let rendered: Vec<&str> = msgs.iter().filter_map(|msg| msg["rendered"].as_str()).collect();
-        let non_json: Vec<&str> = stderr.lines().filter(|line| !line.starts_with('{')).collect();
-        panic!(
-            "did not find lint `{lint_name}` in output of example, got:\n{}\n{}",
-            non_json.join("\n"),
-            rendered.join("\n")
-        );
-    }
-
-    // The reader doesn't need to see `/tmp/.tmpfiy2Qd/lint_example.rs` :)
-    rendered.trim_end().replace(&*tmp_file_path, "lint_example.rs")
-}
-
-#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
-struct SerializableSpan {
-    path: String,
-    line: usize,
-}
-
-impl fmt::Display for SerializableSpan {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}:{}", self.path.rsplit('/').next().unwrap_or_default(), self.line)
-    }
-}
-
-impl SerializableSpan {
-    fn from_item(cx: &LateContext<'_>, item: &Item<'_>) -> Self {
-        Self::from_span(cx, item.ident.span)
-    }
-
-    fn from_span(cx: &LateContext<'_>, span: Span) -> Self {
-        let loc: Loc = cx.sess().source_map().lookup_char_pos(span.lo());
-
-        Self {
-            path: format!("{}", loc.file.name.prefer_remapped_unconditionaly()),
-            line: loc.line,
-        }
-    }
-}
-
-#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
-struct ApplicabilityInfo {
-    /// Indicates if any of the lint emissions uses multiple spans. This is related to
-    /// [rustfix#141](https://github.com/rust-lang/rustfix/issues/141) as such suggestions can
-    /// currently not be applied automatically.
-    is_multi_part_suggestion: bool,
-    applicability: Option<usize>,
-}
-
-impl Serialize for ApplicabilityInfo {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        let mut s = serializer.serialize_struct("ApplicabilityInfo", 2)?;
-        s.serialize_field("is_multi_part_suggestion", &self.is_multi_part_suggestion)?;
-        if let Some(index) = self.applicability {
-            s.serialize_field(
-                "applicability",
-                &paths::APPLICABILITY_VALUES[index][APPLICABILITY_NAME_INDEX],
-            )?;
-        } else {
-            s.serialize_field("applicability", APPLICABILITY_UNRESOLVED_STR)?;
-        }
-        s.end()
-    }
-}
-
-// ==================================================================
-// Lint pass
-// ==================================================================
-impl<'hir> LateLintPass<'hir> for MetadataCollector {
-    /// Collecting lint declarations like:
-    /// ```rust, ignore
-    /// declare_clippy_lint! {
-    ///     /// ### What it does
-    ///     /// Something IDK.
-    ///     pub SOME_LINT,
-    ///     internal,
-    ///     "Who am I?"
-    /// }
-    /// ```
-    fn check_item(&mut self, cx: &LateContext<'hir>, item: &'hir Item<'_>) {
-        if let ItemKind::Static(ty, Mutability::Not, _) = item.kind {
-            // Normal lint
-            if is_lint_ref_type(cx, ty)
-                // item validation
-                // disallow check
-                && let lint_name = sym_to_string(item.ident.name).to_ascii_lowercase()
-                // metadata extraction
-                && let Some((group, level)) = get_lint_group_and_level_or_lint(cx, &lint_name, item)
-                && let Some(mut raw_docs) = extract_attr_docs_or_lint(cx, item)
-            {
-                if let Some(configuration_section) = self.get_lint_configs(&lint_name) {
-                    raw_docs.push_str(&configuration_section);
-                }
-                let version = get_lint_version(cx, item);
-
-                self.lints.push(LintMetadata::new(
-                    lint_name,
-                    SerializableSpan::from_item(cx, item),
-                    group,
-                    level,
-                    version,
-                    raw_docs,
-                ));
-            }
-        }
-    }
-
-    /// Collecting constant applicability from the actual lint emissions
-    ///
-    /// Example:
-    /// ```rust, ignore
-    /// span_lint_and_sugg(
-    ///     cx,
-    ///     SOME_LINT,
-    ///     item.span,
-    ///     "Le lint message",
-    ///     "Here comes help:",
-    ///     "#![allow(clippy::all)]",
-    ///     Applicability::MachineApplicable, // <-- Extracts this constant value
-    /// );
-    /// ```
-    fn check_expr(&mut self, cx: &LateContext<'hir>, expr: &'hir hir::Expr<'_>) {
-        if let Some(args) = match_lint_emission(cx, expr) {
-            let emission_info = extract_emission_info(cx, args);
-            if emission_info.is_empty() {
-                // See:
-                // - src/misc.rs:734:9
-                // - src/methods/mod.rs:3545:13
-                // - src/methods/mod.rs:3496:13
-                // We are basically unable to resolve the lint name itself.
-                return;
-            }
-
-            for (lint_name, applicability, is_multi_part) in emission_info {
-                let app_info = self.applicability_info.entry(lint_name).or_default();
-                app_info.applicability = applicability;
-                app_info.is_multi_part_suggestion = is_multi_part;
-            }
-        }
-    }
-}
-
-// ==================================================================
-// Lint definition extraction
-// ==================================================================
-fn sym_to_string(sym: Symbol) -> String {
-    sym.as_str().to_string()
-}
-
-fn extract_attr_docs_or_lint(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> {
-    extract_attr_docs(cx, item).or_else(|| {
-        lint_collection_error_item(cx, item, "could not collect the lint documentation");
-        None
-    })
-}
-
-/// This function collects all documentation that has been added to an item using
-/// `#[doc = r""]` attributes. Several attributes are aggravated using line breaks
-///
-/// ```ignore
-/// #[doc = r"Hello world!"]
-/// #[doc = r"=^.^="]
-/// struct SomeItem {}
-/// ```
-///
-/// Would result in `Hello world!\n=^.^=\n`
-fn extract_attr_docs(cx: &LateContext<'_>, item: &Item<'_>) -> Option<String> {
-    let attrs = cx.tcx.hir().attrs(item.hir_id());
-    let mut lines = attrs.iter().filter_map(ast::Attribute::doc_str);
-
-    if let Some(line) = lines.next() {
-        let raw_docs = lines.fold(String::from(line.as_str()) + "\n", |s, line| s + line.as_str() + "\n");
-        return Some(raw_docs);
-    }
-
-    None
-}
-
-/// This function may modify the doc comment to ensure that the string can be displayed using a
-/// markdown viewer in Clippy's lint list. The following modifications could be applied:
-/// * Removal of leading space after a new line. (Important to display tables)
-/// * Ensures that code blocks only contain language information
-fn cleanup_docs(docs_collection: &Vec<String>) -> String {
-    let mut in_code_block = false;
-    let mut is_code_block_rust = false;
-
-    let mut docs = String::new();
-    for line in docs_collection {
-        // Rustdoc hides code lines starting with `# ` and this removes them from Clippy's lint list :)
-        if is_code_block_rust && line.trim_start().starts_with("# ") {
-            continue;
-        }
-
-        // The line should be represented in the lint list, even if it's just an empty line
-        docs.push('\n');
-        if let Some(info) = line.trim_start().strip_prefix("```") {
-            in_code_block = !in_code_block;
-            is_code_block_rust = false;
-            if in_code_block {
-                let lang = info
-                    .trim()
-                    .split(',')
-                    // remove rustdoc directives
-                    .find(|&s| !matches!(s, "" | "ignore" | "no_run" | "should_panic" | "compile_fail"))
-                    // if no language is present, fill in "rust"
-                    .unwrap_or("rust");
-                let len_diff = line
-                    .strip_prefix(' ')
-                    .map_or(0, |line| line.len() - line.trim_start().len());
-                if len_diff != 0 {
-                    // We put back the indentation.
-                    docs.push_str(&line[..len_diff]);
-                }
-                docs.push_str("```");
-                docs.push_str(lang);
-
-                is_code_block_rust = lang == "rust";
-                continue;
-            }
-        }
-        // This removes the leading space that the macro translation introduces
-        if let Some(stripped_doc) = line.strip_prefix(' ') {
-            docs.push_str(stripped_doc);
-        } else if !line.is_empty() {
-            docs.push_str(line);
-        }
-    }
-
-    docs
-}
-
-fn get_lint_version(cx: &LateContext<'_>, item: &Item<'_>) -> String {
-    extract_clippy_version_value(cx, item).map_or_else(
-        || VERSION_DEFAULT_STR.to_string(),
-        |version| version.as_str().to_string(),
-    )
-}
-
-fn get_lint_group_and_level_or_lint(
-    cx: &LateContext<'_>,
-    lint_name: &str,
-    item: &Item<'_>,
-) -> Option<(String, &'static str)> {
-    let result = unerased_lint_store(cx.tcx.sess).check_lint_name(
-        lint_name,
-        Some(sym::clippy),
-        &std::iter::once(Ident::with_dummy_span(sym::clippy)).collect(),
-    );
-    if let CheckLintNameResult::Tool(lint_lst, None) = result {
-        if let Some(group) = get_lint_group(cx, lint_lst[0]) {
-            if EXCLUDED_LINT_GROUPS.contains(&group.as_str()) {
-                return None;
-            }
-
-            if let Some(level) = get_lint_level_from_group(&group) {
-                Some((group, level))
-            } else {
-                lint_collection_error_item(
-                    cx,
-                    item,
-                    &format!("Unable to determine lint level for found group `{group}`"),
-                );
-                None
-            }
-        } else {
-            lint_collection_error_item(cx, item, "Unable to determine lint group");
-            None
-        }
-    } else {
-        lint_collection_error_item(cx, item, "Unable to find lint in lint_store");
-        None
-    }
-}
-
-fn get_lint_group(cx: &LateContext<'_>, lint_id: LintId) -> Option<String> {
-    for (group_name, lints, _) in unerased_lint_store(cx.tcx.sess).get_lint_groups() {
-        if IGNORED_LINT_GROUPS.contains(&group_name) {
-            continue;
-        }
-
-        if lints.iter().any(|group_lint| *group_lint == lint_id) {
-            let group = group_name.strip_prefix(CLIPPY_LINT_GROUP_PREFIX).unwrap_or(group_name);
-            return Some((*group).to_string());
-        }
-    }
-
-    None
-}
-
-fn get_lint_level_from_group(lint_group: &str) -> Option<&'static str> {
-    DEFAULT_LINT_LEVELS
-        .iter()
-        .find_map(|(group_name, group_level)| (*group_name == lint_group).then_some(*group_level))
-}
-
-fn collect_renames(lints: &mut Vec<LintMetadata>) {
-    for lint in lints {
-        let mut collected = String::new();
-        let mut names = vec![lint.id.clone()];
-
-        loop {
-            if let Some(lint_name) = names.pop() {
-                for (k, v) in crate::deprecated_lints::RENAMED {
-                    if let Some(name) = v.strip_prefix(CLIPPY_LINT_GROUP_PREFIX)
-                        && name == lint_name
-                        && let Some(past_name) = k.strip_prefix(CLIPPY_LINT_GROUP_PREFIX)
-                    {
-                        lint.former_ids.insert(past_name.to_owned());
-                        writeln!(collected, "* `{past_name}`").unwrap();
-                        names.push(past_name.to_string());
-                    }
-                }
-
-                continue;
-            }
-
-            break;
-        }
-
-        if !collected.is_empty() {
-            write!(
-                &mut lint.docs,
-                r#"
-### Past names
-
-{collected}
-"#
-            )
-            .unwrap();
-        }
-    }
-}
-
-// ==================================================================
-// Lint emission
-// ==================================================================
-fn lint_collection_error_item(cx: &LateContext<'_>, item: &Item<'_>, message: &str) {
-    span_lint(
-        cx,
-        METADATA_COLLECTOR,
-        item.ident.span,
-        format!("metadata collection error for `{}`: {message}", item.ident.name),
-    );
-}
-
-// ==================================================================
-// Applicability
-// ==================================================================
-/// This function checks if a given expression is equal to a simple lint emission function call.
-/// It will return the function arguments if the emission matched any function.
-fn match_lint_emission<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'_>) -> Option<&'hir [hir::Expr<'hir>]> {
-    LINT_EMISSION_FUNCTIONS
-        .iter()
-        .find_map(|emission_fn| match_function_call(cx, expr, emission_fn))
-}
-
-fn take_higher_applicability(a: Option<usize>, b: Option<usize>) -> Option<usize> {
-    a.map_or(b, |a| a.max(b.unwrap_or_default()).into())
-}
-
-fn extract_emission_info<'hir>(
-    cx: &LateContext<'hir>,
-    args: &'hir [hir::Expr<'hir>],
-) -> Vec<(String, Option<usize>, bool)> {
-    let mut lints = Vec::new();
-    let mut applicability = None;
-    let mut multi_part = false;
-
-    for arg in args {
-        let (arg_ty, _) = walk_ptrs_ty_depth(cx.typeck_results().expr_ty(arg));
-
-        if match_type(cx, arg_ty, &paths::LINT) {
-            // If we found the lint arg, extract the lint name
-            let mut resolved_lints = resolve_lints(cx, arg);
-            lints.append(&mut resolved_lints);
-        } else if match_type(cx, arg_ty, &paths::APPLICABILITY) {
-            applicability = resolve_applicability(cx, arg);
-        } else if arg_ty.is_closure() {
-            multi_part |= check_is_multi_part(cx, arg);
-            applicability = applicability.or_else(|| resolve_applicability(cx, arg));
-        }
-    }
-
-    lints
-        .into_iter()
-        .map(|lint_name| (lint_name, applicability, multi_part))
-        .collect()
-}
-
-/// Resolves the possible lints that this expression could reference
-fn resolve_lints<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Vec<String> {
-    let mut resolver = LintResolver::new(cx);
-    resolver.visit_expr(expr);
-    resolver.lints
-}
-
-/// This function tries to resolve the linked applicability to the given expression.
-fn resolve_applicability<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Option<usize> {
-    let mut resolver = ApplicabilityResolver::new(cx);
-    resolver.visit_expr(expr);
-    resolver.complete()
-}
-
-fn check_is_multi_part<'hir>(cx: &LateContext<'hir>, closure_expr: &'hir hir::Expr<'hir>) -> bool {
-    if let ExprKind::Closure(&Closure { body, .. }) = closure_expr.kind {
-        let mut scanner = IsMultiSpanScanner::new(cx);
-        intravisit::walk_body(&mut scanner, cx.tcx.hir().body(body));
-        return scanner.is_multi_part();
-    } else if let Some(local) = get_parent_local(cx, closure_expr) {
-        if let Some(local_init) = local.init {
-            return check_is_multi_part(cx, local_init);
-        }
-    }
-
-    false
-}
-
-struct LintResolver<'a, 'hir> {
-    cx: &'a LateContext<'hir>,
-    lints: Vec<String>,
-}
-
-impl<'a, 'hir> LintResolver<'a, 'hir> {
-    fn new(cx: &'a LateContext<'hir>) -> Self {
-        Self {
-            cx,
-            lints: Vec::<String>::default(),
-        }
-    }
-}
-
-impl<'a, 'hir> Visitor<'hir> for LintResolver<'a, 'hir> {
-    type NestedFilter = nested_filter::All;
-
-    fn nested_visit_map(&mut self) -> Self::Map {
-        self.cx.tcx.hir()
-    }
-
-    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
-        if let ExprKind::Path(qpath) = &expr.kind
-            && let QPath::Resolved(_, path) = qpath
-            && let (expr_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(expr))
-            && match_type(self.cx, expr_ty, &paths::LINT)
-        {
-            if let hir::def::Res::Def(DefKind::Static { .. }, _) = path.res {
-                let lint_name = last_path_segment(qpath).ident.name;
-                self.lints.push(sym_to_string(lint_name).to_ascii_lowercase());
-            } else if let Some(local) = get_parent_local(self.cx, expr) {
-                if let Some(local_init) = local.init {
-                    intravisit::walk_expr(self, local_init);
-                }
-            }
-        }
-
-        intravisit::walk_expr(self, expr);
-    }
-}
-
-/// This visitor finds the highest applicability value in the visited expressions
-struct ApplicabilityResolver<'a, 'hir> {
-    cx: &'a LateContext<'hir>,
-    /// This is the index of highest `Applicability` for `paths::APPLICABILITY_VALUES`
-    applicability_index: Option<usize>,
-}
-
-impl<'a, 'hir> ApplicabilityResolver<'a, 'hir> {
-    fn new(cx: &'a LateContext<'hir>) -> Self {
-        Self {
-            cx,
-            applicability_index: None,
-        }
-    }
-
-    fn add_new_index(&mut self, new_index: usize) {
-        self.applicability_index = take_higher_applicability(self.applicability_index, Some(new_index));
-    }
-
-    fn complete(self) -> Option<usize> {
-        self.applicability_index
-    }
-}
-
-impl<'a, 'hir> Visitor<'hir> for ApplicabilityResolver<'a, 'hir> {
-    type NestedFilter = nested_filter::All;
-
-    fn nested_visit_map(&mut self) -> Self::Map {
-        self.cx.tcx.hir()
-    }
-
-    fn visit_path(&mut self, path: &hir::Path<'hir>, _id: hir::HirId) {
-        for (index, enum_value) in paths::APPLICABILITY_VALUES.iter().enumerate() {
-            if match_path(path, enum_value) {
-                self.add_new_index(index);
-                return;
-            }
-        }
-    }
-
-    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
-        let (expr_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(expr));
-
-        if match_type(self.cx, expr_ty, &paths::APPLICABILITY)
-            && let Some(local) = get_parent_local(self.cx, expr)
-            && let Some(local_init) = local.init
-        {
-            intravisit::walk_expr(self, local_init);
-        };
-
-        intravisit::walk_expr(self, expr);
-    }
-}
-
-/// This returns the parent local node if the expression is a reference one
-fn get_parent_local<'hir>(cx: &LateContext<'hir>, expr: &'hir hir::Expr<'hir>) -> Option<&'hir hir::LetStmt<'hir>> {
-    if let ExprKind::Path(QPath::Resolved(_, path)) = expr.kind {
-        if let hir::def::Res::Local(local_hir) = path.res {
-            return get_parent_local_hir_id(cx, local_hir);
-        }
-    }
-
-    None
-}
-
-fn get_parent_local_hir_id<'hir>(cx: &LateContext<'hir>, hir_id: hir::HirId) -> Option<&'hir hir::LetStmt<'hir>> {
-    match cx.tcx.parent_hir_node(hir_id) {
-        hir::Node::LetStmt(local) => Some(local),
-        hir::Node::Pat(pattern) => get_parent_local_hir_id(cx, pattern.hir_id),
-        _ => None,
-    }
-}
-
-/// This visitor finds the highest applicability value in the visited expressions
-struct IsMultiSpanScanner<'a, 'hir> {
-    cx: &'a LateContext<'hir>,
-    suggestion_count: usize,
-}
-
-impl<'a, 'hir> IsMultiSpanScanner<'a, 'hir> {
-    fn new(cx: &'a LateContext<'hir>) -> Self {
-        Self {
-            cx,
-            suggestion_count: 0,
-        }
-    }
-
-    /// Add a new single expression suggestion to the counter
-    fn add_single_span_suggestion(&mut self) {
-        self.suggestion_count += 1;
-    }
-
-    /// Signals that a suggestion with possible multiple spans was found
-    fn add_multi_part_suggestion(&mut self) {
-        self.suggestion_count += 2;
-    }
-
-    /// Checks if the suggestions include multiple spans
-    fn is_multi_part(&self) -> bool {
-        self.suggestion_count > 1
-    }
-}
-
-impl<'a, 'hir> Visitor<'hir> for IsMultiSpanScanner<'a, 'hir> {
-    type NestedFilter = nested_filter::All;
-
-    fn nested_visit_map(&mut self) -> Self::Map {
-        self.cx.tcx.hir()
-    }
-
-    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
-        // Early return if the lint is already multi span
-        if self.is_multi_part() {
-            return;
-        }
-
-        if let ExprKind::MethodCall(path, recv, _, _arg_span) = &expr.kind {
-            let (self_ty, _) = walk_ptrs_ty_depth(self.cx.typeck_results().expr_ty(recv));
-            if match_type(self.cx, self_ty, &paths::DIAG) {
-                let called_method = path.ident.name.as_str().to_string();
-                for (method_name, is_multi_part) in &SUGGESTION_DIAG_METHODS {
-                    if *method_name == called_method {
-                        if *is_multi_part {
-                            self.add_multi_part_suggestion();
-                        } else {
-                            self.add_single_span_suggestion();
-                        }
-                        break;
-                    }
-                }
-            }
-        }
-
-        intravisit::walk_expr(self, expr);
-    }
-}
diff --git a/declare_clippy_lint/src/lib.rs b/declare_clippy_lint/src/lib.rs
index ca070f6c250..fd366b6b262 100644
--- a/declare_clippy_lint/src/lib.rs
+++ b/declare_clippy_lint/src/lib.rs
@@ -1,4 +1,4 @@
-#![feature(let_chains)]
+#![feature(let_chains, proc_macro_span)]
 // warn on lints, that are included in `rust-lang/rust`s bootstrap
 #![warn(rust_2018_idioms, unused_lifetimes)]
 
@@ -23,6 +23,7 @@ fn parse_attr<const LEN: usize>(path: [&'static str; LEN], attr: &Attribute) ->
 
 struct ClippyLint {
     attrs: Vec<Attribute>,
+    version: Option<LitStr>,
     explanation: String,
     name: Ident,
     category: Ident,
@@ -41,8 +42,14 @@ impl Parse for ClippyLint {
                 let value = lit.value();
                 let line = value.strip_prefix(' ').unwrap_or(&value);
 
-                if line.starts_with("```") {
-                    explanation += "```\n";
+                if let Some(lang) = line.strip_prefix("```") {
+                    let tag = lang.split_once(',').map_or(lang, |(left, _)| left);
+                    if !in_code && matches!(tag, "" | "rust" | "ignore" | "should_panic" | "no_run" | "compile_fail") {
+                        explanation += "```rust\n";
+                    } else {
+                        explanation += line;
+                        explanation.push('\n');
+                    }
                     in_code = !in_code;
                 } else if !(in_code && line.starts_with("# ")) {
                     explanation += line;
@@ -68,6 +75,7 @@ impl Parse for ClippyLint {
 
         Ok(Self {
             attrs,
+            version,
             explanation,
             name,
             category,
@@ -123,6 +131,7 @@ impl Parse for ClippyLint {
 pub fn declare_clippy_lint(input: TokenStream) -> TokenStream {
     let ClippyLint {
         attrs,
+        version,
         explanation,
         name,
         category,
@@ -146,6 +155,14 @@ pub fn declare_clippy_lint(input: TokenStream) -> TokenStream {
     (&mut category[0..1]).make_ascii_uppercase();
     let category_variant = format_ident!("{category}");
 
+    let name_span = name.span().unwrap();
+    let location = format!("{}#{}", name_span.source_file().path().display(), name_span.line());
+
+    let version = match version {
+        Some(version) => quote!(Some(#version)),
+        None => quote!(None),
+    };
+
     let output = quote! {
         rustc_session::declare_tool_lint! {
             #(#attrs)*
@@ -159,6 +176,8 @@ pub fn declare_clippy_lint(input: TokenStream) -> TokenStream {
             lint: &#name,
             category: crate::LintCategory::#category_variant,
             explanation: #explanation,
+            location: #location,
+            version: #version,
         };
     };
 
diff --git a/tests/compile-test.rs b/tests/compile-test.rs
index c7080e5dcdc..9754254cdd0 100644
--- a/tests/compile-test.rs
+++ b/tests/compile-test.rs
@@ -1,22 +1,31 @@
+#![feature(rustc_private, let_chains)]
 #![warn(rust_2018_idioms, unused_lifetimes)]
 #![allow(unused_extern_crates)]
 
+use cargo_metadata::diagnostic::{Applicability, Diagnostic};
+use cargo_metadata::Message;
+use clippy_config::ClippyConfiguration;
+use clippy_lints::declared_lints::LINTS;
+use clippy_lints::deprecated_lints::{DEPRECATED, DEPRECATED_VERSION, RENAMED};
+use clippy_lints::LintInfo;
+use serde::{Deserialize, Serialize};
+use test_utils::IS_RUSTC_TEST_SUITE;
 use ui_test::custom_flags::rustfix::RustfixMode;
+use ui_test::custom_flags::Flag;
 use ui_test::spanned::Spanned;
+use ui_test::test_result::TestRun;
 use ui_test::{status_emitter, Args, CommandBuilder, Config, Match, OutputConflictHandling};
 
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
 use std::env::{self, set_var, var_os};
 use std::ffi::{OsStr, OsString};
-use std::fs;
+use std::fmt::Write;
 use std::path::{Path, PathBuf};
-use std::sync::LazyLock;
-use test_utils::IS_RUSTC_TEST_SUITE;
+use std::sync::mpsc::{channel, Sender};
+use std::{fs, iter, thread};
 
 // Test dependencies may need an `extern crate` here to ensure that they show up
 // in the depinfo file (otherwise cargo thinks they are unused)
-extern crate clippy_lints;
-extern crate clippy_utils;
 extern crate futures;
 extern crate if_chain;
 extern crate itertools;
@@ -52,7 +61,7 @@ static TEST_DEPENDENCIES: &[&str] = &[
 /// dependencies must be added to Cargo.toml at the project root. Test
 /// dependencies that are not *directly* used by this test module require an
 /// `extern crate` declaration.
-static EXTERN_FLAGS: LazyLock<Vec<String>> = LazyLock::new(|| {
+fn extern_flags() -> Vec<String> {
     let current_exe_depinfo = {
         let mut path = env::current_exe().unwrap();
         path.set_extension("d");
@@ -100,70 +109,93 @@ static EXTERN_FLAGS: LazyLock<Vec<String>> = LazyLock::new(|| {
         .into_iter()
         .map(|(name, path)| format!("--extern={name}={path}"))
         .collect()
-});
+}
 
 // whether to run internal tests or not
 const RUN_INTERNAL_TESTS: bool = cfg!(feature = "internal");
 
-fn base_config(test_dir: &str) -> (Config, Args) {
-    let mut args = Args::test().unwrap();
-    args.bless |= var_os("RUSTC_BLESS").is_some_and(|v| v != "0");
-
-    let target_dir = PathBuf::from(var_os("CARGO_TARGET_DIR").unwrap_or_else(|| "target".into()));
-    let mut config = Config {
-        output_conflict_handling: OutputConflictHandling::Error,
-        filter_files: env::var("TESTNAME")
-            .map(|filters| filters.split(',').map(str::to_string).collect())
-            .unwrap_or_default(),
-        target: None,
-        bless_command: Some("cargo uibless".into()),
-        out_dir: target_dir.join("ui_test"),
-        ..Config::rustc(Path::new("tests").join(test_dir))
-    };
-    config.comment_defaults.base().exit_status = None.into();
-    config.comment_defaults.base().require_annotations = None.into();
-    config
-        .comment_defaults
-        .base()
-        .set_custom("rustfix", RustfixMode::Everything);
-    config.comment_defaults.base().diagnostic_code_prefix = Some(Spanned::dummy("clippy::".into())).into();
-    config.with_args(&args);
-    let current_exe_path = env::current_exe().unwrap();
-    let deps_path = current_exe_path.parent().unwrap();
-    let profile_path = deps_path.parent().unwrap();
-
-    config.program.args.extend(
-        [
-            "--emit=metadata",
-            "-Aunused",
-            "-Ainternal_features",
-            "-Zui-testing",
-            "-Zdeduplicate-diagnostics=no",
-            "-Dwarnings",
-            &format!("-Ldependency={}", deps_path.display()),
-        ]
-        .map(OsString::from),
-    );
-
-    config.program.args.extend(EXTERN_FLAGS.iter().map(OsString::from));
-    // Prevent rustc from creating `rustc-ice-*` files the console output is enough.
-    config.program.envs.push(("RUSTC_ICE".into(), Some("0".into())));
+struct TestContext {
+    args: Args,
+    extern_flags: Vec<String>,
+    diagnostic_collector: Option<DiagnosticCollector>,
+    collector_thread: Option<thread::JoinHandle<()>>,
+}
 
-    if let Some(host_libs) = option_env!("HOST_LIBS") {
-        let dep = format!("-Ldependency={}", Path::new(host_libs).join("deps").display());
-        config.program.args.push(dep.into());
+impl TestContext {
+    fn new() -> Self {
+        let mut args = Args::test().unwrap();
+        args.bless |= var_os("RUSTC_BLESS").is_some_and(|v| v != "0");
+        let (diagnostic_collector, collector_thread) = var_os("COLLECT_METADATA")
+            .is_some()
+            .then(DiagnosticCollector::spawn)
+            .unzip();
+        Self {
+            args,
+            extern_flags: extern_flags(),
+            diagnostic_collector,
+            collector_thread,
+        }
     }
 
-    config.program.program = profile_path.join(if cfg!(windows) {
-        "clippy-driver.exe"
-    } else {
-        "clippy-driver"
-    });
-    (config, args)
+    fn base_config(&self, test_dir: &str) -> Config {
+        let target_dir = PathBuf::from(var_os("CARGO_TARGET_DIR").unwrap_or_else(|| "target".into()));
+        let mut config = Config {
+            output_conflict_handling: OutputConflictHandling::Error,
+            filter_files: env::var("TESTNAME")
+                .map(|filters| filters.split(',').map(str::to_string).collect())
+                .unwrap_or_default(),
+            target: None,
+            bless_command: Some("cargo uibless".into()),
+            out_dir: target_dir.join("ui_test"),
+            ..Config::rustc(Path::new("tests").join(test_dir))
+        };
+        let defaults = config.comment_defaults.base();
+        defaults.exit_status = None.into();
+        defaults.require_annotations = None.into();
+        defaults.diagnostic_code_prefix = Some(Spanned::dummy("clippy::".into())).into();
+        defaults.set_custom("rustfix", RustfixMode::Everything);
+        if let Some(collector) = self.diagnostic_collector.clone() {
+            defaults.set_custom("diagnostic-collector", collector);
+        }
+        config.with_args(&self.args);
+        let current_exe_path = env::current_exe().unwrap();
+        let deps_path = current_exe_path.parent().unwrap();
+        let profile_path = deps_path.parent().unwrap();
+
+        config.program.args.extend(
+            [
+                "--emit=metadata",
+                "-Aunused",
+                "-Ainternal_features",
+                "-Zui-testing",
+                "-Zdeduplicate-diagnostics=no",
+                "-Dwarnings",
+                &format!("-Ldependency={}", deps_path.display()),
+            ]
+            .map(OsString::from),
+        );
+
+        config.program.args.extend(self.extern_flags.iter().map(OsString::from));
+        // Prevent rustc from creating `rustc-ice-*` files the console output is enough.
+        config.program.envs.push(("RUSTC_ICE".into(), Some("0".into())));
+
+        if let Some(host_libs) = option_env!("HOST_LIBS") {
+            let dep = format!("-Ldependency={}", Path::new(host_libs).join("deps").display());
+            config.program.args.push(dep.into());
+        }
+
+        config.program.program = profile_path.join(if cfg!(windows) {
+            "clippy-driver.exe"
+        } else {
+            "clippy-driver"
+        });
+
+        config
+    }
 }
 
-fn run_ui() {
-    let (mut config, args) = base_config("ui");
+fn run_ui(cx: &TestContext) {
+    let mut config = cx.base_config("ui");
     config
         .program
         .envs
@@ -173,30 +205,29 @@ fn run_ui() {
         vec![config],
         ui_test::default_file_filter,
         ui_test::default_per_file_config,
-        status_emitter::Text::from(args.format),
+        status_emitter::Text::from(cx.args.format),
     )
     .unwrap();
 }
 
-fn run_internal_tests() {
-    // only run internal tests with the internal-tests feature
+fn run_internal_tests(cx: &TestContext) {
     if !RUN_INTERNAL_TESTS {
         return;
     }
-    let (mut config, args) = base_config("ui-internal");
+    let mut config = cx.base_config("ui-internal");
     config.bless_command = Some("cargo uitest --features internal -- -- --bless".into());
 
     ui_test::run_tests_generic(
         vec![config],
         ui_test::default_file_filter,
         ui_test::default_per_file_config,
-        status_emitter::Text::from(args.format),
+        status_emitter::Text::from(cx.args.format),
     )
     .unwrap();
 }
 
-fn run_ui_toml() {
-    let (mut config, args) = base_config("ui-toml");
+fn run_ui_toml(cx: &TestContext) {
+    let mut config = cx.base_config("ui-toml");
 
     config
         .comment_defaults
@@ -214,19 +245,19 @@ fn run_ui_toml() {
                 .envs
                 .push(("CLIPPY_CONF_DIR".into(), Some(path.parent().unwrap().into())));
         },
-        status_emitter::Text::from(args.format),
+        status_emitter::Text::from(cx.args.format),
     )
     .unwrap();
 }
 
 // Allow `Default::default` as `OptWithSpan` is not nameable
 #[allow(clippy::default_trait_access)]
-fn run_ui_cargo() {
+fn run_ui_cargo(cx: &TestContext) {
     if IS_RUSTC_TEST_SUITE {
         return;
     }
 
-    let (mut config, args) = base_config("ui-cargo");
+    let mut config = cx.base_config("ui-cargo");
     config.program.input_file_flag = CommandBuilder::cargo().input_file_flag;
     config.program.out_dir_flag = CommandBuilder::cargo().out_dir_flag;
     config.program.args = vec!["clippy".into(), "--color".into(), "never".into(), "--quiet".into()];
@@ -261,23 +292,25 @@ fn run_ui_cargo() {
                 .then(|| ui_test::default_any_file_filter(path, config) && !ignored_32bit(path))
         },
         |_config, _file_contents| {},
-        status_emitter::Text::from(args.format),
+        status_emitter::Text::from(cx.args.format),
     )
     .unwrap();
 }
 
 fn main() {
     set_var("CLIPPY_DISABLE_DOCS_LINKS", "true");
+
+    let cx = TestContext::new();
+
     // The SPEEDTEST_* env variables can be used to check Clippy's performance on your PR. It runs the
     // affected test 1000 times and gets the average.
     if let Ok(speedtest) = std::env::var("SPEEDTEST") {
         println!("----------- STARTING SPEEDTEST -----------");
         let f = match speedtest.as_str() {
-            "ui" => run_ui as fn(),
-            "cargo" => run_ui_cargo as fn(),
-            "toml" => run_ui_toml as fn(),
-            "internal" => run_internal_tests as fn(),
-            "ui-cargo-toml-metadata" => ui_cargo_toml_metadata as fn(),
+            "ui" => run_ui,
+            "cargo" => run_ui_cargo,
+            "toml" => run_ui_toml,
+            "internal" => run_internal_tests,
 
             _ => panic!("unknown speedtest: {speedtest} || accepted speedtests are: [ui, cargo, toml, internal]"),
         };
@@ -294,7 +327,7 @@ fn main() {
         let mut sum = 0;
         for _ in 0..iterations {
             let start = std::time::Instant::now();
-            f();
+            f(&cx);
             sum += start.elapsed().as_millis();
         }
         println!(
@@ -303,11 +336,17 @@ fn main() {
             sum / u128::from(iterations)
         );
     } else {
-        run_ui();
-        run_ui_toml();
-        run_ui_cargo();
-        run_internal_tests();
+        run_ui(&cx);
+        run_ui_toml(&cx);
+        run_ui_cargo(&cx);
+        run_internal_tests(&cx);
+        drop(cx.diagnostic_collector);
+
         ui_cargo_toml_metadata();
+
+        if let Some(thread) = cx.collector_thread {
+            thread.join().unwrap();
+        }
     }
 }
 
@@ -346,3 +385,180 @@ fn ui_cargo_toml_metadata() {
         );
     }
 }
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum DiagnosticOrMessage {
+    Diagnostic(Diagnostic),
+    Message(Message),
+}
+
+/// Collects applicabilities from the diagnostics produced for each UI test, producing the
+/// `util/gh-pages/lints.json` file used by <https://rust-lang.github.io/rust-clippy/>
+#[derive(Debug, Clone)]
+struct DiagnosticCollector {
+    sender: Sender<Vec<u8>>,
+}
+
+impl DiagnosticCollector {
+    #[allow(clippy::assertions_on_constants)]
+    fn spawn() -> (Self, thread::JoinHandle<()>) {
+        assert!(!IS_RUSTC_TEST_SUITE && !RUN_INTERNAL_TESTS);
+
+        let (sender, receiver) = channel::<Vec<u8>>();
+
+        let handle = thread::spawn(|| {
+            let mut applicabilities = HashMap::new();
+
+            for stderr in receiver {
+                for line in stderr.split(|&byte| byte == b'\n') {
+                    let diag = match serde_json::from_slice(line) {
+                        Ok(DiagnosticOrMessage::Diagnostic(diag)) => diag,
+                        Ok(DiagnosticOrMessage::Message(Message::CompilerMessage(message))) => message.message,
+                        _ => continue,
+                    };
+
+                    if let Some(lint) = diag.code.as_ref().and_then(|code| code.code.strip_prefix("clippy::")) {
+                        let applicability = applicabilities
+                            .entry(lint.to_string())
+                            .or_insert(Applicability::Unspecified);
+                        let diag_applicability = diag
+                            .children
+                            .iter()
+                            .flat_map(|child| &child.spans)
+                            .filter_map(|span| span.suggestion_applicability.clone())
+                            .max_by_key(applicability_ord);
+                        if let Some(diag_applicability) = diag_applicability
+                            && applicability_ord(&diag_applicability) > applicability_ord(applicability)
+                        {
+                            *applicability = diag_applicability;
+                        }
+                    }
+                }
+            }
+
+            let configs = clippy_config::get_configuration_metadata();
+            let mut metadata: Vec<LintMetadata> = LINTS
+                .iter()
+                .map(|lint| LintMetadata::new(lint, &applicabilities, &configs))
+                .chain(
+                    iter::zip(DEPRECATED, DEPRECATED_VERSION)
+                        .map(|((lint, reason), version)| LintMetadata::new_deprecated(lint, reason, version)),
+                )
+                .collect();
+            metadata.sort_unstable_by(|a, b| a.id.cmp(&b.id));
+
+            let json = serde_json::to_string_pretty(&metadata).unwrap();
+            fs::write("util/gh-pages/lints.json", json).unwrap();
+        });
+
+        (Self { sender }, handle)
+    }
+}
+
+fn applicability_ord(applicability: &Applicability) -> u8 {
+    match applicability {
+        Applicability::MachineApplicable => 4,
+        Applicability::HasPlaceholders => 3,
+        Applicability::MaybeIncorrect => 2,
+        Applicability::Unspecified => 1,
+        _ => unimplemented!(),
+    }
+}
+
+impl Flag for DiagnosticCollector {
+    fn post_test_action(
+        &self,
+        _config: &ui_test::per_test_config::TestConfig<'_>,
+        _cmd: &mut std::process::Command,
+        output: &std::process::Output,
+        _build_manager: &ui_test::build_manager::BuildManager<'_>,
+    ) -> Result<Vec<TestRun>, ui_test::Errored> {
+        if !output.stderr.is_empty() {
+            self.sender.send(output.stderr.clone()).unwrap();
+        }
+        Ok(Vec::new())
+    }
+
+    fn clone_inner(&self) -> Box<dyn Flag> {
+        Box::new(self.clone())
+    }
+
+    fn must_be_unique(&self) -> bool {
+        true
+    }
+}
+
+#[derive(Debug, Serialize)]
+struct LintMetadata {
+    id: String,
+    id_location: Option<&'static str>,
+    group: &'static str,
+    level: &'static str,
+    docs: String,
+    version: &'static str,
+    applicability: Applicability,
+}
+
+impl LintMetadata {
+    fn new(lint: &LintInfo, applicabilities: &HashMap<String, Applicability>, configs: &[ClippyConfiguration]) -> Self {
+        let name = lint.name_lower();
+        let applicability = applicabilities
+            .get(&name)
+            .cloned()
+            .unwrap_or(Applicability::Unspecified);
+        let past_names = RENAMED
+            .iter()
+            .filter(|(_, new_name)| new_name.strip_prefix("clippy::") == Some(&name))
+            .map(|(old_name, _)| old_name.strip_prefix("clippy::").unwrap())
+            .collect::<Vec<_>>();
+        let mut docs = lint.explanation.to_string();
+        if !past_names.is_empty() {
+            docs.push_str("\n### Past names\n\n");
+            for past_name in past_names {
+                writeln!(&mut docs, " * {past_name}").unwrap();
+            }
+        }
+        let configs: Vec<_> = configs
+            .iter()
+            .filter(|conf| conf.lints.contains(&name.as_str()))
+            .collect();
+        if !configs.is_empty() {
+            docs.push_str("\n### Configuration\n\n");
+            for config in configs {
+                writeln!(&mut docs, "{config}").unwrap();
+            }
+        }
+        Self {
+            id: name,
+            id_location: Some(lint.location),
+            group: lint.category_str(),
+            level: lint.lint.default_level.as_str(),
+            docs,
+            version: lint.version.unwrap(),
+            applicability,
+        }
+    }
+
+    fn new_deprecated(name: &str, reason: &str, version: &'static str) -> Self {
+        // The reason starts with a lowercase letter and ends without a period.
+        // This needs to be fixed for the website.
+        let mut reason = reason.to_owned();
+        if let Some(reason) = reason.get_mut(0..1) {
+            reason.make_ascii_uppercase();
+        }
+        Self {
+            id: name.strip_prefix("clippy::").unwrap().into(),
+            id_location: None,
+            group: "deprecated",
+            level: "none",
+            version,
+            docs: format!(
+                "### What it does\n\n\
+                Nothing. This lint has been deprecated\n\n\
+                ### Deprecation reason\n\n{reason}.\n",
+            ),
+            applicability: Applicability::Unspecified,
+        }
+    }
+}
diff --git a/tests/config-metadata.rs b/tests/config-metadata.rs
new file mode 100644
index 00000000000..c0b04827060
--- /dev/null
+++ b/tests/config-metadata.rs
@@ -0,0 +1,76 @@
+use clippy_config::{get_configuration_metadata, ClippyConfiguration};
+use itertools::Itertools;
+use regex::Regex;
+use std::borrow::Cow;
+use std::{env, fs};
+
+fn metadata() -> impl Iterator<Item = ClippyConfiguration> {
+    get_configuration_metadata()
+        .into_iter()
+        .filter(|config| config.deprecation_reason.is_none())
+        .filter(|config| !config.lints.is_empty())
+}
+
+#[test]
+fn book() {
+    let path = "book/src/lint_configuration.md";
+    let current = fs::read_to_string(path).unwrap();
+
+    let configs = metadata().map(|conf| conf.to_markdown_paragraph()).join("\n");
+    let expected = format!(
+        r#"<!--
+This file is generated by `cargo bless --test config-metadata`.
+Please use that command to update the file and do not edit it by hand.
+-->
+
+# Lint Configuration Options
+
+The following list shows each configuration option, along with a description, its default value, an example
+and lints affected.
+
+---
+
+{}
+"#,
+        configs.trim(),
+    );
+
+    if current != expected {
+        if env::var_os("RUSTC_BLESS").is_some_and(|v| v != "0") {
+            fs::write(path, expected).unwrap();
+        } else {
+            panic!("`{path}` is out of date, run `cargo bless --test config-metadata` to update it");
+        }
+    }
+}
+
+#[test]
+fn changelog() {
+    let path = "CHANGELOG.md";
+    let current = fs::read_to_string(path).unwrap();
+
+    let configs = metadata().map(|conf| conf.to_markdown_link()).join("\n");
+
+    let re = Regex::new(
+        "(?s)\
+        (<!-- begin autogenerated links to configuration documentation -->)\
+        .*\
+        (<!-- end autogenerated links to configuration documentation -->)\
+        ",
+    )
+    .unwrap();
+    let expected = re.replace(&current, format!("$1\n{configs}\n$2"));
+
+    assert!(
+        matches!(expected, Cow::Owned(_)),
+        "failed to find configuration section in `{path}`"
+    );
+
+    if current != expected {
+        if env::var_os("RUSTC_BLESS").is_some_and(|v| v != "0") {
+            fs::write(path, expected.as_bytes()).unwrap();
+        } else {
+            panic!("`{path}` is out of date, run `cargo bless --test config-metadata` to update it");
+        }
+    }
+}
diff --git a/util/gh-pages/index.html b/util/gh-pages/index.html
index dd58fa2282c..f3d7e504fdf 100644
--- a/util/gh-pages/index.html
+++ b/util/gh-pages/index.html
@@ -239,7 +239,7 @@ Otherwise, have a great day =^.^=
                         <!-- Applicability -->
                         <div class="lint-additional-info-item">
                             <span> Applicability: </span>
-                            <span class="label label-default label-applicability">{{lint.applicability.applicability}}</span>
+                            <span class="label label-default label-applicability">{{lint.applicability}}</span>
                             <a href="https://doc.rust-lang.org/nightly/nightly-rustc/rustc_lint_defs/enum.Applicability.html#variants">(?)</a>
                         </div>
                         <!-- Clippy version -->
@@ -252,8 +252,8 @@ Otherwise, have a great day =^.^=
                             <a href="https://github.com/rust-lang/rust-clippy/issues?q=is%3Aissue+{{lint.id}}">Related Issues</a>
                         </div>
                         <!-- Jump to source -->
-                        <div class="lint-additional-info-item" ng-if="lint.id_span">
-                            <a href="https://github.com/rust-lang/rust-clippy/blob/{{docVersion}}/clippy_lints/{{lint.id_span.path}}#L{{lint.id_span.line}}">View Source</a>
+                        <div class="lint-additional-info-item" ng-if="lint.id_location">
+                            <a href="https://github.com/rust-lang/rust-clippy/blob/{{docVersion}}/{{lint.id_location}}">View Source</a>
                         </div>
                     </div>
                 </div>
diff --git a/util/gh-pages/script.js b/util/gh-pages/script.js
index d54b5e851c7..1a5330bc0e5 100644
--- a/util/gh-pages/script.js
+++ b/util/gh-pages/script.js
@@ -96,13 +96,13 @@
                 cargo: true,
                 complexity: true,
                 correctness: true,
-                deprecated: false,
                 nursery: true,
                 pedantic: true,
                 perf: true,
                 restriction: true,
                 style: true,
                 suspicious: true,
+                deprecated: false,
             }
 
             $scope.groups = {
@@ -126,11 +126,10 @@
             );
 
             const APPLICABILITIES_FILTER_DEFAULT = {
-                Unspecified: true,
-                Unresolved: true,
                 MachineApplicable: true,
                 MaybeIncorrect: true,
-                HasPlaceholders: true
+                HasPlaceholders: true,
+                Unspecified: true,
             };
 
             $scope.applicabilities = {
@@ -425,7 +424,7 @@
             }
 
             $scope.byApplicabilities = function (lint) {
-                return $scope.applicabilities[lint.applicability.applicability];
+                return $scope.applicabilities[lint.applicability];
             };
 
             // Show details for one lint