about summary refs log tree commit diff
path: root/clippy_dev/src
diff options
context:
space:
mode:
authorJason Newcomb <jsnewcomb@pm.me>2022-04-03 20:28:47 -0400
committerJason Newcomb <jsnewcomb@pm.me>2022-04-24 09:15:26 -0400
commitb3de32ba3cc2985c4e1d890732548a99fd55bba0 (patch)
tree70e1e94751406544f229b50f7588aee839ce5e9d /clippy_dev/src
parentd5ef542d376877380fda93ac7c457b5b8ba66833 (diff)
downloadrust-b3de32ba3cc2985c4e1d890732548a99fd55bba0.tar.gz
rust-b3de32ba3cc2985c4e1d890732548a99fd55bba0.zip
Add `rename_lint` command
Diffstat (limited to 'clippy_dev/src')
-rw-r--r--clippy_dev/src/lib.rs1
-rw-r--r--clippy_dev/src/main.rs33
-rw-r--r--clippy_dev/src/update_lints.rs308
3 files changed, 322 insertions, 20 deletions
diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs
index 414b403827d..c4bb0b97e25 100644
--- a/clippy_dev/src/lib.rs
+++ b/clippy_dev/src/lib.rs
@@ -1,3 +1,4 @@
+#![feature(let_chains)]
 #![feature(let_else)]
 #![feature(once_cell)]
 #![feature(rustc_private)]
diff --git a/clippy_dev/src/main.rs b/clippy_dev/src/main.rs
index 30a241c8ba1..b30f67e6959 100644
--- a/clippy_dev/src/main.rs
+++ b/clippy_dev/src/main.rs
@@ -18,9 +18,9 @@ fn main() {
             if matches.is_present("print-only") {
                 update_lints::print_lints();
             } else if matches.is_present("check") {
-                update_lints::run(update_lints::UpdateMode::Check);
+                update_lints::update(update_lints::UpdateMode::Check);
             } else {
-                update_lints::run(update_lints::UpdateMode::Change);
+                update_lints::update(update_lints::UpdateMode::Change);
             }
         },
         ("new_lint", Some(matches)) => {
@@ -30,7 +30,7 @@ fn main() {
                 matches.value_of("category"),
                 matches.is_present("msrv"),
             ) {
-                Ok(_) => update_lints::run(update_lints::UpdateMode::Change),
+                Ok(_) => update_lints::update(update_lints::UpdateMode::Change),
                 Err(e) => eprintln!("Unable to create lint: {}", e),
             }
         },
@@ -59,6 +59,12 @@ fn main() {
             let filename = matches.value_of("filename").unwrap();
             lint::run(filename);
         },
+        ("rename_lint", Some(matches)) => {
+            let old_name = matches.value_of("old_name").unwrap();
+            let new_name = matches.value_of("new_name").unwrap_or(old_name);
+            let uplift = matches.is_present("uplift");
+            update_lints::rename(old_name, new_name, uplift);
+        },
         _ => {},
     }
 }
@@ -232,5 +238,26 @@ fn get_clap_config<'a>() -> ArgMatches<'a> {
                         .help("The path to a file to lint"),
                 ),
         )
+        .subcommand(
+            SubCommand::with_name("rename_lint")
+                .about("Renames the given lint")
+                .arg(
+                    Arg::with_name("old_name")
+                        .index(1)
+                        .required(true)
+                        .help("The name of the lint to rename"),
+                )
+                .arg(
+                    Arg::with_name("new_name")
+                        .index(2)
+                        .required_unless("uplift")
+                        .help("The new name of the lint"),
+                )
+                .arg(
+                    Arg::with_name("uplift")
+                        .long("uplift")
+                        .help("This lint will be uplifted into rustc"),
+                ),
+        )
         .get_matches()
 }
diff --git a/clippy_dev/src/update_lints.rs b/clippy_dev/src/update_lints.rs
index f15b00ecad1..bbce3875e1d 100644
--- a/clippy_dev/src/update_lints.rs
+++ b/clippy_dev/src/update_lints.rs
@@ -1,11 +1,13 @@
-use core::fmt::Write;
+use aho_corasick::AhoCorasickBuilder;
+use core::fmt::Write as _;
 use itertools::Itertools;
 use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind};
 use std::collections::{HashMap, HashSet};
 use std::ffi::OsStr;
 use std::fs;
