about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJason Newcomb <jsnewcomb@pm.me>2025-04-23 20:04:35 -0400
committerJason Newcomb <jsnewcomb@pm.me>2025-06-18 22:37:23 -0400
commitacd8810e77fd3ee8362ce70ce195ae7c8bd08f25 (patch)
tree1f6d9e0c895f3c955391eff0d4921629da6808cb
parent19c1c709054ea1964d942259c5c33ad6489cd1e0 (diff)
downloadrust-acd8810e77fd3ee8362ce70ce195ae7c8bd08f25.tar.gz
rust-acd8810e77fd3ee8362ce70ce195ae7c8bd08f25.zip
Prepare to split lints into multiple crates
* Move `declare_clippy_lint` to it's own crate
* Move lint/group registration into the driver
* Make `dev update_lints` handle multiple lint crates
-rw-r--r--Cargo.toml1
-rw-r--r--clippy_dev/src/release.rs1
-rw-r--r--clippy_dev/src/update_lints.rs282
-rw-r--r--clippy_dev/src/utils.rs15
-rw-r--r--clippy_lints/Cargo.toml1
-rw-r--r--clippy_lints/src/declare_clippy_lint.rs168
-rw-r--r--clippy_lints/src/declared_lints.rs2
-rw-r--r--clippy_lints/src/floating_point_arithmetic.rs3
-rw-r--r--clippy_lints/src/lib.rs126
-rw-r--r--clippy_lints/src/manual_let_else.rs1
-rw-r--r--clippy_lints/src/needless_parens_on_range_literals.rs1
-rw-r--r--clippy_lints/src/question_mark_used.rs1
-rw-r--r--clippy_lints/src/read_zero_byte_vec.rs3
-rw-r--r--declare_clippy_lint/Cargo.toml10
-rw-r--r--declare_clippy_lint/src/lib.rs280
-rw-r--r--src/driver.rs8
-rw-r--r--tests/compile-test.rs6
-rw-r--r--tests/dogfood.rs1
-rw-r--r--tests/versioncheck.rs1
19 files changed, 480 insertions, 431 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 5584ded0a4a..8cd648cf9f0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,6 +24,7 @@ path = "src/driver.rs"
 clippy_config = { path = "clippy_config" }
 clippy_lints = { path = "clippy_lints" }
 clippy_utils = { path = "clippy_utils" }
+declare_clippy_lint = { path = "declare_clippy_lint" }
 rustc_tools_util = { path = "rustc_tools_util", version = "0.4.2" }
 clippy_lints_internal = { path = "clippy_lints_internal", optional = true }
 tempfile = { version = "3.20", optional = true }
