blob: d62597428e210534b11ef8ecaae1298c5e26d6ab [file] [log] [blame]
use crate::update_lints::{RenamedLint, find_lint_decls, generate_lint_files, read_deprecated_lints};
use crate::utils::{
ErrAction, FileUpdater, RustSearcher, Token, 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 rustc_lexer::TokenKind;
use std::ffi::OsString;
use std::fs;
use std::path::Path;
/// 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.
#[expect(clippy::too_many_lines)]
pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: bool) {
if let Some((prefix, _)) = old_name.split_once("::") {
panic!("`{old_name}` should not contain the `{prefix}` prefix");
}
if let Some((prefix, _)) = new_name.split_once("::") {
panic!("`{new_name}` should not contain the `{prefix}` prefix");
}
let mut updater = FileUpdater::default();
let mut lints = find_lint_decls();
let (deprecated_lints, mut renamed_lints) = read_deprecated_lints();
let Ok(lint_idx) = lints.binary_search_by(|x| x.name.as_str().cmp(old_name)) else {
panic!("could not find lint `{old_name}`");
};
let lint = &lints[lint_idx];
let old_name_prefixed = String::from_iter(["clippy::", old_name]);
let new_name_prefixed = if uplift {
new_name.to_owned()
} else {
String::from_iter(["clippy::", new_name])
};
for lint in &mut renamed_lints {
if lint.new_name == old_name_prefixed {
lint.new_name.clone_from(&new_name_prefixed);
}
}
match renamed_lints.binary_search_by(|x| x.old_name.cmp(&old_name_prefixed)) {
Ok(_) => {
println!("`{old_name}` already has a rename registered");
return;
},
Err(idx) => {
renamed_lints.insert(
idx,
RenamedLint {
old_name: old_name_prefixed,
new_name: if uplift {
new_name.to_owned()
} else {
String::from_iter(["clippy::", new_name])
},
version: clippy_version.rust_display().to_string(),
},
);
},
}
// Some tests are named `lint_name_suffix` which should also be renamed,
// but we can't do that if the renamed lint's name overlaps with another
// lint. e.g. renaming 'foo' to 'bar' when a lint 'foo_bar' also exists.
let change_prefixed_tests = lints.get(lint_idx + 1).is_none_or(|l| !l.name.starts_with(old_name));
let mut mod_edit = ModEdit::None;
if uplift {
let is_unique_mod = lints[..lint_idx].iter().any(|l| l.module == lint.module)
|| lints[lint_idx + 1..].iter().any(|l| l.module == lint.module);
if is_unique_mod {
if delete_file_if_exists(lint.path.as_ref()) {
mod_edit = ModEdit::Delete;
}
} else {
updater.update_file(&lint.path, &mut |_, src, dst| -> UpdateStatus {
let mut start = &src[..lint.declaration_range.start];
if start.ends_with("\n\n") {
start = &start[..start.len() - 1];
}
let mut end = &src[lint.declaration_range.end..];
if end.starts_with("\n\n") {
end = &end[1..];
}
dst.push_str(start);
dst.push_str(end);
UpdateStatus::Changed
});
}
delete_test_files(old_name, change_prefixed_tests);
lints.remove(lint_idx);
} else if lints.binary_search_by(|x| x.name.as_str().cmp(new_name)).is_err() {
let lint = &mut lints[lint_idx];
if lint.module.ends_with(old_name)
&& lint
.path
.file_stem()
.is_some_and(|x| x.as_encoded_bytes() == old_name.as_bytes())
{
let mut new_path = lint.path.with_file_name(new_name).into_os_string();
new_path.push(".rs");
if try_rename_file(lint.path.as_ref(), new_path.as_ref()) {
mod_edit = ModEdit::Rename;
}
let mod_len = lint.module.len();
lint.module.truncate(mod_len - old_name.len());
lint.module.push_str(new_name);
}
rename_test_files(old_name, new_name, change_prefixed_tests);
new_name.clone_into(&mut lints[lint_idx].name);
lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
} else {
println!("Renamed `clippy::{old_name}` to `clippy::{new_name}`");
println!("Since `{new_name}` already exists the existing code has not been changed");
return;
}
let mut update_fn = file_update_fn(old_name, new_name, mod_edit);
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);
}
}
generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints);
if uplift {
println!("Uplifted `clippy::{old_name}` as `{new_name}`");
if matches!(mod_edit, ModEdit::None) {
println!("Only the rename has been registered, the code will need to be edited manually");
} else {
println!("All the lint's code has been deleted");
println!("Make sure to inspect the results as some things may have been missed");
}
} else {
println!("Renamed `clippy::{old_name}` to `clippy::{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");
}
#[derive(Clone, Copy)]
enum ModEdit {
None,
Delete,
Rename,
}
fn collect_ui_test_names(lint: &str, rename_prefixed: bool, 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 let Some((name_only, _)) = name.as_encoded_bytes().split_once(|&x| x == b'.') {
if name_only.starts_with(lint.as_bytes()) && (rename_prefixed || name_only.len() == lint.len()) {
dst.push((name, true));
}
} else if name.as_encoded_bytes().starts_with(lint.as_bytes()) && (rename_prefixed || name.len() == lint.len())
{
dst.push((name, false));
}
}
}
fn collect_ui_toml_test_names(lint: &str, rename_prefixed: bool, dst: &mut Vec<(OsString, bool)>) {
if rename_prefixed {
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()) && e.file_type().is_ok_and(|ty| ty.is_dir()) {
dst.push((name, false));
}
}
} else {
dst.push((lint.into(), false));
}
}
/// Renames all test files for the given lint.
///
/// If `rename_prefixed` is `true` this will also rename tests which have the lint name as a prefix.
fn rename_test_files(old_name: &str, new_name: &str, rename_prefixed: bool) {
let mut tests = Vec::new();
let mut old_buf = OsString::from("tests/ui/");
let mut new_buf = OsString::from("tests/ui/");
collect_ui_test_names(old_name, rename_prefixed, &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, rename_prefixed, &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());
}
}
fn delete_test_files(lint: &str, rename_prefixed: bool) {
let mut tests = Vec::new();
let mut buf = OsString::from("tests/ui/");
collect_ui_test_names(lint, rename_prefixed, &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, rename_prefixed, &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()
}
#[expect(clippy::too_many_lines)]
fn file_update_fn<'a, 'b>(
old_name: &'a str,
new_name: &'b str,
mod_edit: ModEdit,
) -> impl use<'a, 'b> + 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 searcher = RustSearcher::new(src);
let mut capture = "";
loop {
match searcher.peek() {
TokenKind::Eof => break,
TokenKind::Ident => {
let match_start = searcher.pos();
let text = searcher.peek_text();
searcher.step();
match text {
// clippy::line_name or clippy::lint-name
"clippy" => {
if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture])
&& capture == old_name
{
dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
dst.push_str(new_name);
copy_pos = searcher.pos();
changed = true;
}
},
// mod lint_name
"mod" => {
if !matches!(mod_edit, ModEdit::None)
&& searcher.match_tokens(&[Token::CaptureIdent], &mut [&mut capture])
&& capture == old_name
{
match mod_edit {
ModEdit::Rename => {
dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
dst.push_str(new_name);
copy_pos = searcher.pos();
changed = true;
},
ModEdit::Delete if searcher.match_tokens(&[Token::Semi], &mut []) => {
let mut start = &src[copy_pos as usize..match_start as usize];
if start.ends_with("\n\n") {
start = &start[..start.len() - 1];
}
dst.push_str(start);
copy_pos = searcher.pos();
if src[copy_pos as usize..].starts_with("\n\n") {
copy_pos += 1;
}
changed = true;
},
ModEdit::Delete | ModEdit::None => {},
}
}
},
// lint_name::
name if matches!(mod_edit, ModEdit::Rename) && name == old_name => {
let name_end = searcher.pos();
if searcher.match_tokens(&[Token::DoubleColon], &mut []) {
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 = searcher.pos();
changed = true;
},
}
},
// //~ lint_name
TokenKind::LineComment { doc_style: None } => {
let text = searcher.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..searcher.pos() as usize + text.len()]);
dst.push_str(new_name);
copy_pos = searcher.pos() + searcher.peek_len();
changed = true;
}
searcher.step();
},
// ::lint_name
TokenKind::Colon
if searcher.match_tokens(&[Token::DoubleColon, Token::CaptureIdent], &mut [&mut capture])
&& capture == old_name =>
{
dst.push_str(&src[copy_pos as usize..searcher.pos() as usize - capture.len()]);
dst.push_str(new_name);
copy_pos = searcher.pos();
changed = true;
},
_ => searcher.step(),
}
}
dst.push_str(&src[copy_pos as usize..]);
UpdateStatus::from_changed(changed)
}
}