| use crate::parse::cursor::{self, Capture, Cursor}; | |
| use crate::parse::{ActiveLint, DeprecatedLint, Lint, LintData, LintName, ParseCx, ParsedLints, 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 crate::{SourceFile, Span}; | |
| use core::mem; | |
| use rustc_lexer::TokenKind; | |
| use std::collections::hash_map::Entry; | |
| use std::ffi::OsString; | |
| use std::path::Path; | |
| use std::{fs, 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 { | |
| cx.dcx.emit_unknown_lint(name); | |
| cx.dcx.exit_assume_err(); | |
| }; | |
| let prev_lint = mem::replace( | |
| lint.get_mut(), | |
| Lint { | |
| name_sp: Span::new(data.deprecated_file, 0..0), | |
| data: LintData::Deprecated(DeprecatedLint { | |
| reason, | |
| version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), | |
| }), | |
| }, | |
| ); | |
| let LintData::Active(prev_lint_data) = prev_lint.data else { | |
| cx.dcx.emit_already_deprecated(name); | |
| cx.dcx.exit_assume_err(); | |
| }; | |
| cx.dcx.exit_on_err(); | |
| remove_lint_declaration( | |
| name, | |
| prev_lint.name_sp.file, | |
| &prev_lint_data, | |
| &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(); | |
| let Entry::Occupied(mut lint) = data.lints.entry(old_name) else { | |
| cx.dcx.emit_unknown_lint(old_name); | |
| cx.dcx.exit_assume_err(); | |
| }; | |
| let prev_lint = mem::replace( | |
| lint.get_mut(), | |
| Lint { | |
| name_sp: Span::new(data.deprecated_file, 0..0), | |
| data: LintData::Renamed(RenamedLint { | |
| new_name: LintName::new_rustc(new_name), | |
| version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), | |
| }), | |
| }, | |
| ); | |
| let LintData::Active(prev_lint_data) = prev_lint.data else { | |
| cx.dcx.emit_already_deprecated(old_name); | |
| cx.dcx.exit_assume_err(); | |
| }; | |
| cx.dcx.exit_on_err(); | |
| update_rename_targets(&mut data, old_name, LintName::new_rustc(new_name)); | |
| let mut updater = FileUpdater::default(); | |
| let remove_mod = remove_lint_declaration(old_name, prev_lint.name_sp.file, &prev_lint_data, &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 data = cx.parse_lint_decls(); | |
| let Entry::Occupied(mut lint) = data.lints.entry(old_name) else { | |
| cx.dcx.emit_unknown_lint(old_name); | |
| cx.dcx.exit_assume_err(); | |
| }; | |
| let prev_lint = mem::replace( | |
| lint.get_mut(), | |
| Lint { | |
| name_sp: Span::new(data.deprecated_file, 0..0), | |
| data: LintData::Renamed(RenamedLint { | |
| new_name: LintName::new_clippy(new_name), | |
| version: cx.str_buf.alloc_display(cx.arena, clippy_version.rust_display()), | |
| }), | |
| }, | |
| ); | |
| if !matches!(prev_lint.data, LintData::Active(_)) { | |
| cx.dcx.emit_already_deprecated(old_name); | |
| } | |
| cx.dcx.exit_on_err(); | |
| update_rename_targets(&mut data, old_name, LintName::new_clippy(new_name)); | |
| let mut updater = FileUpdater::default(); | |
| let prev_file = prev_lint.name_sp.file; | |
| let mut rename_mod = false; | |
| if let Entry::Vacant(e) = data.lints.entry(new_name) { | |
| if let Some((path, file)) = prev_file.path.get().rsplit_once(path::MAIN_SEPARATOR) | |
| && let Some(file) = file.strip_suffix(".rs") | |
| && file == old_name | |
| { | |
| let new_path = cx | |
| .str_buf | |
| .alloc_display(cx.arena, format_args!("{path}{}{new_name}.rs", path::MAIN_SEPARATOR)); | |
| if try_rename_file(prev_file.path.get(), new_path) { | |
| rename_mod = true; | |
| prev_file.path.set(new_path); | |
| } | |
| } | |
| e.insert(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_file: &SourceFile<'_>, | |
| lint_data: &ActiveLint<'_>, | |
| data: &ParsedLints<'_>, | |
| updater: &mut FileUpdater, | |
| ) -> bool { | |
| let delete_mod = if data.lints.iter().all(|(_, l)| l.name_sp.file != lint_file) { | |
| delete_file_if_exists(lint_file.path.get()) | |
| } else { | |
| updater.update_file(lint_file.path.get(), &mut |_, src, dst| -> UpdateStatus { | |
| let mut start = &src[..lint_data.decl_range.start as usize]; | |
| if start.ends_with("\n\n") { | |
| start = &start[..start.len() - 1]; | |
| } | |
| let mut end = &src[lint_data.decl_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 ParsedLints<'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 LintData::Renamed(lint) = &mut lint.data | |
| && 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: &ParsedLints<'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, &new_buf); | |
| } else { | |
| try_rename_dir(&old_buf, &new_buf); | |
| } | |
| 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, &new_buf); | |
| 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); | |
| } else { | |
| delete_dir_if_exists(&buf); | |
| } | |
| 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); | |
| 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_capture_ident() { | |
| match cursor.get_text(ident) { | |
| "mod" if remove_mod && cursor.eat_ident(old_name) && cursor.eat_semi() => { | |
| 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.eat_double_colon() && cursor.eat_ident(old_name) => { | |
| 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) | |
| .is_ok() | |
| && 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(mod_name) = cursor.capture_ident() | |
| && cursor.get_text(mod_name) == old_name | |
| { | |
| dst.push_str(&src[copy_pos as usize..mod_name.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.eat_double_colon() { | |
| 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) | |
| .is_ok() | |
| && 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) | |
| } | |
| } |