diff --git a/clippy_dev/src/release.rs b/clippy_dev/src/release.rs
index 62c1bee8185..15392dd1d29 100644
--- a/clippy_dev/src/release.rs
+++ b/clippy_dev/src/release.rs
@@ -5,6 +5,7 @@ static CARGO_TOML_FILES: &[&str] = &[
     "clippy_config/Cargo.toml",
     "clippy_lints/Cargo.toml",
     "clippy_utils/Cargo.toml",
+    "declare_clippy_lint/Cargo.toml",
     "Cargo.toml",
 ];
 
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index 08592f2521f..6dbee33ffca 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -4,8 +4,9 @@ use crate::utils::{
 use itertools::Itertools;
 use std::collections::HashSet;
 use std::fmt::Write;
+use std::fs;
 use std::ops::Range;
-use std::path::{Path, PathBuf};
+use std::path::{self, Path, PathBuf};
 use walkdir::{DirEntry, WalkDir};
 
 const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
@@ -36,123 +37,164 @@ pub fn generate_lint_files(
     deprecated: &[DeprecatedLint],
     renamed: &[RenamedLint],
 ) {
-    FileUpdater::default().update_files_checked(
+    let mut updater = FileUpdater::default();
+    updater.update_file_checked(
         "cargo dev update_lints",
         update_mode,
-        &mut [
-            (
-                "README.md",
-                &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
-                    write!(dst, "{}", round_to_fifty(lints.len())).unwrap();
-                }),
-            ),
-            (
-                "book/src/README.md",
-                &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
-                    write!(dst, "{}", round_to_fifty(lints.len())).unwrap();
-                }),
-            ),
-            (
-                "CHANGELOG.md",
-                &mut update_text_region_fn(
-                    "<!-- begin autogenerated links to lint list -->\n",
-                    "<!-- end autogenerated links to lint list -->",
-                    |dst| {
-                        for lint in lints
-                            .iter()
-                            .map(|l| &*l.name)
-                            .chain(deprecated.iter().filter_map(|l| l.name.strip_prefix("clippy::")))
-                            .chain(renamed.iter().filter_map(|l| l.old_name.strip_prefix("clippy::")))
-                            .sorted()
-                        {
-                            writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap();
-                        }
-                    },
-                ),
-            ),
-            (
-                "clippy_lints/src/lib.rs",
-                &mut update_text_region_fn(
-                    "// begin lints modules, do not remove this comment, it's used in `update_lints`\n",
-                    "// end lints modules, do not remove this comment, it's used in `update_lints`",
-                    |dst| {
-                        for lint_mod in lints.iter().map(|l| &l.module).sorted().dedup() {
-                            writeln!(dst, "mod {lint_mod};").unwrap();
-                        }
-                    },
-                ),
-            ),
-            ("clippy_lints/src/declared_lints.rs", &mut |_, src, dst| {
-                dst.push_str(GENERATED_FILE_COMMENT);
-                dst.push_str("pub static LINTS: &[&crate::LintInfo] = &[\n");
-                for (module_name, lint_name) in lints.iter().map(|l| (&l.module, l.name.to_uppercase())).sorted() {
-                    writeln!(dst, "    crate::{module_name}::{lint_name}_INFO,").unwrap();
-                }
-                dst.push_str("];\n");
-                UpdateStatus::from_changed(src != dst)
-            }),
-            ("clippy_lints/src/deprecated_lints.rs", &mut |_, src, dst| {
-                let mut searcher = RustSearcher::new(src);
-                assert!(
-                    searcher.find_token(Token::Ident("declare_with_version"))
-                        && searcher.find_token(Token::Ident("declare_with_version")),
-                    "error reading deprecated lints"
-                );
-                dst.push_str(&src[..searcher.pos() as usize]);
-                dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n");
-                for lint in deprecated {
-                    write!(
-                        dst,
-                        "    #[clippy::version = \"{}\"]\n    (\"{}\", \"{}\"),\n",
-                        lint.version, lint.name, lint.reason,
-                    )
-                    .unwrap();
+        "README.md",
+        &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
+            write!(dst, "{}", round_to_fifty(lints.len())).unwrap();
+        }),
+    );
+    updater.update_file_checked(
+        "cargo dev update_lints",
+        update_mode,
+        "book/src/README.md",
+        &mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
+            write!(dst, "{}", round_to_fifty(lints.len())).unwrap();
+        }),
+    );
+    updater.update_file_checked(
+        "cargo dev update_lints",
+        update_mode,
+        "CHANGELOG.md",
+        &mut update_text_region_fn(
+            "<!-- begin autogenerated links to lint list -->\n",
+            "<!-- end autogenerated links to lint list -->",
+            |dst| {
+                for lint in lints
+                    .iter()
+                    .map(|l| &*l.name)
+                    .chain(deprecated.iter().filter_map(|l| l.name.strip_prefix("clippy::")))
+                    .chain(renamed.iter().filter_map(|l| l.old_name.strip_prefix("clippy::")))
+                    .sorted()
+                {
+                    writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap();
                 }
-                dst.push_str(
-                    "]}\n\n\
+            },
+        ),
+    );
+    updater.update_file_checked(
+        "cargo dev update_lints",
+        update_mode,
+        "clippy_lints/src/deprecated_lints.rs",
+        &mut |_, src, dst| {
+            let mut searcher = RustSearcher::new(src);
+            assert!(
+                searcher.find_token(Token::Ident("declare_with_version"))
+                    && searcher.find_token(Token::Ident("declare_with_version")),
+                "error reading deprecated lints"
+            );
+            dst.push_str(&src[..searcher.pos() as usize]);
+            dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n");
+            for lint in deprecated {
+                write!(
+                    dst,
+                    "    #[clippy::version = \"{}\"]\n    (\"{}\", \"{}\"),\n",
+                    lint.version, lint.name, lint.reason,
+                )
+                .unwrap();
+            }
+            dst.push_str(
+                "]}\n\n\
                     #[rustfmt::skip]\n\
                     declare_with_version! { RENAMED(RENAMED_VERSION) = [\n\
                 ",
-                );
-                for lint in renamed {
-                    write!(
-                        dst,
-                        "    #[clippy::version = \"{}\"]\n    (\"{}\", \"{}\"),\n",
-                        lint.version, lint.old_name, lint.new_name,
-                    )
-                    .unwrap();
+            );
+            for lint in renamed {
+                write!(
+                    dst,
+                    "    #[clippy::version = \"{}\"]\n    (\"{}\", \"{}\"),\n",
+                    lint.version, lint.old_name, lint.new_name,
+                )
+                .unwrap();
+            }
+            dst.push_str("]}\n");
+            UpdateStatus::from_changed(src != dst)
+        },
+    );
+    updater.update_file_checked(
+        "cargo dev update_lints",
+        update_mode,
+        "tests/ui/deprecated.rs",
+        &mut |_, src, dst| {
+            dst.push_str(GENERATED_FILE_COMMENT);
+            for lint in deprecated {
+                writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.name, lint.name).unwrap();
+            }
+            dst.push_str("\nfn main() {}\n");
+            UpdateStatus::from_changed(src != dst)
+        },
+    );
+    updater.update_file_checked(
+        "cargo dev update_lints",
+        update_mode,
+        "tests/ui/rename.rs",
+        &mut move |_, src, dst| {
+            let mut seen_lints = HashSet::new();
+            dst.push_str(GENERATED_FILE_COMMENT);
+            dst.push_str("#![allow(clippy::duplicated_attributes)]\n");
+            for lint in renamed {
+                if seen_lints.insert(&lint.new_name) {
+                    writeln!(dst, "#![allow({})]", lint.new_name).unwrap();
                 }
-                dst.push_str("]}\n");
-                UpdateStatus::from_changed(src != dst)
-            }),
-            ("tests/ui/deprecated.rs", &mut |_, src, dst| {
-                dst.push_str(GENERATED_FILE_COMMENT);
-                for lint in deprecated {
-                    writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.name, lint.name).unwrap();
+            }
+            seen_lints.clear();
+            for lint in renamed {
+                if seen_lints.insert(&lint.old_name) {
+                    writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap();
                 }
-                dst.push_str("\nfn main() {}\n");
-                UpdateStatus::from_changed(src != dst)
-            }),
-            ("tests/ui/rename.rs", &mut move |_, src, dst| {
-                let mut seen_lints = HashSet::new();
-                dst.push_str(GENERATED_FILE_COMMENT);
-                dst.push_str("#![allow(clippy::duplicated_attributes)]\n");
-                for lint in renamed {
-                    if seen_lints.insert(&lint.new_name) {
-                        writeln!(dst, "#![allow({})]", lint.new_name).unwrap();
+            }
+            dst.push_str("\nfn main() {}\n");
+            UpdateStatus::from_changed(src != dst)
+        },
+    );
+    for (crate_name, lints) in lints.iter().into_group_map_by(|&l| {
+        let Some(path::Component::Normal(name)) = l.path.components().next() else {
+            // All paths should start with `{crate_name}/src` when parsed from `find_lint_decls`
+            panic!("internal error: can't read crate name from path `{}`", l.path.display());
+        };
+        name
+    }) {
+        updater.update_file_checked(
+            "cargo dev update_lints",
+            update_mode,
+            Path::new(crate_name).join("src/lib.rs"),
+            &mut update_text_region_fn(
+                "// begin lints modules, do not remove this comment, it's used in `update_lints`\n",
+                "// end lints modules, do not remove this comment, it's used in `update_lints`",
+                |dst| {
+                    for lint_mod in lints
+                        .iter()
+                        .filter(|l| !l.module.is_empty())
+                        .map(|l| l.module.split_once("::").map_or(&*l.module, |x| x.0))
+                        .sorted()
+                        .dedup()
+                    {
+                        writeln!(dst, "mod {lint_mod};").unwrap();
                     }
-                }
-                seen_lints.clear();
-                for lint in renamed {
-                    if seen_lints.insert(&lint.old_name) {
-                        writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap();
+                },
+            ),
+        );
+        updater.update_file_checked(
+            "cargo dev update_lints",
+            update_mode,
+            Path::new(crate_name).join("src/declared_lints.rs"),
+            &mut |_, src, dst| {
+                dst.push_str(GENERATED_FILE_COMMENT);
+                dst.push_str("pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[\n");
+                for (module_path, lint_name) in lints.iter().map(|l| (&l.module, l.name.to_uppercase())).sorted() {
+                    if module_path.is_empty() {
+                        writeln!(dst, "    crate::{lint_name}_INFO,").unwrap();
+                    } else {
+                        writeln!(dst, "    crate::{module_path}::{lint_name}_INFO,").unwrap();
                     }
                 }
-                dst.push_str("\nfn main() {}\n");
+                dst.push_str("];\n");
                 UpdateStatus::from_changed(src != dst)
-            }),
-        ],
-    );
+            },
+        );
+    }
 }
 
 fn round_to_fifty(count: usize) -> usize {
@@ -186,13 +228,25 @@ pub struct RenamedLint {
 pub fn find_lint_decls() -> Vec<Lint> {
     let mut lints = Vec::with_capacity(1000);
     let mut contents = String::new();
-    for (file, module) in read_src_with_module("clippy_lints/src".as_ref()) {
-        parse_clippy_lint_decls(
-            file.path(),
-            File::open_read_to_cleared_string(file.path(), &mut contents),
-            &module,
-            &mut lints,
-        );
+    for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") {
+        let e = expect_action(e, ErrAction::Read, ".");
+        if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() {
+            continue;
+        }
+        let Ok(mut name) = e.file_name().into_string() else {
+            continue;
+        };
+        if name.starts_with("clippy_lints") && name != "clippy_lints_internal" {
+            name.push_str("/src");
+            for (file, module) in read_src_with_module(name.as_ref()) {
+                parse_clippy_lint_decls(
+                    file.path(),
+                    File::open_read_to_cleared_string(file.path(), &mut contents),
+                    &module,
+                    &mut lints,
+                );
+            }
+        }
     }
     lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
     lints
@@ -204,7 +258,7 @@ fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator<Item = (DirE
         let e = expect_action(e, ErrAction::Read, src_root);
         let path = e.path().as_os_str().as_encoded_bytes();
         if let Some(path) = path.strip_suffix(b".rs")
-            && let Some(path) = path.get("clippy_lints/src/".len()..)
+            && let Some(path) = path.get(src_root.as_os_str().len() + 1..)
         {
             if path == b"lib" {
                 Some((e, String::new()))
diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs
index c4808b7048b..89962a11034 100644
--- a/clippy_dev/src/utils.rs
+++ b/clippy_dev/src/utils.rs
@@ -383,21 +383,6 @@ impl FileUpdater {
         self.update_file_checked_inner(tool, mode, path.as_ref(), update);
     }
 
-    #[expect(clippy::type_complexity)]
-    pub fn update_files_checked(
-        &mut self,
-        tool: &str,
-        mode: UpdateMode,
-        files: &mut [(
-            impl AsRef<Path>,
-            &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus,
-        )],
-    ) {
-        for (path, update) in files {
-            self.update_file_checked_inner(tool, mode, path.as_ref(), update);
-        }
-    }
-
     pub fn update_file(
         &mut self,
         path: impl AsRef<Path>,
diff --git a/clippy_lints/Cargo.toml b/clippy_lints/Cargo.toml
index 39e4e2e365e..37b554fd0d8 100644
--- a/clippy_lints/Cargo.toml
+++ b/clippy_lints/Cargo.toml
@@ -13,6 +13,7 @@ arrayvec = { version = "0.7", default-features = false }
 cargo_metadata = "0.18"
 clippy_config = { path = "../clippy_config" }
 clippy_utils = { path = "../clippy_utils" }
+declare_clippy_lint = { path = "../declare_clippy_lint" }
 itertools = "0.12"
 quine-mc_cluskey = "0.2"
 regex-syntax = "0.8"
diff --git a/clippy_lints/src/declare_clippy_lint.rs b/clippy_lints/src/declare_clippy_lint.rs
deleted file mode 100644
index 9f82f876727..00000000000
--- a/clippy_lints/src/declare_clippy_lint.rs
+++ /dev/null
@@ -1,168 +0,0 @@
-#[macro_export]
-#[allow(clippy::crate_in_macro_def)]
-macro_rules! declare_clippy_lint {
-    (@
-        $(#[doc = $lit:literal])*
-        pub $lint_name:ident,
-        $level:ident,
-        $lintcategory:expr,
-        $desc:literal,
-        $version_expr:expr,
-        $version_lit:literal
-        $(, $eval_always: literal)?
-    ) => {
-        rustc_session::declare_tool_lint! {
-            $(#[doc = $lit])*
-            #[clippy::version = $version_lit]
-            pub clippy::$lint_name,
-            $level,
-            $desc,
-            report_in_external_macro:true
-            $(, @eval_always = $eval_always)?
-        }
-
-        pub(crate) static ${concat($lint_name, _INFO)}: &'static crate::LintInfo = &crate::LintInfo {
-            lint: &$lint_name,
-            category:  $lintcategory,
-            explanation: concat!($($lit,"\n",)*),
-            location: concat!(file!(), "#L", line!()),
-            version: $version_expr
-        };
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        restriction,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Allow, crate::LintCategory::Restriction, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-        }
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        style,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Warn, crate::LintCategory::Style, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-        }
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        correctness,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Deny, crate::LintCategory::Correctness, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-
-        }
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        perf,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Warn, crate::LintCategory::Perf, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-        }
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        complexity,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Warn, crate::LintCategory::Complexity, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-        }
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        suspicious,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Warn, crate::LintCategory::Suspicious, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-        }
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        nursery,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Allow, crate::LintCategory::Nursery, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-        }
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        pedantic,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Allow, crate::LintCategory::Pedantic, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-        }
-    };
-    (
-        $(#[doc = $lit:literal])*
-        #[clippy::version = $version:literal]
-        pub $lint_name:ident,
-        cargo,
-        $desc:literal
-        $(, @eval_always = $eval_always: literal)?
-    ) => {
-        declare_clippy_lint! {@
-            $(#[doc = $lit])*
-            pub $lint_name, Allow, crate::LintCategory::Cargo, $desc,
-            Some($version), $version
-            $(, $eval_always)?
-        }
-    };
-}
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 902aff8b971..aac30d29a90 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 static LINTS: &[&crate::LintInfo] = &[
+pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
     crate::absolute_paths::ABSOLUTE_PATHS_INFO,
     crate::almost_complete_range::ALMOST_COMPLETE_RANGE_INFO,
     crate::approx_const::APPROX_CONSTANT_INFO,
diff --git a/clippy_lints/src/floating_point_arithmetic.rs b/clippy_lints/src/floating_point_arithmetic.rs
index 3c7e83b0697..b3c9e860758 100644
--- a/clippy_lints/src/floating_point_arithmetic.rs
+++ b/clippy_lints/src/floating_point_arithmetic.rs
@@ -5,14 +5,13 @@ use clippy_utils::{
     eq_expr_value, get_parent_expr, higher, is_in_const_context, is_inherent_method_call, is_no_std_crate,
     numeric_literal, peel_blocks, sugg, sym,
 };
+use rustc_ast::ast;
 use rustc_errors::Applicability;
 use rustc_hir::{BinOpKind, Expr, ExprKind, PathSegment, UnOp};
 use rustc_lint::{LateContext, LateLintPass};
 use rustc_middle::ty;
 use rustc_session::declare_lint_pass;
 use rustc_span::source_map::Spanned;
-
-use rustc_ast::ast;
 use std::f32::consts as f32_consts;
 use std::f64::consts as f64_consts;
 use sugg::Sugg;
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index be9142b17fe..96a6dee5885 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -59,10 +59,10 @@ extern crate smallvec;
 extern crate thin_vec;
 
 #[macro_use]
-mod declare_clippy_lint;
+extern crate clippy_utils;
 
 #[macro_use]
-extern crate clippy_utils;
+extern crate declare_clippy_lint;
 
 mod utils;
 
@@ -411,108 +411,9 @@ mod zombie_processes;
 use clippy_config::{Conf, get_configuration_metadata, sanitize_explanation};
 use clippy_utils::macros::FormatArgsStorage;
 use rustc_data_structures::fx::FxHashSet;
-use rustc_lint::{Lint, LintId};
+use rustc_lint::Lint;
 use utils::attr_collector::{AttrCollector, AttrStorage};
 
-#[derive(Default)]
-struct RegistrationGroups {
-    all: Vec<LintId>,
-    cargo: Vec<LintId>,
-    complexity: Vec<LintId>,
-    correctness: Vec<LintId>,
-    nursery: Vec<LintId>,
-    pedantic: Vec<LintId>,
-    perf: Vec<LintId>,
-    restriction: Vec<LintId>,
-    style: Vec<LintId>,
-    suspicious: Vec<LintId>,
-}
-
-impl RegistrationGroups {
-    #[rustfmt::skip]
-    fn register(self, store: &mut rustc_lint::LintStore) {
-        store.register_group(true, "clippy::all", Some("clippy_all"), self.all);
-        store.register_group(true, "clippy::cargo", Some("clippy_cargo"), self.cargo);
-        store.register_group(true, "clippy::complexity", Some("clippy_complexity"), self.complexity);
-        store.register_group(true, "clippy::correctness", Some("clippy_correctness"), self.correctness);
-        store.register_group(true, "clippy::nursery", Some("clippy_nursery"), self.nursery);
-        store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), self.pedantic);
-        store.register_group(true, "clippy::perf", Some("clippy_perf"), self.perf);
-        store.register_group(true, "clippy::restriction", Some("clippy_restriction"), self.restriction);
-        store.register_group(true, "clippy::style", Some("clippy_style"), self.style);
-        store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), self.suspicious);
-    }
-}
-
-#[derive(Copy, Clone, Debug)]
-pub(crate) enum LintCategory {
-    Cargo,
-    Complexity,
-    Correctness,
-    Nursery,
-    Pedantic,
-    Perf,
-    Restriction,
-    Style,
-    Suspicious,
-}
-
-#[allow(clippy::enum_glob_use)]
-use LintCategory::*;
-
-impl LintCategory {
-    fn is_all(self) -> bool {
-        matches!(self, Correctness | Suspicious | Style | Complexity | Perf)
-    }
-
-    fn group(self, groups: &mut RegistrationGroups) -> &mut Vec<LintId> {
-        match self {
-            Cargo => &mut groups.cargo,
-            Complexity => &mut groups.complexity,
-            Correctness => &mut groups.correctness,
-            Nursery => &mut groups.nursery,
-            Pedantic => &mut groups.pedantic,
-            Perf => &mut groups.perf,
-            Restriction => &mut groups.restriction,
-            Style => &mut groups.style,
-            Suspicious => &mut groups.suspicious,
-        }
-    }
-}
-
-pub struct LintInfo {
-    /// Double reference to maintain pointer equality
-    pub lint: &'static &'static Lint,
-    category: LintCategory,
-    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",
-        }
-    }
-}
-
 pub fn explain(name: &str) -> i32 {
     let target = format!("clippy::{}", name.to_ascii_uppercase());
 
@@ -535,30 +436,11 @@ pub fn explain(name: &str) -> i32 {
     }
 }
 
