| use crate::parse::cursor::Cursor; | |
| use crate::parse::{LintData, LintPass, ParsedLints}; | |
| use crate::utils::{FileUpdater, UpdateMode, UpdateStatus, VecBuf, slice_groups, update_text_region_fn}; | |
| use core::range::Range; | |
| use itertools::Itertools as _; | |
| use std::collections::HashSet; | |
| use std::fmt::Write as _; | |
| 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://clippy.rust-lang.org/main/index.html"; | |
| impl ParsedLints<'_> { | |
| #[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.data { | |
| LintData::Active(_) => active.push((name, lint.name_sp.file.path_as_krate_mod())), | |
| LintData::Deprecated(lint) => deprecated.push((name, lint)), | |
| LintData::Renamed(lint) => renamed.push((name, lint)), | |
| } | |
| } | |
| active.sort_by_key(|&(_, path)| path); | |
| // 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_loaded_file_checked( | |
| "cargo dev update_lints", | |
| update_mode, | |
| self.deprecated_file, | |
| &mut |_, src, dst| { | |
| let mut cursor = Cursor::new(src); | |
| assert!( | |
| cursor.find_ident("declare_with_version") && cursor.find_ident("declare_with_version"), | |
| "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 lints in slice_groups(&active, |(_, (head, _)), tail| { | |
| tail.iter().take_while(|(_, (x, _))| head == x).count() | |
| }) { | |
| let (_, (krate, _)) = lints[0]; | |
| updater.update_file_checked( | |
| "cargo dev update_lints", | |
| update_mode, | |
| Path::new(krate).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 &(_, (_, mod_path)) in lints { | |
| let module = mod_path.split_once(path::MAIN_SEPARATOR).map_or(mod_path, |(x, _)| x); | |
| if module != prev { | |
| writeln!(dst, "mod {module};").unwrap(); | |
| prev = module; | |
| } | |
| } | |
| }, | |
| ), | |
| ); | |
| updater.update_file_checked( | |
| "cargo dev update_lints", | |
| update_mode, | |
| Path::new(krate).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, (_, mod_path)) in lints { | |
| dst.push_str(" crate::"); | |
| for part in mod_path.split(path::MAIN_SEPARATOR) { | |
| dst.push_str(part); | |
| dst.push_str("::"); | |
| } | |
| buf.clear(); | |
| buf.push_str(name); | |
| buf.make_ascii_uppercase(); | |
| let _ = writeln!(dst, "{buf}_INFO,"); | |
| } | |
| 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_sp.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.lints.sort_unstable(); | |
| 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))); | |
| }); | |
| } |