about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJason Newcomb <jsnewcomb@pm.me>2025-04-13 07:05:29 -0400
committerJason Newcomb <jsnewcomb@pm.me>2025-05-12 17:07:52 -0400
commit2f39264d00eef5e5990438a5c23cb670500548d9 (patch)
treecbc2a05f06490d341dabc6f7321bd476377cfd8d
parentbfc6ad0340dd85f15854388752af8b490ecaee5d (diff)
downloadrust-2f39264d00eef5e5990438a5c23cb670500548d9.tar.gz
rust-2f39264d00eef5e5990438a5c23cb670500548d9.zip
clippy_dev: Set the current directory to clippy's root path.
-rw-r--r--clippy_dev/src/dogfood.rs5
-rw-r--r--clippy_dev/src/fmt.rs32
-rw-r--r--clippy_dev/src/lib.rs2
-rw-r--r--clippy_dev/src/main.rs18
-rw-r--r--clippy_dev/src/new_lint.rs46
-rw-r--r--clippy_dev/src/release.rs14
-rw-r--r--clippy_dev/src/update_lints.rs67
-rw-r--r--clippy_dev/src/utils.rs207
8 files changed, 264 insertions, 127 deletions
diff --git a/clippy_dev/src/dogfood.rs b/clippy_dev/src/dogfood.rs
index 05fa24d8d4e..7e9d92458d0 100644
--- a/clippy_dev/src/dogfood.rs
+++ b/clippy_dev/src/dogfood.rs
@@ -1,4 +1,4 @@
-use crate::utils::{clippy_project_root, exit_if_err};
+use crate::utils::exit_if_err;
 use std::process::Command;
 
 /// # Panics