-fn register_categories(store: &mut rustc_lint::LintStore) {
-    let mut groups = RegistrationGroups::default();
-
-    for LintInfo { lint, category, .. } in declared_lints::LINTS {
-        if category.is_all() {
-            groups.all.push(LintId::of(lint));
-        }
-
-        category.group(&mut groups).push(LintId::of(lint));
-    }
-
-    let lints: Vec<&'static Lint> = declared_lints::LINTS.iter().map(|info| *info.lint).collect();
-
-    store.register_lints(&lints);
-    groups.register(store);
-}
-
 /// Register all lints and lint groups with the rustc lint store
 ///
 /// Used in `./src/driver.rs`.
 #[expect(clippy::too_many_lines)]
-pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
-    register_categories(store);
-
+pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
     for (old_name, new_name) in deprecated_lints::RENAMED {
         store.register_renamed(old_name, new_name);
     }
diff --git a/clippy_lints/src/manual_let_else.rs b/clippy_lints/src/manual_let_else.rs
index 0b3bec714c0..9ff82cdcb66 100644
--- a/clippy_lints/src/manual_let_else.rs
+++ b/clippy_lints/src/manual_let_else.rs
@@ -13,7 +13,6 @@ use rustc_errors::Applicability;
 use rustc_hir::def::{CtorOf, DefKind, Res};
 use rustc_hir::{Arm, Expr, ExprKind, HirId, MatchSource, Pat, PatExpr, PatExprKind, PatKind, QPath, Stmt, StmtKind};
 use rustc_lint::{LateContext, LintContext};
