blob: a4cf15058986b07ed86eb3f5f6923ab803616528 [file] [log] [blame]
use crate::parse::cursor::Cursor;
use crate::parse::{Lint, LintData, ParseCx};
use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn};
use itertools::Itertools;
use std::collections::HashSet;
use std::fmt::Write;
use std::path::{self, Path};
const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
// Use that command to update this file and do not edit by hand.\n\
// Manual edits will be overwritten.\n\n";
const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
/// Runs the `update_lints` command.
///
/// This updates various generated values from the lint source code.
///
/// `update_mode` indicates if the files should be updated or if updates should be checked for.
///
/// # Panics
///
/// Panics if a file path could not read from or then written to
pub fn update(cx: ParseCx<'_>, update_mode: UpdateMode) {
let data = cx.parse_lint_decls();
generate_lint_files(update_mode, &data);
}
#[expect(clippy::too_many_lines)]
pub fn generate_lint_files(update_mode: UpdateMode, data: &LintData<'_>) {
let mut updater = FileUpdater::default();
let mut lints: Vec<_> = data.lints.iter().map(|(&x, y)| (x, y)).collect();
lints.sort_by_key(|&(x, _)| x);
updater.update_file_checked(
"cargo dev update_lints",
update_mode,
"CHANGELOG.md",
&mut update_text_region_fn(
"<!-- begin autogenerated links to lint list -->\n",
"<!-- end autogenerated links to lint list -->",
|dst| {
for &(lint, _) in &lints {
writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap();
}
},
),
);
let mut active = Vec::with_capacity(lints.len());
let mut deprecated = Vec::with_capacity(lints.len() / 8);
let mut renamed = Vec::with_capacity(lints.len() / 8);
for &(name, lint) in &lints {
match lint {
Lint::Active(lint) => active.push((name, lint)),
Lint::Deprecated(lint) => deprecated.push((name, lint)),
Lint::Renamed(lint) => renamed.push((name, lint)),
}
}
active.sort_by_key(|&(_, lint)| lint.module);
// Round to avoid updating the readme every time a lint is added/deprecated.
let lint_count = active.len() / 50 * 50;
updater.update_file_checked(
"cargo dev update_lints",
update_mode,
"README.md",
&mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
write!(dst, "{lint_count}").unwrap();
}),
);
updater.update_file_checked(
"cargo dev update_lints",
update_mode,
"book/src/README.md",
&mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
write!(dst, "{lint_count}").unwrap();
}),
);
updater.update_file_checked(
"cargo dev update_lints",
update_mode,
"clippy_lints/src/deprecated_lints.rs",
&mut |_, src, dst| {
let mut cursor = Cursor::new(src);
assert!(
cursor.find_ident("declare_with_version").is_some()
&& cursor.find_ident("declare_with_version").is_some(),
"error reading deprecated lints"
);
dst.push_str(&src[..cursor.pos() as usize]);
dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n");
for &(name, data) in &deprecated {
write!(
dst,
" #[clippy::version = \"{}\"]\n (\"clippy::{name}\", \"{}\"),\n",
data.version, data.reason,
)
.unwrap();
}
dst.push_str(
"]}\n\n\
#[rustfmt::skip]\n\
declare_with_version! { RENAMED(RENAMED_VERSION) = [\n\
",
);
for &(name, data) in &renamed {
write!(
dst,
" #[clippy::version = \"{}\"]\n (\"clippy::{name}\", \"{}\"),\n",
data.version, data.new_name,
)
.unwrap();
}
dst.push_str("]}\n");
UpdateStatus::from_changed(src != dst)
},
);
updater.update_file_checked(
"cargo dev update_lints",
update_mode,
"tests/ui/deprecated.rs",
&mut |_, src, dst| {
dst.push_str(GENERATED_FILE_COMMENT);
for &(lint, _) in &deprecated {
writeln!(dst, "#![warn(clippy::{lint})] //~ ERROR: lint `clippy::{lint}`").unwrap();
}
dst.push_str("\nfn main() {}\n");
UpdateStatus::from_changed(src != dst)
},
);
updater.update_file_checked(
"cargo dev update_lints",
update_mode,
"tests/ui/rename.rs",
&mut move |_, src, dst| {
let mut seen_lints = HashSet::new();
dst.push_str(GENERATED_FILE_COMMENT);
dst.push_str("#![allow(clippy::duplicated_attributes)]\n");
for &(_, lint) in &renamed {
if seen_lints.insert(lint.new_name) {
writeln!(dst, "#![allow({})]", lint.new_name).unwrap();
}
}
for &(lint, _) in &renamed {
writeln!(dst, "#![warn(clippy::{lint})] //~ ERROR: lint `clippy::{lint}`").unwrap();
}
dst.push_str("\nfn main() {}\n");
UpdateStatus::from_changed(src != dst)
},
);
for (crate_name, lints) in active.iter().copied().into_group_map_by(|&(_, lint)| {
let Some(path::Component::Normal(name)) = lint.path.components().next() else {
// All paths should start with `{crate_name}/src` when parsed from `find_lint_decls`
panic!(
"internal error: can't read crate name from path `{}`",
lint.path.display()
);
};
name
}) {
updater.update_file_checked(
"cargo dev update_lints",
update_mode,
Path::new(crate_name).join("src/lib.rs"),
&mut update_text_region_fn(
"// begin lints modules, do not remove this comment, it's used in `update_lints`\n",
"// end lints modules, do not remove this comment, it's used in `update_lints`",
|dst| {
let mut prev = "";
for &(_, lint) in &lints {
if lint.module != prev {
writeln!(dst, "mod {};", lint.module).unwrap();
prev = lint.module;
}
}
},
),
);
updater.update_file_checked(
"cargo dev update_lints",
update_mode,
Path::new(crate_name).join("src/declared_lints.rs"),
&mut |_, src, dst| {
dst.push_str(GENERATED_FILE_COMMENT);
dst.push_str("pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[\n");
let mut buf = String::new();
for &(name, lint) in &lints {
buf.clear();
buf.push_str(name);
buf.make_ascii_uppercase();
if lint.module.is_empty() {
writeln!(dst, " crate::{buf}_INFO,").unwrap();
} else {
writeln!(dst, " crate::{}::{buf}_INFO,", lint.module).unwrap();
}
}
dst.push_str("];\n");
UpdateStatus::from_changed(src != dst)
},
);
}
}