about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2022-04-02 10:42:12 +0000
committerbors <bors@rust-lang.org>2022-04-02 10:42:12 +0000
commit85b88be2a4fc46fc27d51d4fb273d65818ee63d7 (patch)
treee3f28fc56bb67200f7904895f8839ef218cf0765
parentbaaddf2b84008c6128b928472660bafa56e41b37 (diff)
parentae5af0cd1a3f59cc3537ade7dfba06989969b918 (diff)
downloadrust-85b88be2a4fc46fc27d51d4fb273d65818ee63d7.tar.gz
rust-85b88be2a4fc46fc27d51d4fb273d65818ee63d7.zip
Auto merge of #8605 - Jarcho:remove-deps, r=xFrednet
Remove deps

This remove both `regex` and `cargo_metadata` as dependencies making `clippy_dev` compile ~3x faster (~46s -> ~16s locally). `cargo_metadata` was used to extract the `version` field from `Cargo.toml`, which is done trivially without that. `regex` was used to parse `define_clippy_lint` in `update_lints` which is now done using `rustc_lexer`. This isn't any simpler, but it compiles ~15s faster and runs ~3x faster (~2.1s -> ~0.7s locally).

The next biggest offenders to compile times are `clap` and `winapi` on windows. `clap` could be removed, but re-implementing enough is probably more work than it's worth. `winapi` is used by `opener` and `walkdir` so it's stuck there.

changelog: none
-rw-r--r--Cargo.toml5
-rw-r--r--clippy_dev/Cargo.toml7
-rw-r--r--clippy_dev/src/lib.rs4
-rw-r--r--clippy_dev/src/new_lint.rs24
-rw-r--r--clippy_dev/src/update_lints.rs651
-rw-r--r--tests/versioncheck.rs34
6 files changed, 313 insertions, 412 deletions
diff --git a/Cargo.toml b/Cargo.toml
index b9ba9fe7350..bc7833f8581 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,13 +21,12 @@ name = "clippy-driver"
 path = "src/driver.rs"
 
 [dependencies]
-clippy_lints = { version = "0.1", path = "clippy_lints" }
+clippy_lints = { path = "clippy_lints" }
 semver = "1.0"
-rustc_tools_util = { version = "0.2", path = "rustc_tools_util" }
+rustc_tools_util = { path = "rustc_tools_util" }
 tempfile = { version = "3.2", optional = true }
 
 [dev-dependencies]
-cargo_metadata = "0.14"
 compiletest_rs = { version = "0.7.1", features = ["tmp"] }
 tester = "0.9"
 regex = "1.5"
diff --git a/clippy_dev/Cargo.toml b/clippy_dev/Cargo.toml
index d133e8cddab..1f2d8adecee 100644
--- a/clippy_dev/Cargo.toml
+++ b/clippy_dev/Cargo.toml
@@ -4,15 +4,16 @@ version = "0.0.1"
 edition = "2021"
 
 [dependencies]
-bytecount = "0.6"
 clap = "2.33"
 indoc = "1.0"
 itertools = "0.10.1"
 opener = "0.5"
-regex = "1.5"
 shell-escape = "0.1"
 walkdir = "2.3"
-cargo_metadata = "0.14"
 
 [features]
 deny-warnings = []
+
+[package.metadata.rust-analyzer]
+# This package uses #[feature(rustc_private)]
+rustc_private = true
diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs
index 59fde447547..414b403827d 100644
--- a/clippy_dev/src/lib.rs
+++ b/clippy_dev/src/lib.rs
@@ -1,8 +1,12 @@
+#![feature(let_else)]
 #![feature(once_cell)]
+#![feature(rustc_private)]
 #![cfg_attr(feature = "deny-warnings", deny(warnings))]
 // warn on lints, that are included in `rust-lang/rust`s bootstrap
 #![warn(rust_2018_idioms, unused_lifetimes)]
 
+extern crate rustc_lexer;
+
 use std::path::PathBuf;
 
 pub mod bless;
