| 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))); | |
| }); | |
| } |