diff options
| author | Jason Newcomb <jsnewcomb@pm.me> | 2025-04-14 13:40:05 -0400 |
|---|---|---|
| committer | Jason Newcomb <jsnewcomb@pm.me> | 2025-05-12 17:07:52 -0400 |
| commit | 98cb92f3235c4c963d1c17e8a833f37091b5b487 (patch) | |
| tree | 83a2f29a83d13fd2655c7f8645db07b0828104a8 | |
| parent | 3fe5fb296786e9396c1e91280360d9f029367b44 (diff) | |
clippy_dev: Reuse buffers when updating files and don't write unchanged files in `clippy_dev`
| -rw-r--r-- | clippy_dev/src/main.rs | 4 | ||||
| -rw-r--r-- | clippy_dev/src/release.rs | 26 | ||||
| -rw-r--r-- | clippy_dev/src/rename_lint.rs | 17 | ||||
| -rw-r--r-- | clippy_dev/src/sync.rs | 31 | ||||
| -rw-r--r-- | clippy_dev/src/update_lints.rs | 206 | ||||
| -rw-r--r-- | clippy_dev/src/utils.rs | 207 |
6 files changed, 252 insertions, 239 deletions
diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs index 565416572a9..73248d72d04 100644 --- a/clippy_dev/src/main.rs +++ b/clippy_dev/src/main.rs @@ -30,10 +30,8 @@ fn main() { DevCommand::UpdateLints { print_only, check } => { if print_only { update_lints::print_lints(); - } else if check { - update_lints::update(utils::UpdateMode::Check); } else { - update_lints::update(utils::UpdateMode::Change); + update_lints::update(utils::UpdateMode::from_check(check)); } }, DevCommand::NewLint { diff --git a/clippy_dev/src/release.rs b/clippy_dev/src/release.rs index 34f81e10a39..d3b1a7ff320 100644 --- a/clippy_dev/src/release.rs +++ b/clippy_dev/src/release.rs @@ -1,8 +1,7 @@ +use crate::utils::{FileUpdater, Version, update_text_region_fn}; use std::fmt::Write; -use crate::utils::{UpdateMode, Version, replace_region_in_file}; - -const CARGO_TOML_FILES: [&str; 4] = [ +static CARGO_TOML_FILES: &[&str] = &[ "clippy_config/Cargo.toml", "clippy_lints/Cargo.toml", "clippy_utils/Cargo.toml", @@ -11,15 +10,18 @@ const CARGO_TOML_FILES: [&str; 4] = [ pub fn bump_version(mut version: Version) { version.minor += 1; - for &file in &CARGO_TOML_FILES { - replace_region_in_file( - UpdateMode::Change, - file.as_ref(), - "# begin autogenerated version\n", - "# end autogenerated version", - |res| { - writeln!(res, "version = \"{}\"", version.toml_display()).unwrap(); - }, + + let mut updater = FileUpdater::default(); + for file in CARGO_TOML_FILES { + updater.update_file( + file, + &mut update_text_region_fn( + "# begin autogenerated version\n", + "# end autogenerated version", + |dst| { + writeln!(dst, "version = \"{}\"", version.toml_display()).unwrap(); + }, + ), ); } } diff --git a/clippy_dev/src/rename_lint.rs b/clippy_dev/src/rename_lint.rs index 045e1f83e13..25db95a4489 100644 --- a/clippy_dev/src/rename_lint.rs +++ b/clippy_dev/src/rename_lint.rs @@ -1,8 +1,8 @@ use crate::update_lints::{ - RenamedLint, clippy_lints_src_files, gather_all, gen_renamed_lints_test, generate_lint_files, + RenamedLint, clippy_lints_src_files, gather_all, gen_renamed_lints_test_fn, generate_lint_files, }; use crate::utils::{ - UpdateMode, Version, insert_at_marker, replace_ident_like, rewrite_file, try_rename_file, write_file, + FileUpdater, UpdateMode, Version, insert_at_marker, replace_ident_like, rewrite_file, try_rename_file, }; use std::ffi::OsStr; use std::path::Path; @@ -32,6 +32,7 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b panic!("`{new_name}` should not contain the `{prefix}` prefix"); } + let mut updater = FileUpdater::default(); let (mut lints, deprecated_lints, mut renamed_lints) = gather_all(); let mut old_lint_index = None; let mut found_new_name = false; @@ -72,8 +73,8 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b && 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)]) + updater.update_file(file.path(), &mut |_, src, dst| { + replace_ident_like(&[(&lint.old_name, &lint.new_name)], src, dst) }); } @@ -101,12 +102,12 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b }); if uplift { - write_file(Path::new("tests/ui/rename.rs"), &gen_renamed_lints_test(&renamed_lints)); + updater.update_file("tests/ui/rename.rs", &mut gen_renamed_lints_test_fn(&renamed_lints)); println!( "`{old_name}` has be uplifted. All the code inside `clippy_lints` related to it needs to be removed manually." ); } else if found_new_name { - write_file(Path::new("tests/ui/rename.rs"), &gen_renamed_lints_test(&renamed_lints)); + updater.update_file("tests/ui/rename.rs", &mut gen_renamed_lints_test_fn(&renamed_lints)); println!( "`{new_name}` is already defined. The old linting code inside `clippy_lints` needs to be updated/removed manually." ); @@ -173,7 +174,9 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b .to_str() .is_none_or(|x| x["clippy_lints/src/".len()..] != *"deprecated_lints.rs") { - rewrite_file(file.path(), |s| replace_ident_like(s, replacements)); + updater.update_file(file.path(), &mut |_, src, dst| { + replace_ident_like(replacements, src, dst) + }); } } diff --git a/clippy_dev/src/sync.rs b/clippy_dev/src/sync.rs index a6b65e561c2..c699b0d7b95 100644 --- a/clippy_dev/src/sync.rs +++ b/clippy_dev/src/sync.rs @@ -1,33 +1,18 @@ -use std::fmt::Write; -use std::path::Path; - +use crate::utils::{FileUpdater, update_text_region_fn}; use chrono::offset::Utc; - -use crate::utils::{UpdateMode, replace_region_in_file}; +use std::fmt::Write; pub fn update_nightly() { - // Update rust-toolchain nightly version let date = Utc::now().format("%Y-%m-%d").to_string(); - replace_region_in_file( - UpdateMode::Change, - Path::new("rust-toolchain.toml"), + let update = &mut update_text_region_fn( "# begin autogenerated nightly\n", "# end autogenerated nightly", - |res| { - writeln!(res, "channel = \"nightly-{date}\"").unwrap(); + |dst| { + writeln!(dst, "channel = \"nightly-{date}\"").unwrap(); }, ); - // Update clippy_utils nightly version - replace_region_in_file( - UpdateMode::Change, - Path::new("clippy_utils/README.md"), - "<!-- begin autogenerated nightly -->\n", - "<!-- end autogenerated nightly -->", - |res| { - writeln!(res, "```").unwrap(); - writeln!(res, "nightly-{date}").unwrap(); - writeln!(res, "```").unwrap(); - }, - ); + let mut updater = FileUpdater::default(); + updater.update_file("rust-toolchain.toml", update); + updater.update_file("clippy_utils/README.md", update); } diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs index 2f450b7d9ea..ad995f5e4c2 100644 --- a/clippy_dev/src/update_lints.rs +++ b/clippy_dev/src/update_lints.rs @@ -1,10 +1,10 @@ -use crate::utils::{UpdateMode, exit_with_failure, replace_region_in_file}; +use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn}; use itertools::Itertools; use rustc_lexer::{LiteralKind, TokenKind, tokenize}; use rustc_literal_escaper::{Mode, unescape_unicode}; use std::collections::{HashMap, HashSet}; use std::ffi::OsStr; -use std::fmt::{self, Write}; +use std::fmt::Write; use std::fs; use std::ops::Range; use std::path::Path; @@ -33,74 +33,77 @@ pub fn update(update_mode: UpdateMode) { pub fn generate_lint_files( update_mode: UpdateMode, lints: &[Lint], - deprecated_lints: &[DeprecatedLint], - renamed_lints: &[RenamedLint], + deprecated: &[DeprecatedLint], + renamed: &[RenamedLint], ) { let mut lints = lints.to_owned(); - lints.sort_by_key(|lint| lint.name.clone()); - - replace_region_in_file( - update_mode, - Path::new("README.md"), - "[There are over ", - " lints included in this crate!]", - |res| { - write!(res, "{}", round_to_fifty(lints.len())).unwrap(); - }, - ); - - replace_region_in_file( - update_mode, - Path::new("book/src/README.md"), - "[There are over ", - " lints included in this crate!]", - |res| { - write!(res, "{}", round_to_fifty(lints.len())).unwrap(); - }, - ); - - replace_region_in_file( - update_mode, - Path::new("CHANGELOG.md"), - "<!-- begin autogenerated links to lint list -->\n", - "<!-- end autogenerated links to lint list -->", - |res| { - for lint in lints - .iter() - .map(|l| &*l.name) - .chain(deprecated_lints.iter().filter_map(|l| l.name.strip_prefix("clippy::"))) - .chain(renamed_lints.iter().filter_map(|l| l.old_name.strip_prefix("clippy::"))) - .sorted() - { - writeln!(res, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap(); - } - }, - ); - - // This has to be in lib.rs, otherwise rustfmt doesn't work - replace_region_in_file( - update_mode, - Path::new("clippy_lints/src/lib.rs"), - "// 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`", - |res| { - for lint_mod in lints.iter().map(|l| &l.module).unique().sorted() { - writeln!(res, "mod {lint_mod};").unwrap(); - } - }, - ); - - process_file( - "clippy_lints/src/declared_lints.rs", + lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + FileUpdater::default().update_files_checked( + "cargo dev update_lints", update_mode, - &gen_declared_lints(lints.iter()), + &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) + }), + ("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) + }), + ("tests/ui/rename.rs", &mut gen_renamed_lints_test_fn(renamed)), + ], ); - - let content = gen_deprecated_lints_test(deprecated_lints); - process_file("tests/ui/deprecated.rs", update_mode, &content); - - let content = gen_renamed_lints_test(renamed_lints); - process_file("tests/ui/rename.rs", update_mode, &content); } pub fn print_lints() { @@ -125,19 +128,6 @@ fn round_to_fifty(count: usize) -> usize { count / 50 * 50 } -fn process_file(path: impl AsRef<Path>, update_mode: UpdateMode, content: &str) { - if update_mode == UpdateMode::Check { - let old_content = - fs::read_to_string(&path).unwrap_or_else(|e| panic!("Cannot read from {}: {e}", path.as_ref().display())); - if content != old_content { - exit_with_failure(); - } - } else { - fs::write(&path, content.as_bytes()) - .unwrap_or_else(|e| panic!("Cannot write to {}: {e}", path.as_ref().display())); - } -} - /// Lint data parsed from the Clippy source code. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Lint { @@ -194,51 +184,25 @@ impl RenamedLint { } } -/// Generates the code for registering lints -#[must_use] -fn gen_declared_lints<'a>(lints: impl Iterator<Item = &'a Lint>) -> String { - let mut details: Vec<_> = lints.map(|l| (&l.module, l.name.to_uppercase())).collect(); - details.sort_unstable(); - - let mut output = GENERATED_FILE_COMMENT.to_string(); - output.push_str("pub static LINTS: &[&crate::LintInfo] = &[\n"); - - for (module_name, lint_name) in details { - let _: fmt::Result = writeln!(output, " crate::{module_name}::{lint_name}_INFO,"); - } - output.push_str("];\n"); - - output -} - -fn gen_deprecated_lints_test(lints: &[DeprecatedLint]) -> String { - let mut res: String = GENERATED_FILE_COMMENT.into(); - for lint in lints { - writeln!(res, "#![warn({})] //~ ERROR: lint `{}`", lint.name, lint.name).unwrap(); - } - res.push_str("\nfn main() {}\n"); - res -} - -#[must_use] -pub fn gen_renamed_lints_test(lints: &[RenamedLint]) -> String { - let mut seen_lints = HashSet::new(); - let mut res: String = GENERATED_FILE_COMMENT.into(); - - res.push_str("#![allow(clippy::duplicated_attributes)]\n"); - for lint in lints { - if seen_lints.insert(&lint.new_name) { - writeln!(res, "#![allow({})]", lint.new_name).unwrap(); +pub fn gen_renamed_lints_test_fn(lints: &[RenamedLint]) -> impl Fn(&Path, &str, &mut String) -> UpdateStatus { + 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 lints { + if seen_lints.insert(&lint.new_name) { + writeln!(dst, "#![allow({})]", lint.new_name).unwrap(); + } } - } - seen_lints.clear(); - for lint in lints { - if seen_lints.insert(&lint.old_name) { - writeln!(res, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap(); + seen_lints.clear(); + for lint in lints { + 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) } - res.push_str("\nfn main() {}\n"); - res } /// Gathers all lints defined in `clippy_lints/src` diff --git a/clippy_dev/src/utils.rs b/clippy_dev/src/utils.rs index 38a839c16a7..aaacdcf65c1 100644 --- a/clippy_dev/src/utils.rs +++ b/clippy_dev/src/utils.rs @@ -209,78 +209,145 @@ pub fn exit_if_err(status: io::Result<ExitStatus>) { } } -#[derive(Clone, Copy, PartialEq, Eq)] +#[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 { - Check, Change, + Check, +} +impl UpdateMode { + #[must_use] + pub fn from_check(check: bool) -> Self { + if check { Self::Check } else { Self::Change } + } } -pub(crate) fn exit_with_failure() { - println!( - "Not all lints defined properly. \ - Please run `cargo dev update_lints` to make sure all lints are defined properly." - ); - process::exit(1); +#[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) => {}, + } + } -/// Replaces a region in a file delimited by two lines matching regexes. -/// -/// `path` is the relative path to the file on which you want to perform the replacement. -/// -/// See `replace_region_in_text` for documentation of the other options. -/// -/// # Panics -/// -/// Panics if the path could not read or then written -pub(crate) fn replace_region_in_file( - update_mode: UpdateMode, - path: &Path, - start: &str, - end: &str, - write_replacement: impl FnMut(&mut String), -) { - let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {e}", path.display())); - let new_contents = match replace_region_in_text(&contents, start, end, write_replacement) { - Ok(x) => x, - Err(delim) => panic!("Couldn't find `{delim}` in file `{}`", path.display()), - }; + 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()); + } + } - match update_mode { - UpdateMode::Check if contents != new_contents => exit_with_failure(), - UpdateMode::Check => (), - UpdateMode::Change => { - if let Err(e) = fs::write(path, new_contents.as_bytes()) { - panic!("Cannot write to `{}`: {e}", path.display()); - } - }, + 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(crate) fn replace_region_in_text<'a>( - text: &str, - start: &'a str, - end: &'a str, - mut write_replacement: impl FnMut(&mut String), -) -> Result<String, &'a str> { - let (text_start, rest) = text.split_once(start).ok_or(start)?; - let (_, text_end) = rest.split_once(end).ok_or(end)?; - - let mut res = String::with_capacity(text.len() + 4096); - res.push_str(text_start); - res.push_str(start); - write_replacement(&mut res); - res.push_str(end); - res.push_str(text_end); - - Ok(res) +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) } /// Replace substrings if they aren't bordered by identifier characters. Returns `None` if there /// were no replacements. #[must_use] -pub fn replace_ident_like(contents: &str, replacements: &[(&str, &str)]) -> Option<String> { +pub fn replace_ident_like(replacements: &[(&str, &str)], src: &str, dst: &mut String) -> UpdateStatus { fn is_ident_char(c: u8) -> bool { matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') } @@ -290,26 +357,20 @@ pub fn replace_ident_like(contents: &str, replacements: &[(&str, &str)]) -> Opti .build(replacements.iter().map(|&(x, _)| x.as_bytes())) .unwrap(); - let mut result = String::with_capacity(contents.len() + 1024); let mut pos = 0; - let mut edited = false; - for m in searcher.find_iter(contents) { - let (old, new) = replacements[m.pattern()]; - result.push_str(&contents[pos..m.start()]); - result.push_str( - if !is_ident_char(contents.as_bytes().get(m.start().wrapping_sub(1)).copied().unwrap_or(0)) - && !is_ident_char(contents.as_bytes().get(m.end()).copied().unwrap_or(0)) - { - edited = true; - new - } else { - old - }, - ); - pos = m.end(); + let mut changed = false; + for m in 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)) + { + dst.push_str(&src[pos..m.start()]); + dst.push_str(replacements[m.pattern()].1); + pos = m.end(); + changed = true; + } } - result.push_str(&contents[pos..]); - edited.then_some(result) + dst.push_str(&src[pos..]); + UpdateStatus::from_changed(changed) } #[expect(clippy::must_use_candidate)] |
