| use crate::parse::cursor::{self, Capture, Cursor}; | |
| use crate::parse::{ActiveLint, DeprecatedLint, Lint, LintData, LintName, ParseCx, RenamedLint}; | |
| use crate::utils::{ | |
| ErrAction, FileUpdater, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists, | |
| expect_action, try_rename_dir, try_rename_file, walk_dir_no_dot_or_target, | |
| }; | |
| use core::mem; | |
| use rustc_lexer::TokenKind; | |
| use std::collections::hash_map::Entry; | |
| use std::ffi::OsString; | |
| use std::fs; | |
| use std::path::Path; | |
| /// Runs the `deprecate` command | |
| /// | |
| /// This does the following: | |
| /// * Adds an entry to `deprecated_lints.rs`. | |
| /// * Removes the lint declaration (and the entire file if applicable) | |
| /// | |
| /// # Panics | |
| /// | |
| /// If a file path could not read from or written to | |
| pub fn deprecate<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, name: &'env str, reason: &'env str) { | |
| let mut data = cx.parse_lint_decls(); | |
| let Entry::Occupied(mut lint) = data.lints.entry(name) else { | |
| eprintln!("error: failed to find lint `{name}`"); | |
| return; | |
| }; | |
| let Lint::Active(prev_lint) = mem::replace( | |
| lint.get_mut(), | |
| Lint::Deprecated(DeprecatedLint { | |
| reason, | |
| version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), | |
| }), | |
| ) else { | |
| eprintln!("error: `{name}` is already deprecated"); | |
| return; | |
| }; | |
| remove_lint_declaration(name, &prev_lint, &data, &mut FileUpdater::default()); | |
| data.gen_decls(UpdateMode::Change); | |
| println!("info: `{name}` has successfully been deprecated"); | |
| println!("note: you must run `cargo uitest` to update the test results"); | |
| } | |
| pub fn uplift<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_name: &'env str, new_name: &'env str) { | |
| let mut data = cx.parse_lint_decls(); | |
| update_rename_targets(&mut data, old_name, LintName::new_rustc(new_name)); | |
| let Entry::Occupied(mut lint) = data.lints.entry(old_name) else { | |
| eprintln!("error: failed to find lint `{old_name}`"); | |
| return; | |
| }; | |
| let Lint::Active(prev_lint) = mem::replace( | |
| lint.get_mut(), | |
| Lint::Renamed(RenamedLint { | |
| new_name: LintName::new_rustc(new_name), | |
| version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), | |
| }), | |
| ) else { | |
| eprintln!("error: `{old_name}` is already deprecated"); | |
| return; | |
| }; | |
| let mut updater = FileUpdater::default(); | |
| let remove_mod = remove_lint_declaration(old_name, &prev_lint, &data, &mut updater); | |
| let mut update_fn = uplift_update_fn(old_name, new_name, remove_mod); | |
| for e in walk_dir_no_dot_or_target(".") { | |
| let e = expect_action(e, ErrAction::Read, "."); | |
| if e.path().as_os_str().as_encoded_bytes().ends_with(b".rs") { | |
| updater.update_file(e.path(), &mut update_fn); | |
| } | |
| } | |
| data.gen_decls(UpdateMode::Change); | |
| println!("info: `{old_name}` has successfully been uplifted as `{new_name}`"); | |
| println!("note: you must run `cargo uitest` to update the test results"); | |
| } | |
| /// 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. | |
| pub fn rename<'cx, 'env: 'cx>(cx: ParseCx<'cx>, clippy_version: Version, old_name: &'env str, new_name: &'env str) { | |
| let mut updater = FileUpdater::default(); | |
| let mut data = cx.parse_lint_decls(); | |
| update_rename_targets(&mut data, old_name, LintName::new_clippy(new_name)); | |
| let Entry::Occupied(mut lint) = data.lints.entry(old_name) else { | |
| eprintln!("error: failed to find lint `{old_name}`"); | |
| return; | |
| }; | |
| let Lint::Active(mut prev_lint) = mem::replace( | |
| lint.get_mut(), | |
| Lint::Renamed(RenamedLint { | |
| new_name: LintName::new_clippy(new_name), | |
| version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), | |
| }), | |
| ) else { | |
| eprintln!("error: `{old_name}` is already deprecated"); | |
| return; | |
| }; | |
| let mut rename_mod = false; | |
| if let Entry::Vacant(e) = data.lints.entry(new_name) { | |
| if prev_lint.module.ends_with(old_name) | |
| && prev_lint | |
| .path | |
| .file_stem() | |
| .is_some_and(|x| x.as_encoded_bytes() == old_name.as_bytes()) | |
| { | |
| let mut new_path = prev_lint.path.with_file_name(new_name).into_os_string(); | |
| new_path.push(".rs"); | |
| if try_rename_file(prev_lint.path.as_ref(), new_path.as_ref()) { | |
| rename_mod = true; | |
| } | |
| prev_lint.module = cx.str_buf.with(|buf| { | |
| buf.push_str(&prev_lint.module[..prev_lint.module.len() - old_name.len()]); | |
| buf.push_str(new_name); | |
| cx.arena.alloc_str(buf) | |
| }); | |
| } | |
| e.insert(Lint::Active(prev_lint)); | |
| rename_test_files(old_name, new_name, &create_ignored_prefixes(old_name, &data)); | |
| } else { | |
| println!("Renamed `{old_name}` to `{new_name}`"); | |
| println!("Since `{new_name}` already exists the existing code has not been changed"); | |
| return; | |
| } | |
| let mut update_fn = rename_update_fn(old_name, new_name, rename_mod); | |
| for e in walk_dir_no_dot_or_target(".") { | |
| let e = expect_action(e, ErrAction::Read, "."); | |
| if e.path().as_os_str().as_encoded_bytes().ends_with(b".rs") { | |
| updater.update_file(e.path(), &mut update_fn); | |
| } | |
| } | |
| data.gen_decls(UpdateMode::Change); | |
| println!("Renamed `{old_name}` to `{new_name}`"); | |
| println!("All code referencing the old name has been updated"); | |
| println!("Make sure to inspect the results as some things may have been missed"); | |
| println!("note: `cargo uibless` still needs to be run to update the test results"); | |
| } | |
| /// Removes a lint's declaration and test files. Returns whether the module containing the | |
| /// lint was deleted. | |
| fn remove_lint_declaration(name: &str, lint: &ActiveLint<'_>, data: &LintData<'_>, updater: &mut FileUpdater) -> bool { | |
| let delete_mod = if data.lints.iter().all(|(_, l)| { | |
| if let Lint::Active(l) = l { | |
| l.module != lint.module | |
| } else { | |
| true | |
| } | |
| }) { | |
| delete_file_if_exists(lint.path.as_ref()) | |
| } else { | |
| updater.update_file(&lint.path, &mut |_, src, dst| -> UpdateStatus { | |
| let mut start = &src[..lint.declaration_range.start as usize]; | |
| if start.ends_with("\n\n") { | |
| start = &start[..start.len() - 1]; | |
| } | |
| let mut end = &src[lint.declaration_range.end as usize..]; | |
| if end.starts_with("\n\n") { | |
| end = &end[1..]; | |
| } | |
| dst.push_str(start); | |
| dst.push_str(end); | |
| UpdateStatus::Changed | |
| }); | |
| false | |
| }; | |
| delete_test_files(name, &create_ignored_prefixes(name, data)); | |
| delete_mod | |
| } | |
| /// Updates all renames to the old name to be renames to the new name. | |
| /// | |
| /// This is needed because rustc doesn't allow a lint to be renamed to a lint that has | |
| /// also been renamed. | |
| fn update_rename_targets<'cx>(data: &mut LintData<'cx>, old_name: &str, new_name: LintName<'cx>) { | |
| let old_name = LintName::new_clippy(old_name); | |
| for lint in data.lints.values_mut() { | |
| if let Lint::Renamed(lint) = lint | |
| && lint.new_name == old_name | |
| { | |
| lint.new_name = new_name; | |
| } | |
| } | |
| } | |
| /// Creates a list of prefixes to ignore when | |
| fn create_ignored_prefixes<'cx>(name: &str, data: &LintData<'cx>) -> Vec<&'cx str> { | |
| data.lints | |
| .keys() | |
| .copied() | |
| .filter(|&x| x.len() > name.len() && x.starts_with(name)) | |
| .collect() | |
| } | |
| fn collect_ui_test_names(lint: &str, ignored_prefixes: &[&str], dst: &mut Vec<(OsString, bool)>) { | |
| for e in fs::read_dir("tests/ui").expect("error reading `tests/ui`") { | |
| let e = e.expect("error reading `tests/ui`"); | |
| let name = e.file_name(); | |
| if name.as_encoded_bytes().starts_with(lint.as_bytes()) | |
| && !ignored_prefixes | |
| .iter() | |
| .any(|&pre| name.as_encoded_bytes().starts_with(pre.as_bytes())) | |
| && let Ok(ty) = e.file_type() | |
| && (ty.is_file() || ty.is_dir()) | |
| { | |
| dst.push((name, ty.is_file())); | |
| } | |
| } | |
| } | |
| fn collect_ui_toml_test_names(lint: &str, ignored_prefixes: &[&str], dst: &mut Vec<(OsString, bool)>) { | |
| for e in fs::read_dir("tests/ui-toml").expect("error reading `tests/ui-toml`") { | |
| let e = e.expect("error reading `tests/ui-toml`"); | |
| let name = e.file_name(); | |
| if name.as_encoded_bytes().starts_with(lint.as_bytes()) | |
| && !ignored_prefixes | |
| .iter() | |
| .any(|&pre| name.as_encoded_bytes().starts_with(pre.as_bytes())) | |
| && e.file_type().is_ok_and(|ty| ty.is_dir()) | |
| { | |
| dst.push((name, false)); | |
| } | |
| } | |
| } | |
| /// Renames all test files for the given lint where the file name does not start with any | |
| /// of the given prefixes. | |
| fn rename_test_files(old_name: &str, new_name: &str, ignored_prefixes: &[&str]) { | |
| let mut tests: Vec<(OsString, bool)> = Vec::new(); | |
| let mut old_buf = OsString::from("tests/ui/"); | |
| let mut new_buf = OsString::from("tests/ui/"); | |
| collect_ui_test_names(old_name, ignored_prefixes, &mut tests); | |
| for &(ref name, is_file) in &tests { | |
| old_buf.push(name); | |
| new_buf.extend([new_name.as_ref(), name.slice_encoded_bytes(old_name.len()..)]); | |
| if is_file { | |
| try_rename_file(old_buf.as_ref(), new_buf.as_ref()); | |
| } else { | |
| try_rename_dir(old_buf.as_ref(), new_buf.as_ref()); | |
| } | |
| old_buf.truncate("tests/ui/".len()); | |
| new_buf.truncate("tests/ui/".len()); | |
| } | |
| tests.clear(); | |
| old_buf.truncate("tests/ui".len()); | |
| new_buf.truncate("tests/ui".len()); | |
| old_buf.push("-toml/"); | |
| new_buf.push("-toml/"); | |
| collect_ui_toml_test_names(old_name, ignored_prefixes, &mut tests); | |
| for (name, _) in &tests { | |
| old_buf.push(name); | |
| new_buf.extend([new_name.as_ref(), name.slice_encoded_bytes(old_name.len()..)]); | |
| try_rename_dir(old_buf.as_ref(), new_buf.as_ref()); | |
| old_buf.truncate("tests/ui/".len()); | |
| new_buf.truncate("tests/ui/".len()); | |
| } | |
| } | |
| /// Deletes all test files for the given lint where the file name does not start with any | |
| /// of the given prefixes. | |
| fn delete_test_files(lint: &str, ignored_prefixes: &[&str]) { | |
| let mut tests = Vec::new(); | |
| let mut buf = OsString::from("tests/ui/"); | |
| collect_ui_test_names(lint, ignored_prefixes, &mut tests); | |
| for &(ref name, is_file) in &tests { | |
| buf.push(name); | |
| if is_file { | |
| delete_file_if_exists(buf.as_ref()); | |
| } else { | |
| delete_dir_if_exists(buf.as_ref()); | |
| } | |
| buf.truncate("tests/ui/".len()); | |
| } | |
| buf.truncate("tests/ui".len()); | |
| buf.push("-toml/"); | |
| tests.clear(); | |
| collect_ui_toml_test_names(lint, ignored_prefixes, &mut tests); | |
| for (name, _) in &tests { | |
| buf.push(name); | |
| delete_dir_if_exists(buf.as_ref()); | |
| buf.truncate("tests/ui/".len()); | |
| } | |
| } | |
| fn snake_to_pascal(s: &str) -> String { | |
| let mut dst = Vec::with_capacity(s.len()); | |
| let mut iter = s.bytes(); | |
| || -> Option<()> { | |
| dst.push(iter.next()?.to_ascii_uppercase()); | |
| while let Some(c) = iter.next() { | |
| if c == b'_' { | |
| dst.push(iter.next()?.to_ascii_uppercase()); | |
| } else { | |
| dst.push(c); | |
| } | |
| } | |
| Some(()) | |
| }(); | |
| String::from_utf8(dst).unwrap() | |
| } | |
| /// Creates an update function which replaces all instances of `clippy::old_name` with | |
| /// `new_name`. | |
| fn uplift_update_fn<'a>( | |
| old_name: &'a str, | |
| new_name: &'a str, | |
| remove_mod: bool, | |
| ) -> impl use<'a> + FnMut(&Path, &str, &mut String) -> UpdateStatus { | |
| move |_, src, dst| { | |
| let mut copy_pos = 0u32; | |
| let mut changed = false; | |
| let mut cursor = Cursor::new(src); | |
| while let Some(ident) = cursor.find_any_ident() { | |
| match cursor.get_text(ident) { | |
| "mod" | |
| if remove_mod && cursor.match_all(&[cursor::Pat::Ident(old_name), cursor::Pat::Semi], &mut []) => | |
| { | |
| dst.push_str(&src[copy_pos as usize..ident.pos as usize]); | |
| dst.push_str(new_name); | |
| copy_pos = cursor.pos(); | |
| if src[copy_pos as usize..].starts_with('\n') { | |
| copy_pos += 1; | |
| } | |
| changed = true; | |
| }, | |
| "clippy" if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::Ident(old_name)], &mut []) => { | |
| dst.push_str(&src[copy_pos as usize..ident.pos as usize]); | |
| dst.push_str(new_name); | |
| copy_pos = cursor.pos(); | |
| changed = true; | |
| }, | |
| _ => {}, | |
| } | |
| } | |
| dst.push_str(&src[copy_pos as usize..]); | |
| UpdateStatus::from_changed(changed) | |
| } | |
| } | |
| fn rename_update_fn<'a>( | |
| old_name: &'a str, | |
| new_name: &'a str, | |
| rename_mod: bool, | |
| ) -> impl use<'a> + FnMut(&Path, &str, &mut String) -> UpdateStatus { | |
| let old_name_pascal = snake_to_pascal(old_name); | |
| let new_name_pascal = snake_to_pascal(new_name); | |
| let old_name_upper = old_name.to_ascii_uppercase(); | |
| let new_name_upper = new_name.to_ascii_uppercase(); | |
| move |_, src, dst| { | |
| let mut copy_pos = 0u32; | |
| let mut changed = false; | |
| let mut cursor = Cursor::new(src); | |
| let mut captures = [Capture::EMPTY]; | |
| loop { | |
| match cursor.peek() { | |
| TokenKind::Eof => break, | |
| TokenKind::Ident => { | |
| let match_start = cursor.pos(); | |
| let text = cursor.peek_text(); | |
| cursor.step(); | |
| match text { | |
| // clippy::line_name or clippy::lint-name | |
| "clippy" => { | |
| if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures) | |
| && cursor.get_text(captures[0]) == old_name | |
| { | |
| dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]); | |
| dst.push_str(new_name); | |
| copy_pos = cursor.pos(); | |
| changed = true; | |
| } | |
| }, | |
| // mod lint_name | |
| "mod" => { | |
| if rename_mod && let Some(pos) = cursor.match_ident(old_name) { | |
| dst.push_str(&src[copy_pos as usize..pos as usize]); | |
| dst.push_str(new_name); | |
| copy_pos = cursor.pos(); | |
| changed = true; | |
| } | |
| }, | |
| // lint_name:: | |
| name if rename_mod && name == old_name => { | |
| let name_end = cursor.pos(); | |
| if cursor.match_pat(cursor::Pat::DoubleColon) { | |
| dst.push_str(&src[copy_pos as usize..match_start as usize]); | |
| dst.push_str(new_name); | |
| copy_pos = name_end; | |
| changed = true; | |
| } | |
| }, | |
| // LINT_NAME or LintName | |
| name => { | |
| let replacement = if name == old_name_upper { | |
| &new_name_upper | |
| } else if name == old_name_pascal { | |
| &new_name_pascal | |
| } else { | |
| continue; | |
| }; | |
| dst.push_str(&src[copy_pos as usize..match_start as usize]); | |
| dst.push_str(replacement); | |
| copy_pos = cursor.pos(); | |
| changed = true; | |
| }, | |
| } | |
| }, | |
| // //~ lint_name | |
| TokenKind::LineComment { doc_style: None } => { | |
| let text = cursor.peek_text(); | |
| if text.starts_with("//~") | |
| && let Some(text) = text.strip_suffix(old_name) | |
| && !text.ends_with(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_')) | |
| { | |
| dst.push_str(&src[copy_pos as usize..cursor.pos() as usize + text.len()]); | |
| dst.push_str(new_name); | |
| copy_pos = cursor.pos() + cursor.peek_len(); | |
| changed = true; | |
| } | |
| cursor.step(); | |
| }, | |
| // ::lint_name | |
| TokenKind::Colon | |
| if cursor.match_all(&[cursor::Pat::DoubleColon, cursor::Pat::CaptureIdent], &mut captures) | |
| && cursor.get_text(captures[0]) == old_name => | |
| { | |
| dst.push_str(&src[copy_pos as usize..captures[0].pos as usize]); | |
| dst.push_str(new_name); | |
| copy_pos = cursor.pos(); | |
| changed = true; | |
| }, | |
| _ => cursor.step(), | |
| } | |
| } | |
| dst.push_str(&src[copy_pos as usize..]); | |
| UpdateStatus::from_changed(changed) | |
| } | |
| } |