]> git.lizzy.rs Git - rust.git/blobdiff - src/tools/clippy/clippy_dev/src/update_lints.rs
Merge commit '984330a6ee3c4d15626685d6dc8b7b759ff630bd' into clippyup
[rust.git] / src / tools / clippy / clippy_dev / src / update_lints.rs
index d368ef1f46a2a98866e6aedcf32581c9ce17c047..59db51fbfac5149fec5158a137c91e94fa86bf4b 100644 (file)
@@ -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;
 
      // 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 @@ fn new(name: &str, group: &str, desc: &str, deprecation: Option<&str>, module: &
     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 @@ fn internal_lints(lints: &[Self]) -> Vec<Self> {
         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 @@ fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>
     }
 }
 
+#[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,136 @@ 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{..}(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('r')
+        .unwrap_or(s)
+        .trim_matches('#')
+        .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 +417,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 +467,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 +534,30 @@ fn test_usable_lints() {
     #[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 +576,15 @@ fn test_gen_deprecated() {
             .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()
             + &[