diff options
| author | Jason Newcomb <jsnewcomb@pm.me> | 2025-04-13 07:05:29 -0400 |
|---|---|---|
| committer | Jason Newcomb <jsnewcomb@pm.me> | 2025-05-12 17:07:52 -0400 |
| commit | 2f39264d00eef5e5990438a5c23cb670500548d9 (patch) | |
| tree | cbc2a05f06490d341dabc6f7321bd476377cfd8d | |
| parent | bfc6ad0340dd85f15854388752af8b490ecaee5d (diff) | |
| download | rust-2f39264d00eef5e5990438a5c23cb670500548d9.tar.gz rust-2f39264d00eef5e5990438a5c23cb670500548d9.zip | |
clippy_dev: Set the current directory to clippy's root path.
| -rw-r--r-- | clippy_dev/src/dogfood.rs | 5 | ||||
| -rw-r--r-- | clippy_dev/src/fmt.rs | 32 | ||||
| -rw-r--r-- | clippy_dev/src/lib.rs | 2 | ||||
| -rw-r--r-- | clippy_dev/src/main.rs | 18 | ||||
| -rw-r--r-- | clippy_dev/src/new_lint.rs | 46 | ||||
| -rw-r--r-- | clippy_dev/src/release.rs | 14 | ||||
| -rw-r--r-- | clippy_dev/src/update_lints.rs | 67 | ||||
| -rw-r--r-- | clippy_dev/src/utils.rs | 207 |
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, |