-use std::path::Path;
-use walkdir::WalkDir;
+use std::io::{self, Read as _, Seek as _, Write as _};
+use std::path::{Path, PathBuf};
+use walkdir::{DirEntry, WalkDir};
 
 use crate::clippy_project_root;
 
@@ -30,12 +32,19 @@ pub enum UpdateMode {
 /// # Panics
 ///
 /// Panics if a file path could not read from or then written to
-#[allow(clippy::too_many_lines)]
-pub fn run(update_mode: UpdateMode) {
+pub fn update(update_mode: UpdateMode) {
     let (lints, deprecated_lints, renamed_lints) = gather_all();
+    generate_lint_files(update_mode, &lints, &deprecated_lints, &renamed_lints);
+}
 
-    let internal_lints = Lint::internal_lints(&lints);
-    let usable_lints = Lint::usable_lints(&lints);
+fn generate_lint_files(
+    update_mode: UpdateMode,
+    lints: &[Lint],
+    deprecated_lints: &[DeprecatedLint],
+    renamed_lints: &[RenamedLint],
+) {
+    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());
 
@@ -87,7 +96,7 @@ pub fn run(update_mode: UpdateMode) {
     process_file(
         "clippy_lints/src/lib.deprecated.rs",
         update_mode,
-        &gen_deprecated(&deprecated_lints),
+        &gen_deprecated(deprecated_lints),
     );
 
     let all_group_lints = usable_lints.iter().filter(|l| {
@@ -108,10 +117,10 @@ pub fn run(update_mode: UpdateMode) {
         );
     }
 
-    let content = gen_deprecated_lints_test(&deprecated_lints);
+    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);
+    let content = gen_renamed_lints_test(renamed_lints);
     process_file("tests/ui/rename.rs", update_mode, &content);
 }
 
@@ -134,6 +143,209 @@ pub fn print_lints() {
     println!("there are {} lints", usable_lint_count);
 }
 
+/// Runs the `rename_lint` command.
+///
+/// This does the following:
+/// * Adds an entry to `renamed_lints.rs`.
+/// * Renames all lint attributes to the new name (e.g. `#[allow(clippy::lint_name)]`).
+/// * Renames the lint struct to the new name.
+/// * Renames the module containing the lint struct to the new name if it shares a name with the
+///   lint.
+///
+/// # Panics
+/// Panics for the following conditions:
+/// * If a file path could not read from or then written to
+/// * If either lint name has a prefix
+/// * If `old_name` doesn't name an existing lint.
+/// * If `old_name` names a deprecated or renamed lint.
+#[allow(clippy::too_many_lines)]
+pub fn rename(old_name: &str, new_name: &str, uplift: bool) {
+    if let Some((prefix, _)) = old_name.split_once("::") {
+        panic!("`{}` should not contain the `{}` prefix", old_name, prefix);
+    }
+    if let Some((prefix, _)) = new_name.split_once("::") {
+        panic!("`{}` should not contain the `{}` prefix", new_name, prefix);
+    }
+
+    let (mut lints, deprecated_lints, mut renamed_lints) = gather_all();
+    let mut old_lint_index = None;
+    let mut found_new_name = false;
+    for (i, lint) in lints.iter().enumerate() {
+        if lint.name == old_name {
+            old_lint_index = Some(i);
+        } else if lint.name == new_name {
+            found_new_name = true;
+        }
+    }
+    let old_lint_index = old_lint_index.unwrap_or_else(|| panic!("could not find lint `{}`", old_name));
+
+    let lint = RenamedLint {
+        old_name: format!("clippy::{}", old_name),
+        new_name: if uplift {
+            new_name.into()
+        } else {
+            format!("clippy::{}", new_name)
+        },
+    };
+
+    // Renamed lints and deprecated lints shouldn't have been found in the lint list, but check just in
+    // case.
+    assert!(
+        !renamed_lints.iter().any(|l| lint.old_name == l.old_name),
+        "`{}` has already been renamed",
+        old_name
+    );
+    assert!(
+        !deprecated_lints.iter().any(|l| lint.old_name == l.name),
+        "`{}` has already been deprecated",
+        old_name
+    );
+
+    // Update all lint level attributes. (`clippy::lint_name`)
+    for file in WalkDir::new(clippy_project_root())
+        .into_iter()
+        .map(Result::unwrap)
+        .filter(|f| {
+            let name = f.path().file_name();
+            let ext = f.path().extension();
+            (ext == Some(OsStr::new("rs")) || ext == Some(OsStr::new("fixed")))
+                && name != Some(OsStr::new("rename.rs"))
+                && name != Some(OsStr::new("renamed_lints.rs"))
+        })
+    {
+        rewrite_file(file.path(), |s| {
+            replace_ident_like(s, &[(&lint.old_name, &lint.new_name)])
+        });
+    }
+
+    renamed_lints.push(lint);
+    renamed_lints.sort_by(|lhs, rhs| {
+        lhs.new_name
+            .starts_with("clippy::")
+            .cmp(&rhs.new_name.starts_with("clippy::"))
+            .reverse()
+            .then_with(|| lhs.old_name.cmp(&rhs.old_name))
+    });
+
+    write_file(
+        Path::new("clippy_lints/src/renamed_lints.rs"),
+        &gen_renamed_lints_list(&renamed_lints),
+    );
+
+    if uplift {
+        write_file(Path::new("tests/ui/rename.rs"), &gen_renamed_lints_test(&renamed_lints));
+        println!(
+            "`{}` has be uplifted. All the code inside `clippy_lints` related to it needs to be removed manually.",
+            old_name
+        );
+    } else if found_new_name {
+        write_file(Path::new("tests/ui/rename.rs"), &gen_renamed_lints_test(&renamed_lints));
+        println!(
+            "`{}` is already defined. The old linting code inside `clippy_lints` needs to be updated/removed manually.",
+            new_name
+        );
+    } else {
+        // Rename the lint struct and source files sharing a name with the lint.
+        let lint = &mut lints[old_lint_index];
+        let old_name_upper = old_name.to_uppercase();
+        let new_name_upper = new_name.to_uppercase();
+        lint.name = new_name.into();
+
+        // Rename test files. only rename `.stderr` and `.fixed` files if the new test name doesn't exist.
+        if try_rename_file(
+            Path::new(&format!("tests/ui/{}.rs", old_name)),
+            Path::new(&format!("tests/ui/{}.rs", new_name)),
+        ) {
+            try_rename_file(
+                Path::new(&format!("tests/ui/{}.stderr", old_name)),
+                Path::new(&format!("tests/ui/{}.stderr", new_name)),
+            );
+            try_rename_file(
+                Path::new(&format!("tests/ui/{}.fixed", old_name)),
+                Path::new(&format!("tests/ui/{}.fixed", new_name)),
+            );
+        }
+
+        // Try to rename the file containing the lint if the file name matches the lint's name.
+        let replacements;
+        let replacements = if lint.module == old_name
+            && try_rename_file(
+                Path::new(&format!("clippy_lints/src/{}.rs", old_name)),
+                Path::new(&format!("clippy_lints/src/{}.rs", new_name)),
+            ) {
+            // Edit the module name in the lint list. Note there could be multiple lints.
+            for lint in lints.iter_mut().filter(|l| l.module == old_name) {
+                lint.module = new_name.into();
+            }
+            replacements = [(&*old_name_upper, &*new_name_upper), (old_name, new_name)];
+            replacements.as_slice()
+        } else if !lint.module.contains("::")
+            // Catch cases like `methods/lint_name.rs` where the lint is stored in `methods/mod.rs`
+            && try_rename_file(
+                Path::new(&format!("clippy_lints/src/{}/{}.rs", lint.module, old_name)),
+                Path::new(&format!("clippy_lints/src/{}/{}.rs", lint.module, new_name)),
+            )
+        {
+            // Edit the module name in the lint list. Note there could be multiple lints, or none.
+            let renamed_mod = format!("{}::{}", lint.module, old_name);
+            for lint in lints.iter_mut().filter(|l| l.module == renamed_mod) {
+                lint.module = format!("{}::{}", lint.module, new_name);
+            }
+            replacements = [(&*old_name_upper, &*new_name_upper), (old_name, new_name)];
+            replacements.as_slice()
+        } else {
+            replacements = [(&*old_name_upper, &*new_name_upper), ("", "")];
+            &replacements[0..1]
+        };
+
+        // Don't change `clippy_utils/src/renamed_lints.rs` here as it would try to edit the lint being
+        // renamed.
+        for (_, file) in clippy_lints_src_files().filter(|(rel_path, _)| rel_path != OsStr::new("renamed_lints.rs")) {
+            rewrite_file(file.path(), |s| replace_ident_like(s, replacements));
+        }
+
+        generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints);
+        println!("{} has been successfully renamed", old_name);
+    }
+
+    println!("note: `cargo uitest` still needs to be run to update the test results");
+}
+
+/// Replace substrings if they aren't bordered by identifier characters. Returns `None` if there
+/// were no replacements.
+fn replace_ident_like(contents: &str, replacements: &[(&str, &str)]) -> Option<String> {
+    fn is_ident_char(c: u8) -> bool {
+        matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')
+    }
+
+    let searcher = AhoCorasickBuilder::new()
+        .dfa(true)
+        .match_kind(aho_corasick::MatchKind::LeftmostLongest)
+        .build_with_size::<u16, _, _>(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();
+    }
+    result.push_str(&contents[pos..]);
+    edited.then(|| result)
+}
+
 fn round_to_fifty(count: usize) -> usize {
     count / 50 * 50
 }
@@ -323,19 +535,27 @@ fn gen_renamed_lints_test(lints: &[RenamedLint]) -> String {
     res
 }
 
+fn gen_renamed_lints_list(lints: &[RenamedLint]) -> String {
+    const HEADER: &str = "\
+        // This file is managed by `cargo dev rename_lint`. Prefer using that when possible.\n\n\
+        #[rustfmt::skip]\n\
+        pub static RENAMED_LINTS: &[(&str, &str)] = &[\n";
+
+    let mut res = String::from(HEADER);
+    for lint in lints {
+        writeln!(res, "    (\"{}\", \"{}\"),", lint.old_name, lint.new_name).unwrap();
+    }
+    res.push_str("];\n");
+    res
+}
+
 /// Gathers all lints defined in `clippy_lints/src`
 fn gather_all() -> (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>) {
     let mut lints = Vec::with_capacity(1000);
     let mut deprecated_lints = Vec::with_capacity(50);
     let mut renamed_lints = Vec::with_capacity(50);
-    let root_path = clippy_project_root().join("clippy_lints/src");
 
-    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))
-    {
+    for (rel_path, file) in clippy_lints_src_files() {
         let path = file.path();
         let contents =
             fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
@@ -362,6 +582,14 @@ fn gather_all() -> (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>) {
     (lints, deprecated_lints, renamed_lints)
 }
 
+fn clippy_lints_src_files() -> impl Iterator<Item = (PathBuf, DirEntry)> {
+    let root_path = clippy_project_root().join("clippy_lints/src");
+    let iter = WalkDir::new(&root_path).into_iter();
+    iter.map(Result::unwrap)
+        .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
+        .map(move |f| (f.path().strip_prefix(&root_path).unwrap().to_path_buf(), f))
+}
+
 macro_rules! match_tokens {
     ($iter:ident, $($token:ident $({$($fields:tt)*})? $(($capture:ident))?)*) => {
          {
@@ -526,6 +754,52 @@ fn replace_region_in_text<'a>(
     Ok(res)
 }
 
+fn try_rename_file(old_name: &Path, new_name: &Path) -> bool {
+    match fs::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_file(e, new_name, "create"),
+    };
+    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_file(e, old_name, "rename");
+            }
+        },
+    }
+}
+
+#[allow(clippy::needless_pass_by_value)]
+fn panic_file(error: io::Error, name: &Path, action: &str) -> ! {
+    panic!("failed to {} file `{}`: {}", action, name.display(), error)
+}
+
+fn rewrite_file(path: &Path, f: impl FnOnce(&str) -> Option<String>) {
+    let mut file = fs::OpenOptions::new()
+        .write(true)
+        .read(true)
+        .open(path)
+        .unwrap_or_else(|e| panic_file(e, path, "open"));
+    let mut buf = String::new();
+    file.read_to_string(&mut buf)
+        .unwrap_or_else(|e| panic_file(e, path, "read"));
+    if let Some(new_contents) = f(&buf) {
+        file.rewind().unwrap_or_else(|e| panic_file(e, path, "write"));
+        file.write_all(new_contents.as_bytes())
+            .unwrap_or_else(|e| panic_file(e, path, "write"));
+        file.set_len(new_contents.len() as u64)
+            .unwrap_or_else(|e| panic_file(e, path, "write"));
+    }
+}
+
+fn write_file(path: &Path, contents: &str) {
+    fs::write(path, contents).unwrap_or_else(|e| panic_file(e, path, "write"));
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;