mirror of
https://github.com/rust-lang/rust.git
synced 2026-04-30 06:43:20 +03:00
468 lines
18 KiB
Rust
468 lines
18 KiB
Rust
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)
|
|
}
|
|
}
|