2 use itertools::Itertools;
3 use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind};
4 use std::collections::HashMap;
10 use crate::clippy_project_root;
12 const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
13 // Use that command to update this file and do not edit by hand.\n\
14 // Manual edits will be overwritten.\n\n";
16 const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
18 #[derive(Clone, Copy, PartialEq)]
24 /// Runs the `update_lints` command.
26 /// This updates various generated values from the lint source code.
28 /// `update_mode` indicates if the files should be updated or if updates should be checked for.
32 /// Panics if a file path could not read from or then written to
33 #[allow(clippy::too_many_lines)]
34 pub fn run(update_mode: UpdateMode) {
35 let (lints, deprecated_lints) = gather_all();
37 let internal_lints = Lint::internal_lints(&lints);
38 let usable_lints = Lint::usable_lints(&lints);
39 let mut sorted_usable_lints = usable_lints.clone();
40 sorted_usable_lints.sort_by_key(|lint| lint.name.clone());
42 replace_region_in_file(
44 Path::new("README.md"),
46 " lints included in this crate!]",
48 write!(res, "{}", round_to_fifty(usable_lints.len())).unwrap();
52 replace_region_in_file(
54 Path::new("CHANGELOG.md"),
55 "<!-- begin autogenerated links to lint list -->\n",
56 "<!-- end autogenerated links to lint list -->",
58 for lint in usable_lints
61 .chain(deprecated_lints.iter().map(|l| &l.name))
64 writeln!(res, "[`{}`]: {}#{}", lint, DOCS_LINK, lint).unwrap();
69 // This has to be in lib.rs, otherwise rustfmt doesn't work
70 replace_region_in_file(
72 Path::new("clippy_lints/src/lib.rs"),
73 "// begin lints modules, do not remove this comment, it’s used in `update_lints`\n",
74 "// end lints modules, do not remove this comment, it’s used in `update_lints`",
76 for lint_mod in usable_lints.iter().map(|l| &l.module).unique().sorted() {
77 writeln!(res, "mod {};", lint_mod).unwrap();
83 "clippy_lints/src/lib.register_lints.rs",
85 &gen_register_lint_list(internal_lints.iter(), usable_lints.iter()),
88 "clippy_lints/src/lib.deprecated.rs",
90 &gen_deprecated(&deprecated_lints),
93 let all_group_lints = usable_lints.iter().filter(|l| {
96 "correctness" | "suspicious" | "style" | "complexity" | "perf"
99 let content = gen_lint_group_list("all", all_group_lints);
100 process_file("clippy_lints/src/lib.register_all.rs", update_mode, &content);
102 for (lint_group, lints) in Lint::by_lint_group(usable_lints.into_iter().chain(internal_lints)) {
103 let content = gen_lint_group_list(&lint_group, lints.iter());
105 &format!("clippy_lints/src/lib.register_{}.rs", lint_group),
112 pub fn print_lints() {
113 let (lint_list, _) = gather_all();
114 let usable_lints = Lint::usable_lints(&lint_list);
115 let usable_lint_count = usable_lints.len();
116 let grouped_by_lint_group = Lint::by_lint_group(usable_lints.into_iter());
118 for (lint_group, mut lints) in grouped_by_lint_group {
119 println!("\n## {}", lint_group);
121 lints.sort_by_key(|l| l.name.clone());
124 println!("* [{}]({}#{}) ({})", lint.name, DOCS_LINK, lint.name, lint.desc);
128 println!("there are {} lints", usable_lint_count);
131 fn round_to_fifty(count: usize) -> usize {
135 fn process_file(path: impl AsRef<Path>, update_mode: UpdateMode, content: &str) {
136 if update_mode == UpdateMode::Check {
138 fs::read_to_string(&path).unwrap_or_else(|e| panic!("Cannot read from {}: {}", path.as_ref().display(), e));
139 if content != old_content {
143 fs::write(&path, content.as_bytes())
144 .unwrap_or_else(|e| panic!("Cannot write to {}: {}", path.as_ref().display(), e));
148 fn exit_with_failure() {
150 "Not all lints defined properly. \
151 Please run `cargo dev update_lints` to make sure all lints are defined properly."
153 std::process::exit(1);
156 /// Lint data parsed from the Clippy source code.
157 #[derive(Clone, PartialEq, Debug)]
167 fn new(name: &str, group: &str, desc: &str, module: &str) -> Self {
169 name: name.to_lowercase(),
171 desc: remove_line_splices(desc),
172 module: module.into(),
176 /// Returns all non-deprecated lints and non-internal lints
178 fn usable_lints(lints: &[Self]) -> Vec<Self> {
181 .filter(|l| !l.group.starts_with("internal"))
186 /// Returns all internal lints (not `internal_warn` lints)
188 fn internal_lints(lints: &[Self]) -> Vec<Self> {
189 lints.iter().filter(|l| l.group == "internal").cloned().collect()
192 /// Returns the lints in a `HashMap`, grouped by the different lint groups
194 fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>> {
195 lints.map(|lint| (lint.group.to_string(), lint)).into_group_map()
199 #[derive(Clone, PartialEq, Debug)]
200 struct DeprecatedLint {
204 impl DeprecatedLint {
205 fn new(name: &str, reason: &str) -> Self {
207 name: name.to_lowercase(),
208 reason: remove_line_splices(reason),
213 /// Generates the code for registering a group
214 fn gen_lint_group_list<'a>(group_name: &str, lints: impl Iterator<Item = &'a Lint>) -> String {
215 let mut details: Vec<_> = lints.map(|l| (&l.module, l.name.to_uppercase())).collect();
216 details.sort_unstable();
218 let mut output = GENERATED_FILE_COMMENT.to_string();
220 output.push_str(&format!(
221 "store.register_group(true, \"clippy::{0}\", Some(\"clippy_{0}\"), vec![\n",
224 for (module, name) in details {
225 output.push_str(&format!(" LintId::of({}::{}),\n", module, name));
227 output.push_str("])\n");
232 /// Generates the `register_removed` code
234 fn gen_deprecated(lints: &[DeprecatedLint]) -> String {
235 let mut output = GENERATED_FILE_COMMENT.to_string();
236 output.push_str("{\n");
238 output.push_str(&format!(
240 " store.register_removed(\n",
241 " \"clippy::{}\",\n",
245 lint.name, lint.reason,
248 output.push_str("}\n");
253 /// Generates the code for registering lints
255 fn gen_register_lint_list<'a>(
256 internal_lints: impl Iterator<Item = &'a Lint>,
257 usable_lints: impl Iterator<Item = &'a Lint>,
259 let mut details: Vec<_> = internal_lints
260 .map(|l| (false, &l.module, l.name.to_uppercase()))
261 .chain(usable_lints.map(|l| (true, &l.module, l.name.to_uppercase())))
263 details.sort_unstable();
265 let mut output = GENERATED_FILE_COMMENT.to_string();
266 output.push_str("store.register_lints(&[\n");
268 for (is_public, module_name, lint_name) in details {
270 output.push_str(" #[cfg(feature = \"internal\")]\n");
272 output.push_str(&format!(" {}::{},\n", module_name, lint_name));
274 output.push_str("])\n");
279 /// Gathers all lints defined in `clippy_lints/src`
280 fn gather_all() -> (Vec<Lint>, Vec<DeprecatedLint>) {
281 let mut lints = Vec::with_capacity(1000);
282 let mut deprecated_lints = Vec::with_capacity(50);
283 let root_path = clippy_project_root().join("clippy_lints/src");
285 for (rel_path, file) in WalkDir::new(&root_path)
288 .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
289 .map(|f| (f.path().strip_prefix(&root_path).unwrap().to_path_buf(), f))
291 let path = file.path();
293 fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
294 let module = rel_path
296 .map(|c| c.as_os_str().to_str().unwrap())
300 // If the lints are stored in mod.rs, we get the module name from
301 // the containing directory:
302 let module = if let Some(module) = module.strip_suffix("::mod.rs") {
305 module.strip_suffix(".rs").unwrap_or(&module)
308 if module == "deprecated_lints" {
309 parse_deprecated_contents(&contents, &mut deprecated_lints);
311 parse_contents(&contents, module, &mut lints);
314 (lints, deprecated_lints)
317 macro_rules! match_tokens {
318 ($iter:ident, $($token:ident $({$($fields:tt)*})? $(($capture:ident))?)*) => {
320 $($(let $capture =)? if let Some((TokenKind::$token $({$($fields)*})?, _x)) = $iter.next() {
325 #[allow(clippy::unused_unit)]
326 { ($($($capture,)?)*) }
331 /// Parse a source file looking for `declare_clippy_lint` macro invocations.
332 fn parse_contents(contents: &str, module: &str, lints: &mut Vec<Lint>) {
333 let mut offset = 0usize;
334 let mut iter = tokenize(contents).map(|t| {
335 let range = offset..offset + t.len;
337 (t.kind, &contents[range])
340 while iter.any(|(kind, s)| kind == TokenKind::Ident && s == "declare_clippy_lint") {
343 .filter(|&(kind, _)| !matches!(kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
345 match_tokens!(iter, Bang OpenBrace);
347 // #[clippy::version = "version"] pub
348 Some((TokenKind::Pound, _)) => {
349 match_tokens!(iter, OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket Ident);
352 Some((TokenKind::Ident, _)) => (),
355 let (name, group, desc) = match_tokens!(
362 Literal{..}(desc) CloseBrace
364 lints.push(Lint::new(name, group, desc, module));
368 /// Parse a source file looking for `declare_deprecated_lint` macro invocations.
369 fn parse_deprecated_contents(contents: &str, lints: &mut Vec<DeprecatedLint>) {
370 let mut offset = 0usize;
371 let mut iter = tokenize(contents).map(|t| {
372 let range = offset..offset + t.len;
374 (t.kind, &contents[range])
376 while iter.any(|(kind, s)| kind == TokenKind::Ident && s == "declare_deprecated_lint") {
379 .filter(|&(kind, _)| !matches!(kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
380 let (name, reason) = match_tokens!(
384 // #[clippy::version = "version"]
385 Pound OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket
387 Ident Ident(name) Comma
389 Literal{kind: LiteralKind::Str{..},..}(reason)
393 lints.push(DeprecatedLint::new(name, reason));
397 /// Removes the line splices and surrounding quotes from a string literal
398 fn remove_line_splices(s: &str) -> String {
404 .and_then(|s| s.strip_suffix('"'))
405 .unwrap_or_else(|| panic!("expected quoted string, found `{}`", s));
406 let mut res = String::with_capacity(s.len());
407 unescape::unescape_literal(s, unescape::Mode::Str, &mut |range, _| res.push_str(&s[range]));
411 /// Replaces a region in a file delimited by two lines matching regexes.
413 /// `path` is the relative path to the file on which you want to perform the replacement.
415 /// See `replace_region_in_text` for documentation of the other options.
419 /// Panics if the path could not read or then written
420 fn replace_region_in_file(
421 update_mode: UpdateMode,
425 write_replacement: impl FnMut(&mut String),
427 let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
428 let new_contents = match replace_region_in_text(&contents, start, end, write_replacement) {
430 Err(delim) => panic!("Couldn't find `{}` in file `{}`", delim, path.display()),
434 UpdateMode::Check if contents != new_contents => exit_with_failure(),
435 UpdateMode::Check => (),
436 UpdateMode::Change => {
437 if let Err(e) = fs::write(path, new_contents.as_bytes()) {
438 panic!("Cannot write to `{}`: {}", path.display(), e);
444 /// Replaces a region in a text delimited by two strings. Returns the new text if both delimiters
445 /// were found, or the missing delimiter if not.
446 fn replace_region_in_text<'a>(
450 mut write_replacement: impl FnMut(&mut String),
451 ) -> Result<String, &'a str> {
452 let (text_start, rest) = text.split_once(start).ok_or(start)?;
453 let (_, text_end) = rest.split_once(end).ok_or(end)?;
455 let mut res = String::with_capacity(text.len() + 4096);
456 res.push_str(text_start);
458 write_replacement(&mut res);
460 res.push_str(text_end);
470 fn test_parse_contents() {
471 static CONTENTS: &str = r#"
472 declare_clippy_lint! {
473 #[clippy::version = "Hello Clippy!"]
480 declare_clippy_lint!{
481 #[clippy::version = "Test version"]
487 let mut result = Vec::new();
488 parse_contents(CONTENTS, "module_name", &mut result);
491 Lint::new("ptr_arg", "style", "\"really long text\"", "module_name"),
492 Lint::new("doc_markdown", "pedantic", "\"single line\"", "module_name"),
494 assert_eq!(expected, result);
498 fn test_parse_deprecated_contents() {
499 static DEPRECATED_CONTENTS: &str = r#"
501 declare_deprecated_lint! {
502 #[clippy::version = "I'm a version"]
503 pub SHOULD_ASSERT_EQ,
504 "`assert!()` will be more flexible with RFC 2011"
508 let mut result = Vec::new();
509 parse_deprecated_contents(DEPRECATED_CONTENTS, &mut result);
511 let expected = vec![DeprecatedLint::new(
513 "\"`assert!()` will be more flexible with RFC 2011\"",
515 assert_eq!(expected, result);
519 fn test_usable_lints() {
521 Lint::new("should_assert_eq2", "Not Deprecated", "\"abc\"", "module_name"),
522 Lint::new("should_assert_eq2", "internal", "\"abc\"", "module_name"),
523 Lint::new("should_assert_eq2", "internal_style", "\"abc\"", "module_name"),
525 let expected = vec![Lint::new(
531 assert_eq!(expected, Lint::usable_lints(&lints));
535 fn test_by_lint_group() {
537 Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
538 Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name"),
539 Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
541 let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
543 "group1".to_string(),
545 Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
546 Lint::new("incorrect_match", "group1", "\"abc\"", "module_name"),
550 "group2".to_string(),
551 vec![Lint::new("should_assert_eq2", "group2", "\"abc\"", "module_name")],
553 assert_eq!(expected, Lint::by_lint_group(lints.into_iter()));
557 fn test_gen_deprecated() {
559 DeprecatedLint::new("should_assert_eq", "\"has been superseded by should_assert_eq2\""),
560 DeprecatedLint::new("another_deprecated", "\"will be removed\""),
563 let expected = GENERATED_FILE_COMMENT.to_string()
566 " store.register_removed(",
567 " \"clippy::should_assert_eq\",",
568 " \"has been superseded by should_assert_eq2\",",
570 " store.register_removed(",
571 " \"clippy::another_deprecated\",",
572 " \"will be removed\",",
579 assert_eq!(expected, gen_deprecated(&lints));
583 fn test_gen_lint_group_list() {
585 Lint::new("abc", "group1", "\"abc\"", "module_name"),
586 Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name"),
587 Lint::new("internal", "internal_style", "\"abc\"", "module_name"),
589 let expected = GENERATED_FILE_COMMENT.to_string()
591 "store.register_group(true, \"clippy::group1\", Some(\"clippy_group1\"), vec![",
592 " LintId::of(module_name::ABC),",
593 " LintId::of(module_name::INTERNAL),",
594 " LintId::of(module_name::SHOULD_ASSERT_EQ),",
600 let result = gen_lint_group_list("group1", lints.iter());
602 assert_eq!(expected, result);