blob: 24e4218716abf196423622dcc4ecc03a2b4c683b [file] [log] [blame]
use crate::parse::cursor::Cursor;
use crate::parse::{Lint, LintData, LintPass, VecBuf};
use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, update_text_region_fn};
use core::range::Range;
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";
impl LintData<'_> {
#[expect(clippy::too_many_lines)]
pub fn gen_decls(&self, update_mode: UpdateMode) {
let mut updater = FileUpdater::default();
let mut lints: Vec<_> = self.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)
},
);
}
}
}
impl LintPass<'_> {
pub fn gen_mac(&self, dst: &mut String) {
let mut line_start = dst.len();
dst.extend([self.mac.name(), "!("]);
let has_docs = write_comment_lines(self.docs, "\n ", dst);
let (list_indent, list_multi_end, end) = if has_docs {
dst.push_str("\n ");
line_start = dst.len() - 4;
(" ", "\n ", "]\n);")
} else {
(" ", "\n", "]);")
};
dst.push_str(self.name);
if let Some(lt) = self.lt {
dst.extend(["<", lt, ">"]);
}
dst.push_str(" => [");
let fmt = write_list(
self.lints.iter().copied(),
80usize.saturating_sub(dst.len() - line_start),
list_indent,
dst,
);
if matches!(fmt, ListFmt::MultiLine) {
dst.push_str(list_multi_end);
}
dst.push_str(end);
}
}
fn write_comment_lines(s: &str, prefix: &str, dst: &mut String) -> bool {
let mut has_doc = false;
for line in s.split('\n') {
let line = line.trim_start();
if !line.is_empty() {
has_doc = true;
dst.extend([prefix, line]);
}
}
has_doc
}
#[derive(Clone, Copy)]
enum ListFmt {
SingleLine,
MultiLine,
}
fn write_list<'a>(
items: impl Iterator<Item = &'a str> + Clone,
single_line_limit: usize,
indent: &str,
dst: &mut String,
) -> ListFmt {
let len = items.clone().map(str::len).sum::<usize>();
if len > single_line_limit {
for item in items {
dst.extend(["\n", indent, item, ","]);
}
ListFmt::MultiLine
} else {
let _ = write!(dst, "{}", items.format(", "));
ListFmt::SingleLine
}
}
/// Generates the contents of a lint's source file with all the lint and lint pass
/// declarations sorted.
pub fn gen_sorted_lints_file(
src: &str,
dst: &mut String,
lints: &mut [(&str, Range<u32>)],
passes: &mut [LintPass<'_>],
ranges: &mut VecBuf<Range<u32>>,
) {
ranges.with(|ranges| {
ranges.extend(lints.iter().map(|&(_, x)| x));
ranges.extend(passes.iter().map(|x| x.decl_range));
ranges.sort_unstable_by_key(|x| x.start);
lints.sort_unstable_by_key(|&(x, _)| x);
passes.sort_by_key(|x| x.name);
let mut ranges = ranges.iter();
let pos = if let Some(range) = ranges.next() {
dst.push_str(&src[..range.start as usize]);
for &(_, range) in &*lints {
dst.push_str(&src[range.start as usize..range.end as usize]);
dst.push_str("\n\n");
}
for pass in passes {
pass.gen_mac(dst);
dst.push_str("\n\n");
}
range.end
} else {
dst.push_str(src);
return;
};
let pos = ranges.fold(pos, |start, range| {
let s = &src[start as usize..range.start as usize];
dst.push_str(if s.trim_start().is_empty() {
// Only whitespace between this and the previous item. No need to keep that.
""
} else if src[..pos as usize].ends_with("\n\n")
&& let Some(s) = s.strip_prefix("\n\n")
{
// Empty line before and after. Remove one of them.
s
} else {
// Remove only full lines unless something is in the way.
s.strip_prefix('\n').unwrap_or(s)
});
range.end
});
// Since we always generate an empty line at the end, make sure to always skip it.
let s = &src[pos as usize..];
dst.push_str(s.strip_prefix('\n').map_or(s, |s| s.strip_prefix('\n').unwrap_or(s)));
});
}