-
 use rustc_span::Span;
 use rustc_span::symbol::{Symbol, sym};
 use std::slice;
diff --git a/clippy_lints/src/needless_parens_on_range_literals.rs b/clippy_lints/src/needless_parens_on_range_literals.rs
index 8a62106377c..021a11593f3 100644
--- a/clippy_lints/src/needless_parens_on_range_literals.rs
+++ b/clippy_lints/src/needless_parens_on_range_literals.rs
@@ -5,7 +5,6 @@ use clippy_utils::source::{snippet, snippet_with_applicability};
 use rustc_ast::ast;
 use rustc_errors::Applicability;
 use rustc_hir::{Expr, ExprKind};
-
 use rustc_lint::{LateContext, LateLintPass};
 use rustc_session::declare_lint_pass;
 
diff --git a/clippy_lints/src/question_mark_used.rs b/clippy_lints/src/question_mark_used.rs
index 96ea485d769..7bbbd0d25ac 100644
--- a/clippy_lints/src/question_mark_used.rs
+++ b/clippy_lints/src/question_mark_used.rs
@@ -1,5 +1,4 @@
 use clippy_utils::diagnostics::span_lint_and_then;
-
 use clippy_utils::macros::span_is_local;
 use rustc_hir::{Expr, ExprKind, MatchSource};
 use rustc_lint::{LateContext, LateLintPass};
