| use crate::parse::cursor::Cursor; |
| use crate::parse::{Lint, LintData, LintPass}; |
| 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"; |
| |
| 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 |
| } |
| } |