use crate::utils::{ ErrAction, File, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, expect_action, update_text_region_fn, }; use itertools::Itertools; use std::collections::HashSet; use std::fmt::Write; use std::fs; use std::ops::Range; use std::path::{self, Path, PathBuf}; use walkdir::{DirEntry, WalkDir}; 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(update_mode: UpdateMode) { let lints = find_lint_decls(); let (deprecated, renamed) = read_deprecated_lints(); generate_lint_files(update_mode, &lints, &deprecated, &renamed); } #[expect(clippy::too_many_lines)] pub fn generate_lint_files( update_mode: UpdateMode, lints: &[Lint], deprecated: &[DeprecatedLint], renamed: &[RenamedLint], ) { let mut updater = FileUpdater::default(); 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, "{}", round_to_fifty(lints.len())).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, "{}", round_to_fifty(lints.len())).unwrap(); }), ); updater.update_file_checked( "cargo dev update_lints", update_mode, "CHANGELOG.md", &mut update_text_region_fn( "\n", "", |dst| { for lint in lints .iter() .map(|l| &*l.name) .chain(deprecated.iter().filter_map(|l| l.name.strip_prefix("clippy::"))) .chain(renamed.iter().filter_map(|l| l.old_name.strip_prefix("clippy::"))) .sorted() { writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap(); } }, ), ); updater.update_file_checked( "cargo dev update_lints", update_mode, "clippy_lints/src/deprecated_lints.rs", &mut |_, src, dst| { let mut searcher = RustSearcher::new(src); assert!( searcher.find_token(Token::Ident("declare_with_version")) && searcher.find_token(Token::Ident("declare_with_version")), "error reading deprecated lints" ); dst.push_str(&src[..searcher.pos() as usize]); dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n"); for lint in deprecated { write!( dst, " #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n", lint.version, lint.name, lint.reason, ) .unwrap(); } dst.push_str( "]}\n\n\ #[rustfmt::skip]\n\ declare_with_version! { RENAMED(RENAMED_VERSION) = [\n\ ", ); for lint in renamed { write!( dst, " #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n", lint.version, lint.old_name, lint.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({})] //~ ERROR: lint `{}`", lint.name, lint.name).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(); } } seen_lints.clear(); for lint in renamed { if seen_lints.insert(&lint.old_name) { writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap(); } } dst.push_str("\nfn main() {}\n"); UpdateStatus::from_changed(src != dst) }, ); for (crate_name, lints) in lints.iter().into_group_map_by(|&l| { let Some(path::Component::Normal(name)) = l.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 `{}`", l.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| { for lint_mod in lints .iter() .filter(|l| !l.module.is_empty()) .map(|l| l.module.split_once("::").map_or(&*l.module, |x| x.0)) .sorted() .dedup() { writeln!(dst, "mod {lint_mod};").unwrap(); } }, ), ); 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"); for (module_path, lint_name) in lints.iter().map(|l| (&l.module, l.name.to_uppercase())).sorted() { if module_path.is_empty() { writeln!(dst, " crate::{lint_name}_INFO,").unwrap(); } else { writeln!(dst, " crate::{module_path}::{lint_name}_INFO,").unwrap(); } } dst.push_str("];\n"); UpdateStatus::from_changed(src != dst) }, ); } } fn round_to_fifty(count: usize) -> usize { count / 50 * 50 } /// Lint data parsed from the Clippy source code. #[derive(PartialEq, Eq, Debug)] pub struct Lint { pub name: String, pub group: String, pub module: String, pub path: PathBuf, pub declaration_range: Range, } pub struct DeprecatedLint { pub name: String, pub reason: String, pub version: String, } pub struct RenamedLint { pub old_name: String, pub new_name: String, pub version: String, } /// Finds all lint declarations (`declare_clippy_lint!`) #[must_use] pub fn find_lint_decls() -> Vec { let mut lints = Vec::with_capacity(1000); let mut contents = String::new(); for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") { let e = expect_action(e, ErrAction::Read, "."); if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() { continue; } let Ok(mut name) = e.file_name().into_string() else { continue; }; if name.starts_with("clippy_lints") && name != "clippy_lints_internal" { name.push_str("/src"); for (file, module) in read_src_with_module(name.as_ref()) { parse_clippy_lint_decls( file.path(), File::open_read_to_cleared_string(file.path(), &mut contents), &module, &mut lints, ); } } } lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); lints } /// Reads the source files from the given root directory fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator { WalkDir::new(src_root).into_iter().filter_map(move |e| { let e = expect_action(e, ErrAction::Read, src_root); let path = e.path().as_os_str().as_encoded_bytes(); if let Some(path) = path.strip_suffix(b".rs") && let Some(path) = path.get(src_root.as_os_str().len() + 1..) { if path == b"lib" { Some((e, String::new())) } else { let path = if let Some(path) = path.strip_suffix(b"mod") && let Some(path) = path.strip_suffix(b"/").or_else(|| path.strip_suffix(b"\\")) { path } else { path }; if let Ok(path) = str::from_utf8(path) { let path = path.replace(['/', '\\'], "::"); Some((e, path)) } else { None } } } else { None } }) } /// Parse a source file looking for `declare_clippy_lint` macro invocations. fn parse_clippy_lint_decls(path: &Path, contents: &str, module: &str, lints: &mut Vec) { #[allow(clippy::enum_glob_use)] use Token::*; #[rustfmt::skip] static DECL_TOKENS: &[Token<'_>] = &[ // !{ /// docs Bang, OpenBrace, AnyComment, // #[clippy::version = "version"] Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket, // pub NAME, GROUP, Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma, ]; let mut searcher = RustSearcher::new(contents); while searcher.find_token(Ident("declare_clippy_lint")) { let start = searcher.pos() as usize - "declare_clippy_lint".len(); let (mut name, mut group) = ("", ""); if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) { lints.push(Lint { name: name.to_lowercase(), group: group.into(), module: module.into(), path: path.into(), declaration_range: start..searcher.pos() as usize, }); } } } #[must_use] pub fn read_deprecated_lints() -> (Vec, Vec) { #[allow(clippy::enum_glob_use)] use Token::*; #[rustfmt::skip] static DECL_TOKENS: &[Token<'_>] = &[ // #[clippy::version = "version"] Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, CaptureLitStr, CloseBracket, // ("first", "second"), OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma, ]; #[rustfmt::skip] static DEPRECATED_TOKENS: &[Token<'_>] = &[ // !{ DEPRECATED(DEPRECATED_VERSION) = [ Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket, ]; #[rustfmt::skip] static RENAMED_TOKENS: &[Token<'_>] = &[ // !{ RENAMED(RENAMED_VERSION) = [ Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket, ]; let path = "clippy_lints/src/deprecated_lints.rs"; let mut deprecated = Vec::with_capacity(30); let mut renamed = Vec::with_capacity(80); let mut contents = String::new(); File::open_read_to_cleared_string(path, &mut contents); let mut searcher = RustSearcher::new(&contents); // First instance is the macro definition. assert!( searcher.find_token(Ident("declare_with_version")), "error reading deprecated lints" ); if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) { let mut version = ""; let mut name = ""; let mut reason = ""; while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) { deprecated.push(DeprecatedLint { name: parse_str_single_line(path.as_ref(), name), reason: parse_str_single_line(path.as_ref(), reason), version: parse_str_single_line(path.as_ref(), version), }); } } else { panic!("error reading deprecated lints"); } if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) { let mut version = ""; let mut old_name = ""; let mut new_name = ""; while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) { renamed.push(RenamedLint { old_name: parse_str_single_line(path.as_ref(), old_name), new_name: parse_str_single_line(path.as_ref(), new_name), version: parse_str_single_line(path.as_ref(), version), }); } } else { panic!("error reading renamed lints"); } deprecated.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); renamed.sort_by(|lhs, rhs| lhs.old_name.cmp(&rhs.old_name)); (deprecated, renamed) } /// Removes the line splices and surrounding quotes from a string literal fn parse_str_lit(s: &str) -> String { let s = s.strip_prefix("r").unwrap_or(s).trim_matches('#'); let s = s .strip_prefix('"') .and_then(|s| s.strip_suffix('"')) .unwrap_or_else(|| panic!("expected quoted string, found `{s}`")); let mut res = String::with_capacity(s.len()); rustc_literal_escaper::unescape_str(s, &mut |_, ch| { if let Ok(ch) = ch { res.push(ch); } }); res } fn parse_str_single_line(path: &Path, s: &str) -> String { let value = parse_str_lit(s); assert!( !value.contains('\n'), "error parsing `{}`: `{s}` should be a single line string", path.display(), ); value } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_clippy_lint_decls() { static CONTENTS: &str = r#" declare_clippy_lint! { #[clippy::version = "Hello Clippy!"] pub PTR_ARG, style, "really long \ text" } declare_clippy_lint!{ #[clippy::version = "Test version"] pub DOC_MARKDOWN, pedantic, "single line" } "#; let mut result = Vec::new(); parse_clippy_lint_decls("".as_ref(), CONTENTS, "module_name", &mut result); for r in &mut result { r.declaration_range = Range::default(); } let expected = vec![ Lint { name: "ptr_arg".into(), group: "style".into(), module: "module_name".into(), path: PathBuf::new(), declaration_range: Range::default(), }, Lint { name: "doc_markdown".into(), group: "pedantic".into(), module: "module_name".into(), path: PathBuf::new(), declaration_range: Range::default(), }, ]; assert_eq!(expected, result); } }