diff --git a/clippy_dev/src/new_lint.rs b/clippy_dev/src/new_lint.rs
index 59658b42c79..7a3fd131761 100644
--- a/clippy_dev/src/new_lint.rs
+++ b/clippy_dev/src/new_lint.rs
@@ -133,15 +133,23 @@ fn to_camel_case(name: &str) -> String {
 }
 
 fn get_stabilisation_version() -> String {
-    let mut command = cargo_metadata::MetadataCommand::new();
-    command.no_deps();
-    if let Ok(metadata) = command.exec() {
-        if let Some(pkg) = metadata.packages.iter().find(|pkg| pkg.name == "clippy") {
-            return format!("{}.{}.0", pkg.version.minor, pkg.version.patch);
-        }
+    fn parse_manifest(contents: &str) -> Option<String> {
+        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(format!(
+            "{}.{}.0",
+            minor.parse::<u32>().ok()?,
+            patch.parse::<u32>().ok()?
+        ))
     }
-
-    String::from("<TODO set version(see doc/adding_lints.md)>")
+    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`")
 }
 
 fn get_test_file_contents(lint_name: &str, header_commands: Option<&str>) -> String {
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index d368ef1f46a..4e48b670457 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -1,9 +1,9 @@
+use core::fmt::Write;
 use itertools::Itertools;
-use regex::Regex;
+use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind};
 use std::collections::HashMap;
 use std::ffi::OsStr;
 use std::fs;
-use std::lazy::SyncLazy;
 use std::path::Path;
 use walkdir::WalkDir;
 
@@ -13,35 +13,7 @@ const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev u
      // Use that command to update this file and do not edit by hand.\n\
      // Manual edits will be overwritten.\n\n";
 
-static DEC_CLIPPY_LINT_RE: SyncLazy<Regex> = SyncLazy::new(|| {
-    Regex::new(
-        r#"(?x)
-    declare_clippy_lint!\s*[\{(]
-    (?:\s+///.*)*
-    (?:\s*\#\[clippy::version\s*=\s*"[^"]*"\])?
-    \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s*
-    (?P<cat>[a-z_]+)\s*,\s*
-    "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})]
-"#,
-    )
-    .unwrap()
-});
-
-static DEC_DEPRECATED_LINT_RE: SyncLazy<Regex> = SyncLazy::new(|| {
-    Regex::new(
-        r#"(?x)
-    declare_deprecated_lint!\s*[{(]\s*
-    (?:\s+///.*)*
-    (?:\s*\#\[clippy::version\s*=\s*"[^"]*"\])?
-    \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s*
-    "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})]
-"#,
-    )
-    .unwrap()
-});
-static NL_ESCAPE_RE: SyncLazy<Regex> = SyncLazy::new(|| Regex::new(r#"\\\n\s*"#).unwrap());
-
-static DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
+const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
 
 #[derive(Clone, Copy, PartialEq)]
 pub enum UpdateMode {
@@ -60,60 +32,52 @@ pub enum UpdateMode {
 /// Panics if a file path could not read from or then written to
 #[allow(clippy::too_many_lines)]
 pub fn run(update_mode: UpdateMode) {
-    let lint_list: Vec<Lint> = gather_all().collect();
+    let (lints, deprecated_lints) = gather_all();
 
-    let internal_lints = Lint::internal_lints(&lint_list);
-    let deprecated_lints = Lint::deprecated_lints(&lint_list);
-    let usable_lints = Lint::usable_lints(&lint_list);
+    let internal_lints = Lint::internal_lints(&lints);
+    let usable_lints = Lint::usable_lints(&lints);
     let mut sorted_usable_lints = usable_lints.clone();
     sorted_usable_lints.sort_by_key(|lint| lint.name.clone());
 
-    let usable_lint_count = round_to_fifty(usable_lints.len());
-
-    let mut file_change = false;
-
-    file_change |= replace_region_in_file(
+    replace_region_in_file(
+        update_mode,
         Path::new("README.md"),
-        &format!(
-            r#"\[There are over \d+ lints included in this crate!\]\({}\)"#,
-            DOCS_LINK
-        ),
-        "",
-        true,
-        update_mode == UpdateMode::Change,
-        || {
-            vec![format!(
-                "[There are over {} lints included in this crate!]({})",
-                usable_lint_count, DOCS_LINK
-            )]
+        "[There are over ",
+        " lints included in this crate!]",
+        |res| {
+            write!(res, "{}", round_to_fifty(usable_lints.len())).unwrap();
         },
-    )
-    .changed;
+    );
 
-    file_change |= replace_region_in_file(
+    replace_region_in_file(
+        update_mode,
         Path::new("CHANGELOG.md"),
-        "<!-- begin autogenerated links to lint list -->",
+        "<!-- begin autogenerated links to lint list -->\n",
         "<!-- end autogenerated links to lint list -->",
-        false,
-        update_mode == UpdateMode::Change,
-        || gen_changelog_lint_list(usable_lints.iter().chain(deprecated_lints.iter())),
-    )
-    .changed;
+        |res| {
+            for lint in usable_lints
+                .iter()
+                .map(|l| &l.name)
+                .chain(deprecated_lints.iter().map(|l| &l.name))
+                .sorted()
+            {
+                writeln!(res, "[`{}`]: {}#{}", lint, DOCS_LINK, lint).unwrap();
+            }
+        },
+    );
 
     // This has to be in lib.rs, otherwise rustfmt doesn't work
-    file_change |= replace_region_in_file(
+    replace_region_in_file(
+        update_mode,
         Path::new("clippy_lints/src/lib.rs"),
-        "begin lints modules",
-        "end lints modules",
-        false,
-        update_mode == UpdateMode::Change,
-        || gen_modules_list(usable_lints.iter()),
-    )
-    .changed;
-
-    if file_change && update_mode == UpdateMode::Check {
-        exit_with_failure();
-    }
+        "// 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 usable_lints.iter().map(|l| &l.module).unique().sorted() {
+                writeln!(res, "mod {};", lint_mod).unwrap();
+            }
+        },
+    );
 
     process_file(
         "clippy_lints/src/lib.register_lints.rs",
@@ -123,7 +87,7 @@ pub fn run(update_mode: UpdateMode) {
     process_file(
         "clippy_lints/src/lib.deprecated.rs",
         update_mode,
-        &gen_deprecated(deprecated_lints.iter()),
+        &gen_deprecated(&deprecated_lints),
     );
 
     let all_group_lints = usable_lints.iter().filter(|l| {
@@ -146,15 +110,12 @@ pub fn run(update_mode: UpdateMode) {
 }
 
 pub fn print_lints() {
-    let lint_list: Vec<Lint> = gather_all().collect();
+    let (lint_list, _) = gather_all();
     let usable_lints = Lint::usable_lints(&lint_list);
     let usable_lint_count = usable_lints.len();
     let grouped_by_lint_group = Lint::by_lint_group(usable_lints.into_iter());
 
     for (lint_group, mut lints) in grouped_by_lint_group {
-        if lint_group == "Deprecated" {
-            continue;
-        }
         println!("\n## {}", lint_group);
 
         lints.sort_by_key(|l| l.name.clone());
@@ -198,19 +159,17 @@ struct Lint {
     name: String,
     group: String,
     desc: String,
-    deprecation: Option<String>,
     module: String,
 }
 
 impl Lint {
     #[must_use]
-    fn new(name: &str, group: &str, desc: &str, deprecation: Option<&str>, module: &str) -> Self {
+    fn new(name: &str, group: &str, desc: &str, module: &str) -> Self {
         Self {
             name: name.to_lowercase(),
-            group: group.to_string(),
-            desc: NL_ESCAPE_RE.replace(&desc.replace("\\\"", "\""), "").to_string(),
-            deprecation: deprecation.map(ToString::to_string),
-            module: module.to_string(),
+            group: group.into(),
+            desc: remove_line_splices(desc),
+            module: module.into(),
         }
     }
 
@@ -219,7 +178,7 @@ impl Lint {
     fn usable_lints(lints: &[Self]) -> Vec<Self> {
         lints
             .iter()
-            .filter(|l| l.deprecation.is_none() && !l.group.starts_with("internal"))
+            .filter(|l| !l.group.starts_with("internal"))
             .cloned()
             .collect()
     }
@@ -230,12 +189,6 @@ impl Lint {
         lints.iter().filter(|l| l.group == "internal").cloned().collect()
     }
 
-    /// Returns all deprecated lints
-    #[must_use]
-    fn deprecated_lints(lints: &[Self]) -> Vec<Self> {
-        lints.iter().filter(|l| l.deprecation.is_some()).cloned().collect()
-    }
-
     /// Returns the lints in a `HashMap`, grouped by the different lint groups
     #[must_use]
     fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>> {
@@ -243,6 +196,20 @@ impl Lint {
     }
 }
 
+#[derive(Clone, PartialEq, Debug)]
+struct DeprecatedLint {
+    name: String,
+    reason: String,
+}
+impl DeprecatedLint {
+    fn new(name: &str, reason: &str) -> Self {
+        Self {
+            name: name.to_lowercase(),
+            reason: remove_line_splices(reason),
+        }
+    }
+}
+
 /// Generates the code for registering a group
 fn gen_lint_group_list<'a>(group_name: &str, lints: impl Iterator<Item = &'a Lint>) -> String {
     let mut details: Vec<_> = lints.map(|l| (&l.module, l.name.to_uppercase())).collect();
@@ -262,32 +229,12 @@ fn gen_lint_group_list<'a>(group_name: &str, lints: impl Iterator<Item = &'a Lin
     output
 }
 
-/// Generates the module declarations for `lints`
-#[must_use]
-fn gen_modules_list<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String> {
-    lints
-        .map(|l| &l.module)
-        .unique()
-        .map(|module| format!("mod {};", module))
-        .sorted()
-        .collect::<Vec<String>>()
-}
-
-/// Generates the list of lint links at the bottom of the CHANGELOG
-#[must_use]
-fn gen_changelog_lint_list<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String> {
-    lints
-        .sorted_by_key(|l| &l.name)
-        .map(|l| format!("[`{}`]: {}#{}", l.name, DOCS_LINK, l.name))
-        .collect()
-}
-
 /// Generates the `register_removed` code
 #[must_use]
-fn gen_deprecated<'a>(lints: impl Iterator<Item = &'a Lint>) -> String {
+fn gen_deprecated(lints: &[DeprecatedLint]) -> String {
     let mut output = GENERATED_FILE_COMMENT.to_string();
     output.push_str("{\n");
-    for Lint { name, deprecation, .. } in lints {
+    for lint in lints {
         output.push_str(&format!(
             concat!(
                 "    store.register_removed(\n",
@@ -295,8 +242,7 @@ fn gen_deprecated<'a>(lints: impl Iterator<Item = &'a Lint>) -> String {
                 "        \"{}\",\n",
                 "    );\n"
             ),
-            name,
-            deprecation.as_ref().expect("`lints` are deprecated")
+            lint.name, lint.reason,
         ));
     }
     output.push_str("}\n");
@@ -330,61 +276,133 @@ fn gen_register_lint_list<'a>(
     output
 }
 
-/// Gathers all files in `src/clippy_lints` and gathers all lints inside
-fn gather_all() -> impl Iterator<Item = Lint> {
-    lint_files().flat_map(|f| gather_from_file(&f))
-}
+/// Gathers all lints defined in `clippy_lints/src`
+fn gather_all() -> (Vec<Lint>, Vec<DeprecatedLint>) {
+    let mut lints = Vec::with_capacity(1000);
+    let mut deprecated_lints = Vec::with_capacity(50);
+    let root_path = clippy_project_root().join("clippy_lints/src");
 
-fn gather_from_file(dir_entry: &walkdir::DirEntry) -> impl Iterator<Item = Lint> {
-    let content = fs::read_to_string(dir_entry.path()).unwrap();
-    let path = dir_entry.path();
-    let filename = path.file_stem().unwrap();
-    let path_buf = path.with_file_name(filename);
-    let mut rel_path = path_buf
-        .strip_prefix(clippy_project_root().join("clippy_lints/src"))
-        .expect("only files in `clippy_lints/src` should be looked at");
-    // If the lints are stored in mod.rs, we get the module name from
-    // the containing directory:
-    if filename == "mod" {
-        rel_path = rel_path.parent().unwrap();
-    }
+    for (rel_path, file) in WalkDir::new(&root_path)
+        .into_iter()
+        .map(Result::unwrap)
+        .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
+        .map(|f| (f.path().strip_prefix(&root_path).unwrap().to_path_buf(), f))
+    {
+        let path = file.path();
+        let contents =
+            fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
+        let module = rel_path
+            .components()
+            .map(|c| c.as_os_str().to_str().unwrap())
+            .collect::<Vec<_>>()
+            .join("::");
+
+        // If the lints are stored in mod.rs, we get the module name from
+        // the containing directory:
+        let module = if let Some(module) = module.strip_suffix("::mod.rs") {
+            module
+        } else {
+            module.strip_suffix(".rs").unwrap_or(&module)
+        };
 
-    let module = rel_path
-        .components()
-        .map(|c| c.as_os_str().to_str().unwrap())
-        .collect::<Vec<_>>()
-        .join("::");
+        if module == "deprecated_lints" {
+            parse_deprecated_contents(&contents, &mut deprecated_lints);
+        } else {
+            parse_contents(&contents, module, &mut lints);
+        }
+    }
+    (lints, deprecated_lints)
+}
 
-    parse_contents(&content, &module)
+macro_rules! match_tokens {
+    ($iter:ident, $($token:ident $({$($fields:tt)*})? $(($capture:ident))?)*) => {
+         {
+            $($(let $capture =)? if let Some((TokenKind::$token $({$($fields)*})?, _x)) = $iter.next() {
+                _x
+            } else {
+                continue;
+            };)*
+            #[allow(clippy::unused_unit)]
+            { ($($($capture,)?)*) }
+        }
+    }
 }
 
-fn parse_contents(content: &str, module: &str) -> impl Iterator<Item = Lint> {
-    let lints = DEC_CLIPPY_LINT_RE
-        .captures_iter(content)
-        .map(|m| Lint::new(&m["name"], &m["cat"], &m["desc"], None, module));
-    let deprecated = DEC_DEPRECATED_LINT_RE
-        .captures_iter(content)
-        .map(|m| Lint::new(&m["name"], "Deprecated", &m["desc"], Some(&m["desc"]), module));
-    // Removing the `.collect::<Vec<Lint>>().into_iter()` causes some lifetime issues due to the map
-    lints.chain(deprecated).collect::<Vec<Lint>>().into_iter()
+/// Parse a source file looking for `declare_clippy_lint` macro invocations.
+fn parse_contents(contents: &str, module: &str, lints: &mut Vec<Lint>) {
+    let mut offset = 0usize;
+    let mut iter = tokenize(contents).map(|t| {
+        let range = offset..offset + t.len;
+        offset = range.end;
+        (t.kind, &contents[range])
+    });
+
+    while iter.any(|(kind, s)| kind == TokenKind::Ident && s == "declare_clippy_lint") {
+        let mut iter = iter
+            .by_ref()
+            .filter(|&(kind, _)| !matches!(kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
+        // matches `!{`
+        match_tokens!(iter, Bang OpenBrace);
+        match iter.next() {
+            // #[clippy::version = "version"] pub
+            Some((TokenKind::Pound, _)) => {
+                match_tokens!(iter, OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket Ident);
+            },
+            // pub
+            Some((TokenKind::Ident, _)) => (),
+            _ => continue,
+        }
+        let (name, group, desc) = match_tokens!(
+            iter,
+            // LINT_NAME
+            Ident(name) Comma
+            // group,
+            Ident(group) Comma
+            // "description" }
+            Literal{kind: LiteralKind::Str{..}, ..}(desc) CloseBrace
+        );
+        lints.push(Lint::new(name, group, desc, module));
+    }
 }
 
-/// Collects all .rs files in the `clippy_lints/src` directory
-fn lint_files() -> impl Iterator<Item = walkdir::DirEntry> {
-    // We use `WalkDir` instead of `fs::read_dir` here in order to recurse into subdirectories.
-    // Otherwise we would not collect all the lints, for example in `clippy_lints/src/methods/`.
-    let path = clippy_project_root().join("clippy_lints/src");
-    WalkDir::new(path)
-        .into_iter()
-        .filter_map(Result::ok)
-        .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
+/// Parse a source file looking for `declare_deprecated_lint` macro invocations.
+fn parse_deprecated_contents(contents: &str, lints: &mut Vec<DeprecatedLint>) {
+    let mut offset = 0usize;
+    let mut iter = tokenize(contents).map(|t| {
+        let range = offset..offset + t.len;
+        offset = range.end;
+        (t.kind, &contents[range])
+    });
+    while iter.any(|(kind, s)| kind == TokenKind::Ident && s == "declare_deprecated_lint") {
+        let mut iter = iter
+            .by_ref()
+            .filter(|&(kind, _)| !matches!(kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
+        let (name, reason) = match_tokens!(
+            iter,
+            // !{
+            Bang OpenBrace
+            // #[clippy::version = "version"]
+            Pound OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket
+            // pub LINT_NAME,
+            Ident Ident(name) Comma
+            // "description"
+            Literal{kind: LiteralKind::Str{..},..}(reason)
+            // }
+            CloseBrace
+        );
+        lints.push(DeprecatedLint::new(name, reason));
+    }
 }
 
-/// Whether a file has had its text changed or not
-#[derive(PartialEq, Debug)]
-struct FileChange {
-    changed: bool,
-    new_lines: String,
+/// Removes the line splices and surrounding quotes from a string literal
+fn remove_line_splices(s: &str) -> String {
+    let s = s
+        .strip_prefix('"')
+        .and_then(|s| s.strip_suffix('"'))
+        .unwrap_or_else(|| panic!("expected quoted string, found `{}`", s));
+    let mut res = String::with_capacity(s.len());
+    unescape::unescape_literal(s, unescape::Mode::Str, &mut |range, _| res.push_str(&s[range]));
+    res
 }
 
 /// Replaces a region in a file delimited by two lines matching regexes.
@@ -396,144 +414,49 @@ struct FileChange {
 /// # Panics
 ///
 /// Panics if the path could not read or then written
-fn replace_region_in_file<F>(
+fn replace_region_in_file(
+    update_mode: UpdateMode,
     path: &Path,
     start: &str,
     end: &str,
-    replace_start: bool,
-    write_back: bool,
-    replacements: F,
-) -> FileChange
-where
-    F: FnOnce() -> Vec<String>,
-{
-    let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from {}: {}", path.display(), e));
-    let file_change = replace_region_in_text(&contents, start, end, replace_start, replacements);
-
-    if write_back {
-        if let Err(e) = fs::write(path, file_change.new_lines.as_bytes()) {
-            panic!("Cannot write to {}: {}", path.display(), e);
-        }
-    }
-    file_change
-}
-
-/// Replaces a region in a text delimited by two lines matching regexes.
-///
-/// * `text` is the input text on which you want to perform the replacement
-/// * `start` is a `&str` that describes the delimiter line before the region you want to replace.
-///   As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
-/// * `end` is a `&str` that describes the delimiter line until where the replacement should happen.
-///   As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
-/// * If `replace_start` is true, the `start` delimiter line is replaced as well. The `end`
-///   delimiter line is never replaced.
-/// * `replacements` is a closure that has to return a `Vec<String>` which contains the new text.
-///
-/// If you want to perform the replacement on files instead of already parsed text,
-/// use `replace_region_in_file`.
-///
-/// # Example
-///
-/// ```ignore
-/// let the_text = "replace_start\nsome text\nthat will be replaced\nreplace_end";
-/// let result =
-///     replace_region_in_text(the_text, "replace_start", "replace_end", false, || {
-///         vec!["a different".to_string(), "text".to_string()]
-///     })
-///     .new_lines;
-/// assert_eq!("replace_start\na different\ntext\nreplace_end", result);
-/// ```
-///
-/// # Panics
-///
-/// Panics if start or end is not valid regex
-fn replace_region_in_text<F>(text: &str, start: &str, end: &str, replace_start: bool, replacements: F) -> FileChange
-where
-    F: FnOnce() -> Vec<String>,
-{
-    let replace_it = replacements();
-    let mut in_old_region = false;
-    let mut found = false;
-    let mut new_lines = vec![];
-    let start = Regex::new(start).unwrap();
-    let end = Regex::new(end).unwrap();
-
-    for line in text.lines() {
-        if in_old_region {
-            if end.is_match(line) {
-                in_old_region = false;
-                new_lines.extend(replace_it.clone());
-                new_lines.push(line.to_string());
-            }
-        } else if start.is_match(line) {
-            if !replace_start {
-                new_lines.push(line.to_string());
+    write_replacement: impl FnMut(&mut String),
+) {
+    let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
+    let new_contents = match replace_region_in_text(&contents, start, end, write_replacement) {
+        Ok(x) => x,
+        Err(delim) => panic!("Couldn't find `{}` in file `{}`", delim, path.display()),
+    };
+
+    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 `{}`: {}", path.display(), e);
             }
-            in_old_region = true;
-            found = true;
-        } else {
-            new_lines.push(line.to_string());
-        }
-    }
-
-    if !found {
-        // This happens if the provided regex in `clippy_dev/src/main.rs` does not match in the
-        // given text or file. Most likely this is an error on the programmer's side and the Regex
-        // is incorrect.
-        eprintln!("error: regex \n{:?}\ndoesn't match. You may have to update it.", start);
-        std::process::exit(1);
-    }
-
-    let mut new_lines = new_lines.join("\n");
-    if text.ends_with('\n') {
-        new_lines.push('\n');
+        },
     }
-    let changed = new_lines != text;
-    FileChange { changed, new_lines }
-}
-
-#[test]
-fn test_parse_contents() {
-    let result: Vec<Lint> = parse_contents(
-        r#"
-declare_clippy_lint! {
-    #[clippy::version = "Hello Clippy!"]
-    pub PTR_ARG,
-    style,
-    "really long \
-     text"
 }
 
-declare_clippy_lint!{
-    #[clippy::version = "Test version"]
-    pub DOC_MARKDOWN,
-    pedantic,
-    "single line"
-}
-
-/// some doc comment
-declare_deprecated_lint! {
-    #[clippy::version = "I'm a version"]
-    pub SHOULD_ASSERT_EQ,
-    "`assert!()` will be more flexible with RFC 2011"
-}
-    "#,
-        "module_name",
-    )
-    .collect();
-
-    let expected = vec![
-        Lint::new("ptr_arg", "style", "really long text", None, "module_name"),
-        Lint::new("doc_markdown", "pedantic", "single line", None, "module_name"),
-        Lint::new(
-            "should_assert_eq",
-            "Deprecated",
-            "`assert!()` will be more flexible with RFC 2011",
-            Some("`assert!()` will be more flexible with RFC 2011"),
-            "module_name",
-        ),
-    ];
-    assert_eq!(expected, result);
+/// 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.
+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)
 }
 
 #[cfg(test)]
@@ -541,55 +464,65 @@ mod tests {
     use super::*;
 
     #[test]
-    fn test_replace_region() {
-        let text = "\nabc\n123\n789\ndef\nghi";
-        let expected = FileChange {
-            changed: true,
-            new_lines: "\nabc\nhello world\ndef\nghi".to_string(),
-        };
-        let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, false, || {
-            vec!["hello world".to_string()]
-        });
-        assert_eq!(expected, result);
-    }
+    fn test_parse_contents() {
+        static CONTENTS: &str = r#"
+            declare_clippy_lint! {
+                #[clippy::version = "Hello Clippy!"]
+                pub PTR_ARG,
+                style,
+                "really long \
+                text"
+            }
 
-    #[test]
-    fn test_replace_region_with_start() {
-        let text = "\nabc\n123\n789\ndef\nghi";
-        let expected = FileChange {
-            changed: true,
-            new_lines: "\nhello world\ndef\nghi".to_string(),
-        };
-        let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, true, || {
-            vec!["hello world".to_string()]
-        });
+            declare_clippy_lint!{
+                #[clippy::version = "Test version"]
+                pub DOC_MARKDOWN,
+                pedantic,
+                "single line"
+            }
+        "#;
+        let mut result = Vec::new();
+        parse_contents(CONTENTS, "module_name", &mut result);
+
+        let expected = vec![
+            Lint::new("ptr_arg", "style", "\"really long text\"", "module_name"),
+            Lint::new("doc_markdown", "pedantic", "\"single line\"", "module_name"),
+        ];
         assert_eq!(expected, result);
     }
 
     #[test]
-    fn test_replace_region_no_changes() {
-        let text = "123\n456\n789";
-        let expected = FileChange {
-            changed: false,
-            new_lines: "123\n456\n789".to_string(),
-        };
-        let result = replace_region_in_text(text, r#"^\s*123$"#, r#"^\s*456"#, false, Vec::new);
+    fn test_parse_deprecated_contents() {
+        static DEPRECATED_CONTENTS: &str = r#"
+            /// some doc comment
+            declare_deprecated_lint! {
+                #[clippy::version = "I'm a version"]
+                pub SHOULD_ASSERT_EQ,
+                "`assert!()` will be more flexible with RFC 2011"
+            }
+        "#;
+
+        let mut result = Vec::new();
+        parse_deprecated_contents(DEPRECATED_CONTENTS, &mut result);
+
+        let expected = vec![DeprecatedLint::new(
+            "should_assert_eq",
+            "\"`assert!()` will be more flexible with RFC 2011\"",
+        )];
         assert_eq!(expected, result);
     }
 
     #[test]
     fn test_usable_lints() {
         let lints = vec![
-            Lint::new("should_assert_eq", "Deprecated", "abc", Some("Reason"), "module_name"),
-            Lint::new("should_assert_eq2", "Not Deprecated", "abc", None, "module_name"),
-            Lint::new("should_assert_eq2", "internal", "abc", None, "module_name"),
-            Lint::new("should_assert_eq2", "internal_style", "abc", None, "module_name"),
+            Lint::new("should_assert_eq2", "Not Deprecated", "\"abc\"", "module_name"),
+            Lint::new("should_assert_eq2", "internal", "\"abc\"", "module_name"),
+            Lint::new("should_assert_eq2", "internal_style", "\"abc\"", "module_name"),
         ];
         let expected = vec![Lint::new(
             "should_assert_eq2",
             "Not Deprecated",
-            "abc",
-            None,
+            "\"abc\"",
             "module_name",
         )];
         assert_eq!(expected, Lint::usable_lints(&lints));
@@ -598,55 +531,30 @@ mod tests {
     #[test]
     fn test_by_lint_group() {
         let lints = vec![
-            Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
-            Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
-            Lint::new("incorrect_match", "group1", "abc", None, "module_name"),
+            Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
+            Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name"),
+            Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
         ];
         let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
         expected.insert(
             "group1".to_string(),
             vec![
-                Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
-                Lint::new("incorrect_match", "group1", "abc", None, "module_name"),
+                Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
+                Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
             ],
         );
         expected.insert(
             "group2".to_string(),
-            vec![Lint::new("should_assert_eq2", "group2", "abc", None, "module_name")],
+            vec![Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name")],
         );
         assert_eq!(expected, Lint::by_lint_group(lints.into_iter()));
     }
 
     #[test]
-    fn test_gen_changelog_lint_list() {
-        let lints = vec![
-            Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
-            Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
-        ];
-        let expected = vec![
-            format!("[`should_assert_eq`]: {}#should_assert_eq", DOCS_LINK),
-            format!("[`should_assert_eq2`]: {}#should_assert_eq2", DOCS_LINK),
-        ];
-        assert_eq!(expected, gen_changelog_lint_list(lints.iter()));
-    }
-
-    #[test]
     fn test_gen_deprecated() {
         let lints = vec![
-            Lint::new(
-                "should_assert_eq",
-                "group1",
-                "abc",
-                Some("has been superseded by should_assert_eq2"),
-                "module_name",
-            ),
-            Lint::new(
-                "another_deprecated",
-                "group2",
-                "abc",
-                Some("will be removed"),
-                "module_name",
-            ),
+            DeprecatedLint::new("should_assert_eq", "\"has been superseded by should_assert_eq2\""),
+            DeprecatedLint::new("another_deprecated", "\"will be removed\""),
         ];
 
         let expected = GENERATED_FILE_COMMENT.to_string()
@@ -665,32 +573,15 @@ mod tests {
             .join("\n")
             + "\n";
 
-        assert_eq!(expected, gen_deprecated(lints.iter()));
-    }
-
-    #[test]
-    #[should_panic]
-    fn test_gen_deprecated_fail() {
-        let lints = vec![Lint::new("should_assert_eq2", "group2", "abc", None, "module_name")];
-        let _deprecated_lints = gen_deprecated(lints.iter());
-    }
-
-    #[test]
-    fn test_gen_modules_list() {
-        let lints = vec![
-            Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
-            Lint::new("incorrect_stuff", "group3", "abc", None, "another_module"),
-        ];
-        let expected = vec!["mod another_module;".to_string(), "mod module_name;".to_string()];
-        assert_eq!(expected, gen_modules_list(lints.iter()));
+        assert_eq!(expected, gen_deprecated(&lints));
     }
 
     #[test]
     fn test_gen_lint_group_list() {
         let lints = vec![
-            Lint::new("abc", "group1", "abc", None, "module_name"),
-            Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
-            Lint::new("internal", "internal_style", "abc", None, "module_name"),
+            Lint::new("abc", "group1", "\"abc\"", "module_name"),
+            Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
+            Lint::new("internal", "internal_style", "\"abc\"", "module_name"),
         ];
         let expected = GENERATED_FILE_COMMENT.to_string()
             + &[
diff --git a/tests/versioncheck.rs b/tests/versioncheck.rs
index 77102b8cac0..38498ebdcf2 100644
--- a/tests/versioncheck.rs
+++ b/tests/versioncheck.rs
@@ -3,34 +3,32 @@
 #![allow(clippy::single_match_else)]
 
 use rustc_tools_util::VersionInfo;
+use std::fs;
 
 #[test]
 fn check_that_clippy_lints_and_clippy_utils_have_the_same_version_as_clippy() {
+    fn read_version(path: &str) -> String {
+        let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("error reading `{}`: {:?}", path, e));
+        contents
+            .lines()
+            .filter_map(|l| l.split_once('='))
+            .find_map(|(k, v)| (k.trim() == "version").then(|| v.trim()))
+            .unwrap_or_else(|| panic!("error finding version in `{}`", path))
+            .to_string()
+    }
+
     // do not run this test inside the upstream rustc repo:
     // https://github.com/rust-lang/rust-clippy/issues/6683
     if option_env!("RUSTC_TEST_SUITE").is_some() {
         return;
     }
 
-    let clippy_meta = cargo_metadata::MetadataCommand::new()
-        .no_deps()
-        .exec()
-        .expect("could not obtain cargo metadata");
+    let clippy_version = read_version("Cargo.toml");
+    let clippy_lints_version = read_version("clippy_lints/Cargo.toml");
+    let clippy_utils_version = read_version("clippy_utils/Cargo.toml");
 
-    for krate in &["clippy_lints", "clippy_utils"] {
-        let krate_meta = cargo_metadata::MetadataCommand::new()
-            .current_dir(std::env::current_dir().unwrap().join(krate))
-            .no_deps()
-            .exec()
-            .expect("could not obtain cargo metadata");
-        assert_eq!(krate_meta.packages[0].version, clippy_meta.packages[0].version);
-        for package in &clippy_meta.packages[0].dependencies {
-            if package.name == *krate {
-                assert!(package.req.matches(&krate_meta.packages[0].version));
-                break;
-            }
-        }
-    }
+    assert_eq!(clippy_version, clippy_lints_version);
+    assert_eq!(clippy_version, clippy_utils_version);
 }
 
 #[test]