| 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( |
| "<!-- begin autogenerated links to lint list -->\n", |
| "<!-- end autogenerated links to lint list -->", |
| |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<usize>, |
| } |
| |
| 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<Lint> { |
| 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<Item = (DirEntry, String)> { |
| 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<Lint>) { |
| #[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<DeprecatedLint>, Vec<RenamedLint>) { |
| #[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); |
| } |
| } |