diff --git a/clippy_lints/src/read_zero_byte_vec.rs b/clippy_lints/src/read_zero_byte_vec.rs
index 6b1dc864fb7..acd840401c6 100644
--- a/clippy_lints/src/read_zero_byte_vec.rs
+++ b/clippy_lints/src/read_zero_byte_vec.rs
@@ -3,11 +3,10 @@ use clippy_utils::higher::{VecInitKind, get_vec_init_kind};
 use clippy_utils::source::snippet;
 use clippy_utils::{get_enclosing_block, sym};
 
-use hir::{Expr, ExprKind, HirId, LetStmt, PatKind, PathSegment, QPath, StmtKind};
 use rustc_errors::Applicability;
-use rustc_hir as hir;
 use rustc_hir::def::Res;
 use rustc_hir::intravisit::{Visitor, walk_expr};
+use rustc_hir::{self as hir, Expr, ExprKind, HirId, LetStmt, PatKind, PathSegment, QPath, StmtKind};
 use rustc_lint::{LateContext, LateLintPass};
 use rustc_session::declare_lint_pass;
 
diff --git a/declare_clippy_lint/Cargo.toml b/declare_clippy_lint/Cargo.toml
new file mode 100644
index 00000000000..c8a9b13e6ce
--- /dev/null
+++ b/declare_clippy_lint/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "declare_clippy_lint"
+version = "0.1.89"
+edition = "2024"
+repository = "https://github.com/rust-lang/rust-clippy"
+license = "MIT OR Apache-2.0"
+
+[package.metadata.rust-analyzer]
+# This crate uses #[feature(rustc_private)]
+rustc_private = true
diff --git a/declare_clippy_lint/src/lib.rs b/declare_clippy_lint/src/lib.rs
new file mode 100644
index 00000000000..f7d9c64bfbd
--- /dev/null
+++ b/declare_clippy_lint/src/lib.rs
@@ -0,0 +1,280 @@
+#![feature(macro_metavar_expr_concat, rustc_private)]
+
+extern crate rustc_lint;
+
+use rustc_lint::{Lint, LintId, LintStore};
+
+// Needed by `declare_clippy_lint!`.
+pub extern crate rustc_session;
+
+#[derive(Default)]
+pub struct LintListBuilder {
+    lints: Vec<&'static Lint>,
+    all: Vec<LintId>,
+    cargo: Vec<LintId>,
+    complexity: Vec<LintId>,
+    correctness: Vec<LintId>,
+    nursery: Vec<LintId>,
+    pedantic: Vec<LintId>,
+    perf: Vec<LintId>,
+    restriction: Vec<LintId>,
+    style: Vec<LintId>,
+    suspicious: Vec<LintId>,
+}
+impl LintListBuilder {
+    pub fn insert(&mut self, lints: &[&LintInfo]) {
+        #[allow(clippy::enum_glob_use)]
+        use LintCategory::*;
+
+        self.lints.extend(lints.iter().map(|&x| x.lint));
+        for &&LintInfo { lint, category, .. } in lints {
+            let (all, cat) = match category {
+                Complexity => (Some(&mut self.all), &mut self.complexity),
+                Correctness => (Some(&mut self.all), &mut self.correctness),
+                Perf => (Some(&mut self.all), &mut self.perf),
+                Style => (Some(&mut self.all), &mut self.style),
+                Suspicious => (Some(&mut self.all), &mut self.suspicious),
+                Cargo => (None, &mut self.cargo),
+                Nursery => (None, &mut self.nursery),
+                Pedantic => (None, &mut self.pedantic),
+                Restriction => (None, &mut self.restriction),
+            };
+            if let Some(all) = all {
+                all.push(LintId::of(lint));
+            }
+            cat.push(LintId::of(lint));
+        }
+    }
+
+    pub fn register(self, store: &mut LintStore) {
+        store.register_lints(&self.lints);
+        store.register_group(true, "clippy::all", Some("clippy_all"), self.all);
+        store.register_group(true, "clippy::cargo", Some("clippy_cargo"), self.cargo);
+        store.register_group(true, "clippy::complexity", Some("clippy_complexity"), self.complexity);
+        store.register_group(
+            true,
+            "clippy::correctness",
+            Some("clippy_correctness"),
+            self.correctness,
+        );
+        store.register_group(true, "clippy::nursery", Some("clippy_nursery"), self.nursery);
+        store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), self.pedantic);
+        store.register_group(true, "clippy::perf", Some("clippy_perf"), self.perf);
+        store.register_group(
+            true,
+            "clippy::restriction",
+            Some("clippy_restriction"),
+            self.restriction,
+        );
+        store.register_group(true, "clippy::style", Some("clippy_style"), self.style);
+        store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), self.suspicious);
+    }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum LintCategory {
+    Cargo,
+    Complexity,
+    Correctness,
+    Nursery,
+    Pedantic,
+    Perf,
+    Restriction,
+    Style,
+    Suspicious,
+}
+impl LintCategory {
+    #[must_use]
+    pub fn name(self) -> &'static str {
+        match self {
+            Self::Cargo => "cargo",
+            Self::Complexity => "complexity",
+            Self::Correctness => "correctness",
+            Self::Nursery => "nursery",
+            Self::Pedantic => "pedantic",
+            Self::Perf => "perf",
+            Self::Restriction => "restriction",
+            Self::Style => "style",
+            Self::Suspicious => "suspicious",
+        }
+    }
+}
+
+pub struct LintInfo {
+    pub lint: &'static Lint,
+    pub category: LintCategory,
+    pub explanation: &'static str,
+    /// e.g. `clippy_lints/src/absolute_paths.rs#43`
+    pub location: &'static str,
+    pub version: &'static str,
+}
+
+impl LintInfo {
+    /// Returns the lint name in lowercase without the `clippy::` prefix
+    #[must_use]
+    #[expect(clippy::missing_panics_doc)]
+    pub fn name_lower(&self) -> String {
+        self.lint.name.strip_prefix("clippy::").unwrap().to_ascii_lowercase()
+    }
+}
+
+#[macro_export]
+macro_rules! declare_clippy_lint_inner {
+    (
+        $(#[doc = $docs:literal])*
+        #[clippy::version = $version:literal]
+        $vis:vis $lint_name:ident,
+        $level:ident,
+        $category:ident,
+        $desc:literal
+        $(, @eval_always = $eval_always:literal)?
+    ) => {
+        $crate::rustc_session::declare_tool_lint! {
+            $(#[doc = $docs])*
+            #[clippy::version = $version]
+            $vis clippy::$lint_name,
+            $level,
+            $desc,
+            report_in_external_macro:true
+            $(, @eval_always = $eval_always)?
+        }
+
+        pub(crate) static ${concat($lint_name, _INFO)}: &'static $crate::LintInfo = &$crate::LintInfo {
+            lint: $lint_name,
+            category: $crate::LintCategory::$category,
+            explanation: concat!($($docs,"\n",)*),
+            location: concat!(file!(), "#L", line!()),
+            version: $version,
+        };
+    };
+}
+
+#[macro_export]
+macro_rules! declare_clippy_lint {
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        correctness,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Deny,
+            Correctness,
+            $($rest)*
+        }
+    };
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        complexity,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Warn,
+            Complexity,
+            $($rest)*
+        }
+    };
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        perf,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Warn,
+            Perf,
+            $($rest)*
+        }
+    };
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        style,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Warn,
+            Style,
+            $($rest)*
+        }
+    };
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        suspicious,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Warn,
+            Suspicious,
+            $($rest)*
+        }
+    };
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        cargo,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Allow,
+            Cargo,
+            $($rest)*
+        }
+    };
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        nursery,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Allow,
+            Nursery,
+            $($rest)*
+        }
+    };
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        pedantic,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Allow,
+            Pedantic,
+            $($rest)*
+        }
+    };
+    (
+        $(#[$($meta:tt)*])*
+        $vis:vis $lint_name:ident,
+        restriction,
+        $($rest:tt)*
+    ) => {
+        $crate::declare_clippy_lint_inner! {
+            $(#[$($meta)*])*
+            $vis $lint_name,
+            Allow,
+            Restriction,
+            $($rest)*
+        }
+    };
+}
diff --git a/src/driver.rs b/src/driver.rs
index 37adb14169a..202c74413cf 100644
--- a/src/driver.rs
+++ b/src/driver.rs
@@ -14,6 +14,7 @@ extern crate rustc_session;
 extern crate rustc_span;
 
 use clippy_utils::sym;
+use declare_clippy_lint::LintListBuilder;
 use rustc_interface::interface;
 use rustc_session::EarlyDiagCtxt;
 use rustc_session::config::ErrorOutputType;
@@ -151,8 +152,13 @@ impl rustc_driver::Callbacks for ClippyCallbacks {
                 (previous)(sess, lint_store);
             }
 
+            let mut list_builder = LintListBuilder::default();
+            list_builder.insert(clippy_lints::declared_lints::LINTS);
+            list_builder.register(lint_store);
+
             let conf = clippy_config::Conf::read(sess, &conf_path);
-            clippy_lints::register_lints(lint_store, conf);
+            clippy_lints::register_lint_passes(lint_store, conf);
+
             #[cfg(feature = "internal")]
             clippy_lints_internal::register_lints(lint_store);
         }));