@@ -8,8 +8,7 @@ use std::process::Command;
 pub fn dogfood(fix: bool, allow_dirty: bool, allow_staged: bool, allow_no_vcs: bool) {
     let mut cmd = Command::new("cargo");
 
-    cmd.current_dir(clippy_project_root())
-        .args(["test", "--test", "dogfood"])
+    cmd.args(["test", "--test", "dogfood"])
         .args(["--features", "internal"])
         .args(["--", "dogfood_clippy", "--nocapture"]);
 
diff --git a/clippy_dev/src/fmt.rs b/clippy_dev/src/fmt.rs
index bdddf46a2cb..b4c13213f55 100644
--- a/clippy_dev/src/fmt.rs
+++ b/clippy_dev/src/fmt.rs
@@ -1,4 +1,3 @@
-use crate::utils::clippy_project_root;
 use itertools::Itertools;
 use rustc_lexer::{TokenKind, tokenize};
 use shell_escape::escape;
@@ -104,15 +103,8 @@ fn fmt_conf(check: bool) -> Result<(), Error> {
         Field,
     }
 
-    let path: PathBuf = [
-        clippy_project_root().as_path(),
-        "clippy_config".as_ref(),
-        "src".as_ref(),
-        "conf.rs".as_ref(),
-    ]
-    .into_iter()
-    .collect();
-    let text = fs::read_to_string(&path)?;
+    let path = "clippy_config/src/conf.rs";
+    let text = fs::read_to_string(path)?;
 
     let (pre, conf) = text
         .split_once("define_Conf! {\n")
@@ -203,7 +195,7 @@ fn fmt_conf(check: bool) -> Result<(), Error> {
             | (State::Lints, TokenKind::Comma | TokenKind::OpenParen | TokenKind::CloseParen) => {},
             _ => {
                 return Err(Error::Parse(
-                    path,
+                    PathBuf::from(path),
                     offset_to_line(&text, conf_offset + i),
                     format!("unexpected token `{}`", &conf[i..i + t.len as usize]),
                 ));
@@ -213,7 +205,7 @@ fn fmt_conf(check: bool) -> Result<(), Error> {
 
     if !matches!(state, State::Field) {
         return Err(Error::Parse(
-            path,
+            PathBuf::from(path),
             offset_to_line(&text, conf_offset + conf.len()),
             "incomplete field".into(),
         ));
@@ -260,18 +252,16 @@ fn fmt_conf(check: bool) -> Result<(), Error> {
         if check {
             return Err(Error::CheckFailed);
         }
-        fs::write(&path, new_text.as_bytes())?;
+        fs::write(path, new_text.as_bytes())?;
     }
     Ok(())
 }
 
 fn run_rustfmt(context: &FmtContext) -> Result<(), Error> {
-    let project_root = clippy_project_root();
-
     // if we added a local rustc repo as path dependency to clippy for rust analyzer, we do NOT want to
     // format because rustfmt would also format the entire rustc repo as it is a local
     // dependency
-    if fs::read_to_string(project_root.join("Cargo.toml"))
+    if fs::read_to_string("Cargo.toml")
         .expect("Failed to read clippy Cargo.toml")
         .contains("[target.'cfg(NOT_A_PLATFORM)'.dependencies]")
     {
@@ -280,12 +270,12 @@ fn run_rustfmt(context: &FmtContext) -> Result<(), Error> {
 
     check_for_rustfmt(context)?;
 
-    cargo_fmt(context, project_root.as_path())?;
-    cargo_fmt(context, &project_root.join("clippy_dev"))?;
-    cargo_fmt(context, &project_root.join("rustc_tools_util"))?;
-    cargo_fmt(context, &project_root.join("lintcheck"))?;
+    cargo_fmt(context, ".".as_ref())?;
+    cargo_fmt(context, "clippy_dev".as_ref())?;
+    cargo_fmt(context, "rustc_tools_util".as_ref())?;
+    cargo_fmt(context, "lintcheck".as_ref())?;
 
-    let chunks = WalkDir::new(project_root.join("tests"))
+    let chunks = WalkDir::new("tests")
         .into_iter()
         .filter_map(|entry| {
             let entry = entry.expect("failed to find tests");
diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs
index db4b4d07c15..70e57fcfc26 100644
--- a/clippy_dev/src/lib.rs
+++ b/clippy_dev/src/lib.rs
@@ -1,4 +1,4 @@
-#![feature(rustc_private)]
+#![feature(rustc_private, if_let_guard, let_chains)]
 #![warn(
     trivial_casts,
     trivial_numeric_casts,
diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs
index 83f8e66b334..bd87e980c22 100644
--- a/clippy_dev/src/main.rs
+++ b/clippy_dev/src/main.rs
@@ -5,9 +5,14 @@
 use clap::{Args, Parser, Subcommand};
 use clippy_dev::{dogfood, fmt, lint, new_lint, release, serve, setup, sync, update_lints, utils};
 use std::convert::Infallible;
+use std::env;
 
 fn main() {
     let dev = Dev::parse();
+    let clippy = utils::ClippyInfo::search_for_manifest();
+    if let Err(e) = env::set_current_dir(&clippy.path) {
+        panic!("error setting current directory to `{}`: {e}", clippy.path.display());
+    }
 
     match dev.command {
         DevCommand::Bless => {
@@ -35,7 +40,7 @@ fn main() {
             category,
             r#type,
             msrv,
-        } => match new_lint::create(pass, &name, &category, r#type.as_deref(), msrv) {
+        } => match new_lint::create(clippy.version, pass, &name, &category, r#type.as_deref(), msrv) {
             Ok(()) => update_lints::update(utils::UpdateMode::Change),
             Err(e) => eprintln!("Unable to create lint: {e}"),
         },
@@ -79,13 +84,18 @@ fn main() {
             old_name,
             new_name,
             uplift,
-        } => update_lints::rename(&old_name, new_name.as_ref().unwrap_or(&old_name), uplift),
-        DevCommand::Deprecate { name, reason } => update_lints::deprecate(&name, &reason),
+        } => update_lints::rename(
+            clippy.version,
+            &old_name,
+            new_name.as_ref().unwrap_or(&old_name),
+            uplift,
+        ),
+        DevCommand::Deprecate { name, reason } => update_lints::deprecate(clippy.version, &name, &reason),
         DevCommand::Sync(SyncCommand { subcommand }) => match subcommand {
             SyncSubcommand::UpdateNightly => sync::update_nightly(),
         },
         DevCommand::Release(ReleaseCommand { subcommand }) => match subcommand {
-            ReleaseSubcommand::BumpVersion => release::bump_version(),
+            ReleaseSubcommand::BumpVersion => release::bump_version(clippy.version),
         },
     }
 }
diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs
index 771f48b3f8b..a6e8c3ac324 100644
--- a/clippy_dev/src/new_lint.rs
+++ b/clippy_dev/src/new_lint.rs
@@ -1,4 +1,4 @@
-use crate::utils::{clippy_project_root, clippy_version};
+use crate::utils::Version;
 use clap::ValueEnum;
 use indoc::{formatdoc, writedoc};
 use std::fmt::{self, Write as _};
@@ -22,11 +22,11 @@ impl fmt::Display for Pass {
 }
 
 struct LintData<'a> {
+    clippy_version: Version,
     pass: Pass,
     name: &'a str,
     category: &'a str,
     ty: Option<&'a str>,
-    project_root: PathBuf,
 }
 
 trait Context {
@@ -50,18 +50,25 @@ impl<T> Context for io::Result<T> {
 /// # Errors
 ///
 /// This function errors out if the files couldn't be created or written to.
-pub fn create(pass: Pass, name: &str, category: &str, mut ty: Option<&str>, msrv: bool) -> io::Result<()> {
+pub fn create(
+    clippy_version: Version,
+    pass: Pass,
+    name: &str,
+    category: &str,
+    mut ty: Option<&str>,
+    msrv: bool,
+) -> io::Result<()> {
     if category == "cargo" && ty.is_none() {
         // `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo`
         ty = Some("cargo");
     }
 
     let lint = LintData {
+        clippy_version,
         pass,
         name,
         category,
         ty,
-        project_root: clippy_project_root(),
     };
 
     create_lint(&lint, msrv).context("Unable to create lint implementation")?;
@@ -88,7 +95,7 @@ fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
     } else {
         let lint_contents = get_lint_file_contents(lint, enable_msrv);
         let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
-        write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())?;
+        write_file(&lint_path, lint_contents.as_bytes())?;
         println!("Generated lint file: `{lint_path}`");
 
         Ok(())
@@ -115,8 +122,7 @@ fn create_test(lint: &LintData<'_>, msrv: bool) -> io::Result<()> {
     }
 
     if lint.category == "cargo" {
-        let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
-        let test_dir = lint.project_root.join(&relative_test_dir);
+        let test_dir = format!("tests/ui-cargo/{}", lint.name);
         fs::create_dir(&test_dir)?;
 
         create_project_layout(
@@ -134,11 +140,11 @@ fn create_test(lint: &LintData<'_>, msrv: bool) -> io::Result<()> {
             false,
         )?;
 
-        println!("Generated test directories: `{relative_test_dir}/pass`, `{relative_test_dir}/fail`");
+        println!("Generated test directories: `{test_dir}/pass`, `{test_dir}/fail`");
     } else {
         let test_path = format!("tests/ui/{}.rs", lint.name);
         let test_contents = get_test_file_contents(lint.name, msrv);
-        write_file(lint.project_root.join(&test_path), test_contents)?;
+        write_file(&test_path, test_contents)?;
 
         println!("Generated test file: `{test_path}`");
     }
@@ -193,11 +199,6 @@ fn to_camel_case(name: &str) -> String {
         .collect()
 }
 
-pub(crate) fn get_stabilization_version() -> String {
-    let (minor, patch) = clippy_version();
-    format!("{minor}.{patch}.0")
-}
-
 fn get_test_file_contents(lint_name: &str, msrv: bool) -> String {
     let mut test = formatdoc!(
         r"
@@ -292,7 +293,11 @@ fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
         );
     }
 
-    let _: fmt::Result = writeln!(result, "{}", get_lint_declaration(&name_upper, category));
+    let _: fmt::Result = writeln!(
+        result,
+        "{}",
+        get_lint_declaration(lint.clippy_version, &name_upper, category)
+    );
 
     if enable_msrv {
         let _: fmt::Result = writedoc!(
@@ -330,7 +335,7 @@ fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
     result
 }
 
-fn get_lint_declaration(name_upper: &str, category: &str) -> String {
+fn get_lint_declaration(version: Version, name_upper: &str, category: &str) -> String {
     let justification_heading = if category == "restriction" {
         "Why restrict this?"
     } else {
@@ -357,7 +362,7 @@ fn get_lint_declaration(name_upper: &str, category: &str) -> String {
                 "default lint description"
             }}
         "#,
-        get_stabilization_version(),
+        version.rust_display(),
     )
 }
 
@@ -371,7 +376,7 @@ fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::R
         _ => {},
     }
 
-    let ty_dir = lint.project_root.join(format!("clippy_lints/src/{ty}"));
+    let ty_dir = PathBuf::from(format!("clippy_lints/src/{ty}"));
     assert!(
         ty_dir.exists() && ty_dir.is_dir(),
         "Directory `{}` does not exist!",
@@ -529,7 +534,10 @@ fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str>
     file_contents.replace_range(
         // Remove the trailing newline, which should always be present
         last_decl_curly_offset..=last_decl_curly_offset,
-        &format!("\n\n{}", get_lint_declaration(&lint_name_upper, lint.category)),
+        &format!(
+            "\n\n{}",
+            get_lint_declaration(lint.clippy_version, &lint_name_upper, lint.category)
+        ),
     );
 
     // Add the lint to `impl_lint_pass`/`declare_lint_pass`
diff --git a/clippy_dev/src/release.rs b/clippy_dev/src/release.rs
index ac755168701..34f81e10a39 100644
--- a/clippy_dev/src/release.rs
+++ b/clippy_dev/src/release.rs
@@ -1,7 +1,6 @@
 use std::fmt::Write;
-use std::path::Path;
 
-use crate::utils::{UpdateMode, clippy_version, replace_region_in_file};
+use crate::utils::{UpdateMode, Version, replace_region_in_file};
 
 const CARGO_TOML_FILES: [&str; 4] = [
     "clippy_config/Cargo.toml",
@@ -10,17 +9,16 @@ const CARGO_TOML_FILES: [&str; 4] = [
     "Cargo.toml",
 ];
 
-pub fn bump_version() {
-    let (minor, mut patch) = clippy_version();
-    patch += 1;
-    for file in &CARGO_TOML_FILES {
+pub fn bump_version(mut version: Version) {
+    version.minor += 1;
+    for &file in &CARGO_TOML_FILES {
         replace_region_in_file(
             UpdateMode::Change,
-            Path::new(file),
+            file.as_ref(),
             "# begin autogenerated version\n",
             "# end autogenerated version",
             |res| {
-                writeln!(res, "version = \"0.{minor}.{patch}\"").unwrap();
+                writeln!(res, "version = \"{}\"", version.toml_display()).unwrap();
             },
         );
     }
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index d848a97f86d..e53c454be7a 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -1,4 +1,4 @@
-use crate::utils::{UpdateMode, clippy_project_root, exit_with_failure, replace_region_in_file};
+use crate::utils::{UpdateMode, Version, exit_with_failure, replace_region_in_file};
 use aho_corasick::AhoCorasickBuilder;
 use itertools::Itertools;
 use rustc_lexer::{LiteralKind, TokenKind, tokenize};
@@ -139,7 +139,7 @@ pub fn print_lints() {
 /// * If `old_name` doesn't name an existing lint.
 /// * If `old_name` names a deprecated or renamed lint.
 #[allow(clippy::too_many_lines)]
-pub fn rename(old_name: &str, new_name: &str, uplift: bool) {
+pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: bool) {
     if let Some((prefix, _)) = old_name.split_once("::") {
         panic!("`{old_name}` should not contain the `{prefix}` prefix");
     }
@@ -180,31 +180,28 @@ pub fn rename(old_name: &str, new_name: &str, uplift: bool) {
     );
 
     // Update all lint level attributes. (`clippy::lint_name`)
-    for file in WalkDir::new(clippy_project_root())
-        .into_iter()
-        .map(Result::unwrap)
-        .filter(|f| {
-            let name = f.path().file_name();
-            let ext = f.path().extension();
-            (ext == Some(OsStr::new("rs")) || ext == Some(OsStr::new("fixed")))
-                && name != Some(OsStr::new("rename.rs"))
-                && name != Some(OsStr::new("deprecated_lints.rs"))
-        })
-    {
+    for file in WalkDir::new(".").into_iter().map(Result::unwrap).filter(|f| {
+        let name = f.path().file_name();
+        let ext = f.path().extension();
+        (ext == Some(OsStr::new("rs")) || ext == Some(OsStr::new("fixed")))
+            && name != Some(OsStr::new("rename.rs"))
+            && name != Some(OsStr::new("deprecated_lints.rs"))
+    }) {
         rewrite_file(file.path(), |s| {
             replace_ident_like(s, &[(&lint.old_name, &lint.new_name)])
         });
     }
 
-    let version = crate::new_lint::get_stabilization_version();
     rewrite_file(Path::new("clippy_lints/src/deprecated_lints.rs"), |s| {
         insert_at_marker(
             s,
             "// end renamed lints. used by `cargo dev rename_lint`",
             &format!(
-                "#[clippy::version = \"{version}\"]\n    \
+                "#[clippy::version = \"{}\"]\n    \
                 (\"{}\", \"{}\"),\n    ",
-                lint.old_name, lint.new_name,
+                clippy_version.rust_display(),
+                lint.old_name,
+                lint.new_name,
             ),
         )
     });
@@ -284,9 +281,15 @@ pub fn rename(old_name: &str, new_name: &str, uplift: bool) {
 
         // Don't change `clippy_utils/src/renamed_lints.rs` here as it would try to edit the lint being
         // renamed.
-        for (_, file) in clippy_lints_src_files().filter(|(rel_path, _)| rel_path != OsStr::new("deprecated_lints.rs"))
-        {
-            rewrite_file(file.path(), |s| replace_ident_like(s, replacements));
+        for file in clippy_lints_src_files() {
+            if file
+                .path()
+                .as_os_str()
+                .to_str()
+                .is_none_or(|x| x["clippy_lints/src/".len()..] != *"deprecated_lints.rs")
+            {
+                rewrite_file(file.path(), |s| replace_ident_like(s, replacements));
+            }
         }
 
         generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints);
@@ -305,7 +308,7 @@ pub fn rename(old_name: &str, new_name: &str, uplift: bool) {
 /// # Panics
 ///
 /// If a file path could not read from or written to
-pub fn deprecate(name: &str, reason: &str) {
+pub fn deprecate(clippy_version: Version, name: &str, reason: &str) {
     let prefixed_name = if name.starts_with("clippy::") {
         name.to_owned()
     } else {
@@ -329,15 +332,15 @@ pub fn deprecate(name: &str, reason: &str) {
         mod_path
     };
 
-    let deprecated_lints_path = &*clippy_project_root().join("clippy_lints/src/deprecated_lints.rs");
-
     if remove_lint_declaration(stripped_name, &mod_path, &mut lints).unwrap_or(false) {
-        let version = crate::new_lint::get_stabilization_version();
-        rewrite_file(deprecated_lints_path, |s| {
+        rewrite_file("clippy_lints/src/deprecated_lints.rs".as_ref(), |s| {
             insert_at_marker(
                 s,
                 "// end deprecated lints. used by `cargo dev deprecate_lint`",
-                &format!("#[clippy::version = \"{version}\"]\n    (\"{prefixed_name}\", \"{reason}\"),\n    ",),
+                &format!(
+                    "#[clippy::version = \"{}\"]\n    (\"{prefixed_name}\", \"{reason}\"),\n    ",
+                    clippy_version.rust_display(),
+                ),
             )
         });
 
@@ -612,15 +615,11 @@ fn gather_all() -> (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>) {
     let mut deprecated_lints = Vec::with_capacity(50);
     let mut renamed_lints = Vec::with_capacity(50);
 
-    for (rel_path, file) in clippy_lints_src_files() {
+    for file in clippy_lints_src_files() {
         let path = file.path();
         let contents =
             fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {e}", path.display()));
-        let module = rel_path
-            .components()
-            .map(|c| c.as_os_str().to_str().unwrap())
-            .collect::<Vec<_>>()
-            .join("::");
+        let module = path.as_os_str().to_str().unwrap()["clippy_lints/src/".len()..].replace(['/', '\\'], "::");
 
         // If the lints are stored in mod.rs, we get the module name from
         // the containing directory:
@@ -639,12 +638,10 @@ fn gather_all() -> (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>) {
     (lints, deprecated_lints, renamed_lints)
 }
 
-fn clippy_lints_src_files() -> impl Iterator<Item = (PathBuf, DirEntry)> {
-    let root_path = clippy_project_root().join("clippy_lints/src");
-    let iter = WalkDir::new(&root_path).into_iter();
+fn clippy_lints_src_files() -> impl Iterator<Item = DirEntry> {
+    let iter = WalkDir::new("clippy_lints/src").into_iter();
     iter.map(Result::unwrap)
         .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
-        .map(move |f| (f.path().strip_prefix(&root_path).unwrap().to_path_buf(), f))
 }
 
 macro_rules! match_tokens {
diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs
index 206816398f5..2b199d96fcd 100644
--- a/clippy_dev/src/utils.rs
+++ b/clippy_dev/src/utils.rs
@@ -1,12 +1,90 @@
+use core::fmt::{self, Display};
+use core::str::FromStr;
+use std::env;
+use std::fs::{self, OpenOptions};
+use std::io::{self, Read as _, Seek as _, SeekFrom, Write};
 use std::path::{Path, PathBuf};
 use std::process::{self, ExitStatus};
-use std::{fs, io};
 
 #[cfg(not(windows))]
 static CARGO_CLIPPY_EXE: &str = "cargo-clippy";
 #[cfg(windows)]
 static CARGO_CLIPPY_EXE: &str = "cargo-clippy.exe";
 
+#[cold]
+#[track_caller]
+fn panic_io(e: &io::Error, action: &str, path: &Path) -> ! {
+    panic!("error {action} `{}`: {}", path.display(), *e)
+}
+
+/// Wrapper around `std::fs::File` which panics with a path on failure.
+pub struct File<'a> {
+    pub inner: fs::File,
+    pub path: &'a Path,
+}
+impl<'a> File<'a> {
+    /// Opens a file panicking on failure.
+    #[track_caller]
+    pub fn open(path: &'a (impl AsRef<Path> + ?Sized), options: &mut OpenOptions) -> Self {
+        let path = path.as_ref();
+        match options.open(path) {
+            Ok(inner) => Self { inner, path },
+            Err(e) => panic_io(&e, "opening", path),
+        }
+    }
+
+    /// Opens a file if it exists, panicking on any other failure.
+    #[track_caller]
+    pub fn open_if_exists(path: &'a (impl AsRef<Path> + ?Sized), options: &mut OpenOptions) -> Option<Self> {
+        let path = path.as_ref();
+        match options.open(path) {
+            Ok(inner) => Some(Self { inner, path }),
+            Err(e) if e.kind() == io::ErrorKind::NotFound => None,
+            Err(e) => panic_io(&e, "opening", path),
+        }
+    }
+
+    /// Opens and reads a file into a string, panicking of failure.
+    #[track_caller]
+    pub fn open_read_to_cleared_string<'dst>(
+        path: &'a (impl AsRef<Path> + ?Sized),
+        dst: &'dst mut String,
+    ) -> &'dst mut String {
+        Self::open(path, OpenOptions::new().read(true)).read_to_cleared_string(dst)
+    }
+
+    /// Read the entire contents of a file to the given buffer.
+    #[track_caller]
+    pub fn read_append_to_string<'dst>(&mut self, dst: &'dst mut String) -> &'dst mut String {
+        match self.inner.read_to_string(dst) {
+            Ok(_) => {},
+            Err(e) => panic_io(&e, "reading", self.path),
+        }
+        dst
+    }
+
+    #[track_caller]
+    pub fn read_to_cleared_string<'dst>(&mut self, dst: &'dst mut String) -> &'dst mut String {
+        dst.clear();
+        self.read_append_to_string(dst)
+    }
+
+    /// Replaces the entire contents of a file.
+    #[track_caller]
+    pub fn replace_contents(&mut self, data: &[u8]) {
+        let res = match self.inner.seek(SeekFrom::Start(0)) {
+            Ok(_) => match self.inner.write_all(data) {
+                Ok(()) => self.inner.set_len(data.len() as u64),
+                Err(e) => Err(e),
+            },
+            Err(e) => Err(e),
+        };
+        if let Err(e) = res {
+            panic_io(&e, "writing", self.path);
+        }
+    }
+}
+
 /// Returns the path to the `cargo-clippy` binary
 ///
 /// # Panics
@@ -14,34 +92,107 @@ static CARGO_CLIPPY_EXE: &str = "cargo-clippy.exe";
 /// Panics if the path of current executable could not be retrieved.
 #[must_use]
 pub fn cargo_clippy_path() -> PathBuf {
-    let mut path = std::env::current_exe().expect("failed to get current executable name");
+    let mut path = env::current_exe().expect("failed to get current executable name");
     path.set_file_name(CARGO_CLIPPY_EXE);
     path
 }
 
-/// Returns the path to the Clippy project directory
-///
-/// # Panics
-///
-/// Panics if the current directory could not be retrieved, there was an error reading any of the
-/// Cargo.toml files or ancestor directory is the clippy root directory
-#[must_use]
-pub fn clippy_project_root() -> PathBuf {
-    let current_dir = std::env::current_dir().unwrap();
-    for path in current_dir.ancestors() {
-        let result = fs::read_to_string(path.join("Cargo.toml"));
-        if let Err(err) = &result
-            && err.kind() == io::ErrorKind::NotFound
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Version {
+    pub major: u16,
+    pub minor: u16,
+}
+impl FromStr for Version {
+    type Err = ();
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(s) = s.strip_prefix("0.")
+            && let Some((major, minor)) = s.split_once('.')
+            && let Ok(major) = major.parse()
+            && let Ok(minor) = minor.parse()
         {
-            continue;
+            Ok(Self { major, minor })
+        } else {
+            Err(())
+        }
+    }
+}
+impl Version {
+    /// Displays the version as a rust version. i.e. `x.y.0`
+    #[must_use]
+    pub fn rust_display(self) -> impl Display {
+        struct X(Version);
+        impl Display for X {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                write!(f, "{}.{}.0", self.0.major, self.0.minor)
+            }
+        }
+        X(self)
+    }
+
+    /// Displays the version as it should appear in clippy's toml files. i.e. `0.x.y`
+    #[must_use]
+    pub fn toml_display(self) -> impl Display {
+        struct X(Version);
+        impl Display for X {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+                write!(f, "0.{}.{}", self.0.major, self.0.minor)
+            }
         }
+        X(self)
+    }
+}
+
+pub struct ClippyInfo {
+    pub path: PathBuf,
+    pub version: Version,
+}
+impl ClippyInfo {
+    #[must_use]
+    pub fn search_for_manifest() -> Self {
+        let mut path = env::current_dir().expect("error reading the working directory");
+        let mut buf = String::new();
+        loop {
+            path.push("Cargo.toml");
+            if let Some(mut file) = File::open_if_exists(&path, OpenOptions::new().read(true)) {
+                let mut in_package = false;
+                let mut is_clippy = false;
+                let mut version: Option<Version> = None;
+
+                // Ad-hoc parsing to avoid dependencies. We control all the file so this
+                // isn't actually a problem
+                for line in file.read_to_cleared_string(&mut buf).lines() {
+                    if line.starts_with('[') {
+                        in_package = line.starts_with("[package]");
+                    } else if in_package && let Some((name, value)) = line.split_once('=') {
+                        match name.trim() {
+                            "name" => is_clippy = value.trim() == "\"clippy\"",
+                            "version"
+                                if let Some(value) = value.trim().strip_prefix('"')
+                                    && let Some(value) = value.strip_suffix('"') =>
+                            {
+                                version = value.parse().ok();
+                            },
+                            _ => {},
+                        }
+                    }
+                }
 
-        let content = result.unwrap();
-        if content.contains("[package]\nname = \"clippy\"") {
-            return path.to_path_buf();
+                if is_clippy {
+                    let Some(version) = version else {
+                        panic!("error reading clippy version from {}", file.path.display());
+                    };
+                    path.pop();
+                    return ClippyInfo { path, version };
+                }
+            }
+
+            path.pop();
+            assert!(
+                path.pop(),
+                "error finding project root, please run from inside the clippy directory"
+            );
         }
     }
-    panic!("error: Can't determine root of project. Please run inside a Clippy working dir.");
 }
 
 /// # Panics
@@ -57,22 +208,6 @@ pub fn exit_if_err(status: io::Result<ExitStatus>) {
     }
 }
 
-pub(crate) fn clippy_version() -> (u32, u32) {
-    fn parse_manifest(contents: &str) -> Option<(u32, u32)> {
-        let version = contents
-            .lines()
-            .filter_map(|l| l.split_once('='))
-            .find_map(|(k, v)| (k.trim() == "version").then(|| v.trim()))?;
-        let Some(("0", version)) = version.get(1..version.len() - 1)?.split_once('.') else {
-            return None;
-        };
-        let (minor, patch) = version.split_once('.')?;
-        Some((minor.parse().ok()?, patch.parse().ok()?))
-    }
-    let contents = fs::read_to_string("Cargo.toml").expect("Unable to read `Cargo.toml`");
-    parse_manifest(&contents).expect("Unable to find package version in `Cargo.toml`")
-}
-
 #[derive(Clone, Copy, PartialEq, Eq)]
 pub enum UpdateMode {
     Check,