blob: 9f48832af006fcadbd873334b203caae599cbf7c [file] [log] [blame]
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
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};
#[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
///
/// 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<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()
{
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();
},
_ => {},
}
}
}
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"
);
}
}
}
/// # Panics
/// Panics if given command result was failed.
pub fn exit_if_err(status: io::Result<ExitStatus>) {
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 }
}
}
#[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<Path>,
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<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>,
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)
}
#[must_use]
pub fn is_ident_char(c: u8) -> bool {
matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')
}
pub struct StringReplacer<'a> {
searcher: AhoCorasick,
replacements: &'a [(&'a str, &'a str)],
}
impl<'a> StringReplacer<'a> {
#[must_use]
pub fn new(replacements: &'a [(&'a str, &'a str)]) -> Self {
Self {
searcher: AhoCorasickBuilder::new()
.match_kind(aho_corasick::MatchKind::LeftmostLongest)
.build(replacements.iter().map(|&(x, _)| x))
.unwrap(),
replacements,
}
}
/// Replace substrings if they aren't bordered by identifier characters.
pub fn replace_ident_fn(&self) -> impl Fn(&Path, &str, &mut String) -> UpdateStatus {
move |_, src, dst| {
let mut pos = 0;
let mut changed = false;
for m in self.searcher.find_iter(src) {
if !is_ident_char(src.as_bytes().get(m.start().wrapping_sub(1)).copied().unwrap_or(0))
&& !is_ident_char(src.as_bytes().get(m.end()).copied().unwrap_or(0))
{
changed = true;
dst.push_str(&src[pos..m.start()]);
dst.push_str(self.replacements[m.pattern()].1);
pos = m.end();
}
}
dst.push_str(&src[pos..]);
UpdateStatus::from_changed(changed)
}
}
}
#[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(e) => panic_io(&e, "creating", new_name),
}
match fs::rename(old_name, new_name) {
Ok(()) => true,
Err(e) => {
drop(fs::remove_file(new_name));
if e.kind() == io::ErrorKind::NotFound {
false
} else {
panic_io(&e, "renaming", old_name);
}
},
}
}
#[must_use]
pub fn insert_at_marker(text: &str, marker: &str, new_text: &str) -> Option<String> {
let i = text.find(marker)?;
let (pre, post) = text.split_at(i);
Some([pre, new_text, post].into_iter().collect())
}
pub fn rewrite_file(path: &Path, f: impl FnOnce(&str) -> Option<String>) {
let mut file = OpenOptions::new()
.write(true)
.read(true)
.open(path)
.unwrap_or_else(|e| panic_io(&e, "opening", path));
let mut buf = String::new();
file.read_to_string(&mut buf)
.unwrap_or_else(|e| panic_io(&e, "reading", path));
if let Some(new_contents) = f(&buf) {
file.rewind().unwrap_or_else(|e| panic_io(&e, "writing", path));
file.write_all(new_contents.as_bytes())
.unwrap_or_else(|e| panic_io(&e, "writing", path));
file.set_len(new_contents.len() as u64)
.unwrap_or_else(|e| panic_io(&e, "writing", path));
}
}
pub fn write_file(path: &Path, contents: &str) {
fs::write(path, contents).unwrap_or_else(|e| panic_io(&e, "writing", path));
}