diff --git a/tests/compile-test.rs b/tests/compile-test.rs
index 99a01257a7b..cefe654fef6 100644
--- a/tests/compile-test.rs
+++ b/tests/compile-test.rs
@@ -7,9 +7,9 @@ use askama::filters::Safe;
 use cargo_metadata::Message;
 use cargo_metadata::diagnostic::{Applicability, Diagnostic};
 use clippy_config::ClippyConfiguration;
-use clippy_lints::LintInfo;
 use clippy_lints::declared_lints::LINTS;
 use clippy_lints::deprecated_lints::{DEPRECATED, DEPRECATED_VERSION, RENAMED};
+use declare_clippy_lint::LintInfo;
 use pulldown_cmark::{Options, Parser, html};
 use serde::Deserialize;
 use test_utils::IS_RUSTC_TEST_SUITE;
@@ -568,10 +568,10 @@ impl LintMetadata {
         Self {
             id: name,
             id_location: Some(lint.location),
-            group: lint.category_str(),
+            group: lint.category.name(),
             level: lint.lint.default_level.as_str(),
             docs,
-            version: lint.version.unwrap(),
+            version: lint.version,
             applicability,
         }
     }
diff --git a/tests/dogfood.rs b/tests/dogfood.rs
index 4ac2bd53285..389616801fc 100644
--- a/tests/dogfood.rs
+++ b/tests/dogfood.rs
@@ -40,6 +40,7 @@ fn dogfood() {
         "clippy_lints",
         "clippy_utils",
         "clippy_config",
+        "declare_clippy_lint",
         "lintcheck",
         "rustc_tools_util",
     ] {
diff --git a/tests/versioncheck.rs b/tests/versioncheck.rs
index f6fc2354ca0..b0179387b2b 100644
--- a/tests/versioncheck.rs
+++ b/tests/versioncheck.rs
@@ -27,6 +27,7 @@ fn consistent_clippy_crate_versions() {
         "clippy_config/Cargo.toml",
         "clippy_lints/Cargo.toml",
         "clippy_utils/Cargo.toml",
+        "declare_clippy_lint/Cargo.toml",
     ];
 
     for path in paths {