use core::fmt::{self, Display}; use core::num::NonZero; use core::ops::Range; use core::slice; use core::str::FromStr; use rustc_lexer::{self as lexer, FrontmatterAllowed}; use std::ffi::OsStr; use std::fs::{self, OpenOptions}; use std::io::{self, Read as _, Seek as _, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::process::{self, Command, ExitStatus, Stdio}; use std::{env, thread}; use walkdir::WalkDir; #[cfg(not(windows))] static CARGO_CLIPPY_EXE: &str = "cargo-clippy"; #[cfg(windows)] static CARGO_CLIPPY_EXE: &str = "cargo-clippy.exe"; #[derive(Clone, Copy)] pub enum ErrAction { Open, Read, Write, Create, Rename, Delete, Run, } impl ErrAction { fn as_str(self) -> &'static str { match self { Self::Open => "opening", Self::Read => "reading", Self::Write => "writing", Self::Create => "creating", Self::Rename => "renaming", Self::Delete => "deleting", Self::Run => "running", } } } #[cold] #[track_caller] pub fn panic_action(err: &impl Display, action: ErrAction, path: &Path) -> ! { panic!("error {} `{}`: {}", action.as_str(), path.display(), *err) } #[track_caller] pub fn expect_action(res: Result, action: ErrAction, path: impl AsRef) -> T { match res { Ok(x) => x, Err(ref e) => panic_action(e, action, path.as_ref()), } } /// 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 + ?Sized), options: &mut OpenOptions) -> Self { let path = path.as_ref(); Self { inner: expect_action(options.open(path), ErrAction::Open, path), path, } } /// Opens a file if it exists, panicking on any other failure. #[track_caller] pub fn open_if_exists(path: &'a (impl AsRef + ?Sized), options: &mut OpenOptions) -> Option { 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_action(&e, ErrAction::Open, 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 + ?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 { expect_action(self.inner.read_to_string(dst), ErrAction::Read, 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), }; expect_action(res, ErrAction::Write, self.path); } } /// Returns the path to the `cargo-clippy` binary /// /// # Panics /// /// Panics if the path of current executable could not be retrieved. #[must_use] pub fn cargo_clippy_path() -> PathBuf { let mut path = env::current_exe().expect("failed to get current executable name"); path.set_file_name(CARGO_CLIPPY_EXE); path } #[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 { 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() { 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) } } enum TomlPart<'a> { Table(&'a str), Value(&'a str, &'a str), } fn toml_iter(s: &str) -> impl Iterator)> { let mut pos = 0; s.split('\n') .map(move |s| { let x = pos; pos += s.len() + 1; (x, s) }) .filter_map(|(pos, s)| { if let Some(s) = s.strip_prefix('[') { s.split_once(']').map(|(name, _)| (pos, TomlPart::Table(name))) } else if matches!(s.bytes().next(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) { s.split_once('=').map(|(key, value)| (pos, TomlPart::Value(key, value))) } else { None } }) } pub struct CargoPackage<'a> { pub name: &'a str, pub version_range: Range, pub not_a_platform_range: Range, } #[must_use] pub fn parse_cargo_package(s: &str) -> CargoPackage<'_> { let mut in_package = false; let mut in_platform_deps = false; let mut name = ""; let mut version_range = 0..0; let mut not_a_platform_range = 0..0; for (offset, part) in toml_iter(s) { match part { TomlPart::Table(name) => { if in_platform_deps { not_a_platform_range.end = offset; } in_package = false; in_platform_deps = false; match name.trim() { "package" => in_package = true, "target.'cfg(NOT_A_PLATFORM)'.dependencies" => { in_platform_deps = true; not_a_platform_range.start = offset; }, _ => {}, } }, TomlPart::Value(key, value) if in_package => match key.trim_end() { "name" => name = value.trim(), "version" => { version_range.start = offset + (value.len() - value.trim().len()) + key.len() + 1; version_range.end = offset + key.len() + value.trim_end().len() + 1; }, _ => {}, }, TomlPart::Value(..) => {}, } } CargoPackage { name, version_range, not_a_platform_range, } } pub struct ClippyInfo { pub path: PathBuf, pub version: Version, pub has_intellij_hook: bool, } 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)) { file.read_to_cleared_string(&mut buf); let package = parse_cargo_package(&buf); if package.name == "\"clippy\"" { if let Some(version) = buf[package.version_range].strip_prefix('"') && let Some(version) = version.strip_suffix('"') && let Ok(version) = version.parse() { path.pop(); return ClippyInfo { path, version, has_intellij_hook: !package.not_a_platform_range.is_empty(), }; } panic!("error reading clippy version from `{}`", file.path.display()); } } path.pop(); assert!( path.pop(), "error finding project root, please run from inside the clippy directory" ); } } } /// # Panics /// Panics if given command result was failed. pub fn exit_if_err(status: io::Result) { match status.expect("failed to run command").code() { Some(0) => {}, Some(n) => process::exit(n), None => { eprintln!("Killed by signal"); process::exit(1); }, } } #[derive(Clone, Copy)] pub enum UpdateStatus { Unchanged, Changed, } impl UpdateStatus { #[must_use] pub fn from_changed(value: bool) -> Self { if value { Self::Changed } else { Self::Unchanged } } #[must_use] pub fn is_changed(self) -> bool { matches!(self, Self::Changed) } } #[derive(Clone, Copy)] pub enum UpdateMode { Change, Check, } impl UpdateMode { #[must_use] pub fn from_check(check: bool) -> Self { if check { Self::Check } else { Self::Change } } #[must_use] pub fn is_check(self) -> bool { matches!(self, Self::Check) } } #[derive(Default)] pub struct FileUpdater { src_buf: String, dst_buf: String, } impl FileUpdater { fn update_file_checked_inner( &mut self, tool: &str, mode: UpdateMode, path: &Path, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, ) { let mut file = File::open(path, OpenOptions::new().read(true).write(true)); file.read_to_cleared_string(&mut self.src_buf); self.dst_buf.clear(); match (mode, update(path, &self.src_buf, &mut self.dst_buf)) { (UpdateMode::Check, UpdateStatus::Changed) => { eprintln!( "the contents of `{}` are out of date\nplease run `{tool}` to update", path.display() ); process::exit(1); }, (UpdateMode::Change, UpdateStatus::Changed) => file.replace_contents(self.dst_buf.as_bytes()), (UpdateMode::Check | UpdateMode::Change, UpdateStatus::Unchanged) => {}, } } fn update_file_inner(&mut self, path: &Path, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus) { let mut file = File::open(path, OpenOptions::new().read(true).write(true)); file.read_to_cleared_string(&mut self.src_buf); self.dst_buf.clear(); if update(path, &self.src_buf, &mut self.dst_buf).is_changed() { file.replace_contents(self.dst_buf.as_bytes()); } } pub fn update_file_checked( &mut self, tool: &str, mode: UpdateMode, path: impl AsRef, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, ) { 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, &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, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, ) { self.update_file_inner(path.as_ref(), update); } } /// Replaces a region in a text delimited by two strings. Returns the new text if both delimiters /// were found, or the missing delimiter if not. pub fn update_text_region( path: &Path, start: &str, end: &str, src: &str, dst: &mut String, insert: &mut impl FnMut(&mut String), ) -> UpdateStatus { let Some((src_start, src_end)) = src.split_once(start) else { panic!("`{}` does not contain `{start}`", path.display()); }; let Some((replaced_text, src_end)) = src_end.split_once(end) else { panic!("`{}` does not contain `{end}`", path.display()); }; dst.push_str(src_start); dst.push_str(start); let new_start = dst.len(); insert(dst); let changed = dst[new_start..] != *replaced_text; dst.push_str(end); dst.push_str(src_end); UpdateStatus::from_changed(changed) } pub fn update_text_region_fn( start: &str, end: &str, mut insert: impl FnMut(&mut String), ) -> impl FnMut(&Path, &str, &mut String) -> UpdateStatus { move |path, src, dst| update_text_region(path, start, end, src, dst, &mut insert) } #[derive(Clone, Copy)] pub enum Token<'a> { /// Matches any number of comments / doc comments. AnyComment, Ident(&'a str), CaptureIdent, LitStr, CaptureLitStr, Bang, CloseBrace, CloseBracket, CloseParen, /// This will consume the first colon even if the second doesn't exist. DoubleColon, Comma, Eq, Lifetime, Lt, Gt, OpenBrace, OpenBracket, OpenParen, Pound, Semi, Slash, } pub struct RustSearcher<'txt> { text: &'txt str, cursor: lexer::Cursor<'txt>, pos: u32, next_token: lexer::Token, } impl<'txt> RustSearcher<'txt> { #[must_use] #[expect(clippy::inconsistent_struct_constructor)] pub fn new(text: &'txt str) -> Self { let mut cursor = lexer::Cursor::new(text, FrontmatterAllowed::Yes); Self { text, pos: 0, next_token: cursor.advance_token(), cursor, } } #[must_use] pub fn peek_text(&self) -> &'txt str { &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize] } #[must_use] pub fn peek_len(&self) -> u32 { self.next_token.len } #[must_use] pub fn peek(&self) -> lexer::TokenKind { self.next_token.kind } #[must_use] pub fn pos(&self) -> u32 { self.pos } #[must_use] pub fn at_end(&self) -> bool { self.next_token.kind == lexer::TokenKind::Eof } pub fn step(&mut self) { // `next_len` is zero for the sentinel value and the eof marker. self.pos += self.next_token.len; self.next_token = self.cursor.advance_token(); } /// Consumes the next token if it matches the requested value and captures the value if /// requested. Returns true if a token was matched. fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool { loop { match (token, self.next_token.kind) { (_, lexer::TokenKind::Whitespace) | ( Token::AnyComment, lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. }, ) => self.step(), (Token::AnyComment, _) => return true, (Token::Bang, lexer::TokenKind::Bang) | (Token::CloseBrace, lexer::TokenKind::CloseBrace) | (Token::CloseBracket, lexer::TokenKind::CloseBracket) | (Token::CloseParen, lexer::TokenKind::CloseParen) | (Token::Comma, lexer::TokenKind::Comma) | (Token::Eq, lexer::TokenKind::Eq) | (Token::Lifetime, lexer::TokenKind::Lifetime { .. }) | (Token::Lt, lexer::TokenKind::Lt) | (Token::Gt, lexer::TokenKind::Gt) | (Token::OpenBrace, lexer::TokenKind::OpenBrace) | (Token::OpenBracket, lexer::TokenKind::OpenBracket) | (Token::OpenParen, lexer::TokenKind::OpenParen) | (Token::Pound, lexer::TokenKind::Pound) | (Token::Semi, lexer::TokenKind::Semi) | (Token::Slash, lexer::TokenKind::Slash) | ( Token::LitStr, lexer::TokenKind::Literal { kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. }, .. }, ) => { self.step(); return true; }, (Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => { self.step(); return true; }, (Token::DoubleColon, lexer::TokenKind::Colon) => { self.step(); if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) { self.step(); return true; } return false; }, ( Token::CaptureLitStr, lexer::TokenKind::Literal { kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. }, .. }, ) | (Token::CaptureIdent, lexer::TokenKind::Ident) => { **captures.next().unwrap() = self.peek_text(); self.step(); return true; }, _ => return false, } } } #[must_use] pub fn find_token(&mut self, token: Token<'_>) -> bool { let mut capture = [].iter_mut(); while !self.read_token(token, &mut capture) { self.step(); if self.at_end() { return false; } } true } #[must_use] pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> { let mut res = ""; let mut capture = &mut res; let mut capture = slice::from_mut(&mut capture).iter_mut(); while !self.read_token(token, &mut capture) { self.step(); if self.at_end() { return None; } } Some(res) } #[must_use] pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [&mut &'txt str]) -> bool { let mut captures = captures.iter_mut(); tokens.iter().all(|&t| self.read_token(t, &mut captures)) } } #[expect(clippy::must_use_candidate)] pub fn try_rename_file(old_name: &Path, new_name: &Path) -> bool { match OpenOptions::new().create_new(true).write(true).open(new_name) { Ok(file) => drop(file), Err(e) if matches!(e.kind(), io::ErrorKind::AlreadyExists | io::ErrorKind::NotFound) => return false, Err(ref e) => panic_action(e, ErrAction::Create, new_name), } match fs::rename(old_name, new_name) { Ok(()) => true, Err(ref e) => { drop(fs::remove_file(new_name)); // `NotADirectory` happens on posix when renaming a directory to an existing file. // Windows will ignore this and rename anyways. if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) { false } else { panic_action(e, ErrAction::Rename, old_name); } }, } } #[expect(clippy::must_use_candidate)] pub fn try_rename_dir(old_name: &Path, new_name: &Path) -> bool { match fs::create_dir(new_name) { Ok(()) => {}, Err(e) if matches!(e.kind(), io::ErrorKind::AlreadyExists | io::ErrorKind::NotFound) => return false, Err(ref e) => panic_action(e, ErrAction::Create, new_name), } // Windows can't reliably rename to an empty directory. #[cfg(windows)] drop(fs::remove_dir(new_name)); match fs::rename(old_name, new_name) { Ok(()) => true, Err(ref e) => { // Already dropped earlier on windows. #[cfg(not(windows))] drop(fs::remove_dir(new_name)); // `NotADirectory` happens on posix when renaming a file to an existing directory. if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) { false } else { panic_action(e, ErrAction::Rename, old_name); } }, } } pub fn write_file(path: &Path, contents: &str) { expect_action(fs::write(path, contents), ErrAction::Write, path); } #[must_use] pub fn run_with_output(path: &(impl AsRef + ?Sized), cmd: &mut Command) -> Vec { fn f(path: &Path, cmd: &mut Command) -> Vec { let output = expect_action( cmd.stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .output(), ErrAction::Run, path, ); expect_action(output.status.exit_ok(), ErrAction::Run, path); output.stdout } f(path.as_ref(), cmd) } /// Splits an argument list across multiple `Command` invocations. /// /// The argument list will be split into a number of batches based on /// `thread::available_parallelism`, with `min_batch_size` setting a lower bound on the size of each /// batch. /// /// If the size of the arguments would exceed the system limit additional batches will be created. pub fn split_args_for_threads( min_batch_size: usize, make_cmd: impl FnMut() -> Command, args: impl ExactSizeIterator>, ) -> impl Iterator { struct Iter { make_cmd: F, args: I, min_batch_size: usize, batch_size: usize, thread_count: usize, } impl Iterator for Iter where F: FnMut() -> Command, I: ExactSizeIterator>, { type Item = Command; fn next(&mut self) -> Option { if self.thread_count > 1 { self.thread_count -= 1; } let mut cmd = (self.make_cmd)(); let mut cmd_len = 0usize; for arg in self.args.by_ref().take(self.batch_size) { cmd.arg(arg.as_ref()); // `+ 8` to account for the `argv` pointer on unix. // Windows is complicated since the arguments are first converted to UTF-16ish, // but this needs to account for the space between arguments and whatever additional // is needed to escape within an argument. cmd_len += arg.as_ref().len() + 8; cmd_len += 8; // Windows has a command length limit of 32767. For unix systems this is more // complicated since the limit includes environment variables and room needs to be // left to edit them once the program starts, but the total size comes from // `getconf ARG_MAX`. // // For simplicity we use 30000 here under a few assumptions. // * Individual arguments aren't super long (the final argument is still added) // * `ARG_MAX` is set to a reasonable amount. Basically every system will be configured way above // what windows supports, but POSIX only requires `4096`. if cmd_len > 30000 { self.batch_size = self.args.len().div_ceil(self.thread_count).max(self.min_batch_size); break; } } (cmd_len != 0).then_some(cmd) } } let thread_count = thread::available_parallelism().map_or(1, NonZero::get); let batch_size = args.len().div_ceil(thread_count).max(min_batch_size); Iter { make_cmd, args, min_batch_size, batch_size, thread_count, } } #[expect(clippy::must_use_candidate)] pub fn delete_file_if_exists(path: &Path) -> bool { match fs::remove_file(path) { Ok(()) => true, Err(e) if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::IsADirectory) => false, Err(ref e) => panic_action(e, ErrAction::Delete, path), } } pub fn delete_dir_if_exists(path: &Path) { match fs::remove_dir_all(path) { Ok(()) => {}, Err(e) if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) => {}, Err(ref e) => panic_action(e, ErrAction::Delete, path), } } /// Walks all items excluding top-level dot files/directories and any target directories. pub fn walk_dir_no_dot_or_target() -> impl Iterator> { WalkDir::new(".").into_iter().filter_entry(|e| { e.path() .file_name() .is_none_or(|x| x != "target" && x.as_encoded_bytes().first().copied() != Some(b'.')) }) }