]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/lib.rs
Fix lint register code format
[rust.git] / clippy_dev / src / lib.rs
1 #![feature(once_cell)]
2 #![cfg_attr(feature = "deny-warnings", deny(warnings))]
3 // warn on lints, that are included in `rust-lang/rust`s bootstrap
4 #![warn(rust_2018_idioms, unused_lifetimes)]
5
6 use itertools::Itertools;
7 use regex::Regex;
8 use std::collections::{BTreeSet, HashMap};
9 use std::ffi::OsStr;
10 use std::fs;
11 use std::lazy::SyncLazy;
12 use std::path::{Path, PathBuf};
13 use walkdir::WalkDir;
14
15 pub mod bless;
16 pub mod fmt;
17 pub mod new_lint;
18 pub mod serve;
19 pub mod setup;
20 pub mod update_lints;
21
22 const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
23      // Use that command to update this file and do not edit by hand.\n\
24      // Manual edits will be overwritten.\n\n";
25
26 static DEC_CLIPPY_LINT_RE: SyncLazy<Regex> = SyncLazy::new(|| {
27     Regex::new(
28         r#"(?x)
29     declare_clippy_lint!\s*[\{(]
30     (?:\s+///.*)*
31     \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s*
32     (?P<cat>[a-z_]+)\s*,\s*
33     "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})]
34 "#,
35     )
36     .unwrap()
37 });
38
39 static DEC_DEPRECATED_LINT_RE: SyncLazy<Regex> = SyncLazy::new(|| {
40     Regex::new(
41         r#"(?x)
42     declare_deprecated_lint!\s*[{(]\s*
43     (?:\s+///.*)*
44     \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s*
45     "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})]
46 "#,
47     )
48     .unwrap()
49 });
50 static NL_ESCAPE_RE: SyncLazy<Regex> = SyncLazy::new(|| Regex::new(r#"\\\n\s*"#).unwrap());
51
52 pub static DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
53
54 /// Lint data parsed from the Clippy source code.
55 #[derive(Clone, PartialEq, Debug)]
56 pub struct Lint {
57     pub name: String,
58     pub group: String,
59     pub desc: String,
60     pub deprecation: Option<String>,
61     pub module: String,
62 }
63
64 impl Lint {
65     #[must_use]
66     pub fn new(name: &str, group: &str, desc: &str, deprecation: Option<&str>, module: &str) -> Self {
67         Self {
68             name: name.to_lowercase(),
69             group: group.to_string(),
70             desc: NL_ESCAPE_RE.replace(&desc.replace("\\\"", "\""), "").to_string(),
71             deprecation: deprecation.map(ToString::to_string),
72             module: module.to_string(),
73         }
74     }
75
76     /// Returns all non-deprecated lints and non-internal lints
77     #[must_use]
78     pub fn usable_lints(lints: &[Self]) -> Vec<Self> {
79         lints
80             .iter()
81             .filter(|l| l.deprecation.is_none() && !l.group.starts_with("internal"))
82             .cloned()
83             .collect()
84     }
85
86     /// Returns all internal lints (not `internal_warn` lints)
87     #[must_use]
88     pub fn internal_lints(lints: &[Self]) -> Vec<Self> {
89         lints.iter().filter(|l| l.group == "internal").cloned().collect()
90     }
91
92     /// Returns all deprecated lints
93     #[must_use]
94     pub fn deprecated_lints(lints: &[Self]) -> Vec<Self> {
95         lints.iter().filter(|l| l.deprecation.is_some()).cloned().collect()
96     }
97
98     /// Returns the lints in a `HashMap`, grouped by the different lint groups
99     #[must_use]
100     pub fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>> {
101         lints.map(|lint| (lint.group.to_string(), lint)).into_group_map()
102     }
103 }
104
105 /// Generates the code for registering a group
106 pub fn gen_lint_group_list<'a>(group_name: &str, lints: impl Iterator<Item = &'a Lint>) -> String {
107     let mut details: Vec<_> = lints.map(|l| (&l.module, l.name.to_uppercase())).collect();
108     details.sort_unstable();
109
110     let mut output = GENERATED_FILE_COMMENT.to_string();
111
112     output.push_str(&format!(
113         "store.register_group(true, \"clippy::{0}\", Some(\"clippy_{0}\"), vec![\n",
114         group_name
115     ));
116     for (module, name) in details {
117         output.push_str(&format!("    LintId::of({}::{}),\n", module, name));
118     }
119     output.push_str("])\n");
120
121     output
122 }
123
124 /// Generates the module declarations for `lints`
125 #[must_use]
126 pub fn gen_modules_list<'a>(lints: impl Iterator<Item = &'a Lint>) -> String {
127     let module_names: BTreeSet<_> = lints.map(|l| &l.module).collect();
128
129     let mut output = GENERATED_FILE_COMMENT.to_string();
130     for name in module_names {
131         output.push_str(&format!("mod {};\n", name));
132     }
133     output
134 }
135
136 /// Generates the list of lint links at the bottom of the README
137 #[must_use]
138 pub fn gen_changelog_lint_list<'a>(lints: impl Iterator<Item = &'a Lint>) -> Vec<String> {
139     lints
140         .sorted_by_key(|l| &l.name)
141         .map(|l| format!("[`{}`]: {}#{}", l.name, DOCS_LINK, l.name))
142         .collect()
143 }
144
145 /// Generates the `register_removed` code
146 #[must_use]
147 pub fn gen_deprecated<'a>(lints: impl Iterator<Item = &'a Lint>) -> String {
148     let mut output = GENERATED_FILE_COMMENT.to_string();
149     output.push_str("{\n");
150     for Lint { name, deprecation, .. } in lints {
151         output.push_str(&format!(
152             concat!(
153                 "    store.register_removed(\n",
154                 "        \"clippy::{}\",\n",
155                 "        \"{}\",\n",
156                 "    );\n"
157             ),
158             name,
159             deprecation.as_ref().expect("`lints` are deprecated")
160         ));
161     }
162     output.push_str("}\n");
163
164     output
165 }
166
167 /// Generates the code for registering lints
168 #[must_use]
169 pub fn gen_register_lint_list<'a>(
170     internal_lints: impl Iterator<Item = &'a Lint>,
171     usable_lints: impl Iterator<Item = &'a Lint>,
172 ) -> String {
173     let mut details: Vec<_> = internal_lints
174         .map(|l| (false, &l.module, l.name.to_uppercase()))
175         .chain(usable_lints.map(|l| (true, &l.module, l.name.to_uppercase())))
176         .collect();
177     details.sort_unstable();
178
179     let mut output = GENERATED_FILE_COMMENT.to_string();
180     output.push_str("store.register_lints(&[\n");
181
182     for (is_public, module_name, lint_name) in details {
183         if !is_public {
184             output.push_str("    #[cfg(feature = \"internal-lints\")]\n");
185         }
186         output.push_str(&format!("    {}::{},\n", module_name, lint_name));
187     }
188     output.push_str("])\n");
189
190     output
191 }
192
193 /// Gathers all files in `src/clippy_lints` and gathers all lints inside
194 pub fn gather_all() -> impl Iterator<Item = Lint> {
195     lint_files().flat_map(|f| gather_from_file(&f))
196 }
197
198 fn gather_from_file(dir_entry: &walkdir::DirEntry) -> impl Iterator<Item = Lint> {
199     let content = fs::read_to_string(dir_entry.path()).unwrap();
200     let path = dir_entry.path();
201     let filename = path.file_stem().unwrap();
202     let path_buf = path.with_file_name(filename);
203     let mut rel_path = path_buf
204         .strip_prefix(clippy_project_root().join("clippy_lints/src"))
205         .expect("only files in `clippy_lints/src` should be looked at");
206     // If the lints are stored in mod.rs, we get the module name from
207     // the containing directory:
208     if filename == "mod" {
209         rel_path = rel_path.parent().unwrap();
210     }
211
212     let module = rel_path
213         .components()
214         .map(|c| c.as_os_str().to_str().unwrap())
215         .collect::<Vec<_>>()
216         .join("::");
217
218     parse_contents(&content, &module)
219 }
220
221 fn parse_contents(content: &str, module: &str) -> impl Iterator<Item = Lint> {
222     let lints = DEC_CLIPPY_LINT_RE
223         .captures_iter(content)
224         .map(|m| Lint::new(&m["name"], &m["cat"], &m["desc"], None, module));
225     let deprecated = DEC_DEPRECATED_LINT_RE
226         .captures_iter(content)
227         .map(|m| Lint::new(&m["name"], "Deprecated", &m["desc"], Some(&m["desc"]), module));
228     // Removing the `.collect::<Vec<Lint>>().into_iter()` causes some lifetime issues due to the map
229     lints.chain(deprecated).collect::<Vec<Lint>>().into_iter()
230 }
231
232 /// Collects all .rs files in the `clippy_lints/src` directory
233 fn lint_files() -> impl Iterator<Item = walkdir::DirEntry> {
234     // We use `WalkDir` instead of `fs::read_dir` here in order to recurse into subdirectories.
235     // Otherwise we would not collect all the lints, for example in `clippy_lints/src/methods/`.
236     let path = clippy_project_root().join("clippy_lints/src");
237     WalkDir::new(path)
238         .into_iter()
239         .filter_map(Result::ok)
240         .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
241 }
242
243 /// Whether a file has had its text changed or not
244 #[derive(PartialEq, Debug)]
245 pub struct FileChange {
246     pub changed: bool,
247     pub new_lines: String,
248 }
249
250 /// Replaces a region in a file delimited by two lines matching regexes.
251 ///
252 /// `path` is the relative path to the file on which you want to perform the replacement.
253 ///
254 /// See `replace_region_in_text` for documentation of the other options.
255 ///
256 /// # Panics
257 ///
258 /// Panics if the path could not read or then written
259 pub fn replace_region_in_file<F>(
260     path: &Path,
261     start: &str,
262     end: &str,
263     replace_start: bool,
264     write_back: bool,
265     replacements: F,
266 ) -> FileChange
267 where
268     F: FnOnce() -> Vec<String>,
269 {
270     let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from {}: {}", path.display(), e));
271     let file_change = replace_region_in_text(&contents, start, end, replace_start, replacements);
272
273     if write_back {
274         if let Err(e) = fs::write(path, file_change.new_lines.as_bytes()) {
275             panic!("Cannot write to {}: {}", path.display(), e);
276         }
277     }
278     file_change
279 }
280
281 /// Replaces a region in a text delimited by two lines matching regexes.
282 ///
283 /// * `text` is the input text on which you want to perform the replacement
284 /// * `start` is a `&str` that describes the delimiter line before the region you want to replace.
285 ///   As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
286 /// * `end` is a `&str` that describes the delimiter line until where the replacement should happen.
287 ///   As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
288 /// * If `replace_start` is true, the `start` delimiter line is replaced as well. The `end`
289 ///   delimiter line is never replaced.
290 /// * `replacements` is a closure that has to return a `Vec<String>` which contains the new text.
291 ///
292 /// If you want to perform the replacement on files instead of already parsed text,
293 /// use `replace_region_in_file`.
294 ///
295 /// # Example
296 ///
297 /// ```
298 /// let the_text = "replace_start\nsome text\nthat will be replaced\nreplace_end";
299 /// let result =
300 ///     clippy_dev::replace_region_in_text(the_text, "replace_start", "replace_end", false, || {
301 ///         vec!["a different".to_string(), "text".to_string()]
302 ///     })
303 ///     .new_lines;
304 /// assert_eq!("replace_start\na different\ntext\nreplace_end", result);
305 /// ```
306 ///
307 /// # Panics
308 ///
309 /// Panics if start or end is not valid regex
310 pub fn replace_region_in_text<F>(text: &str, start: &str, end: &str, replace_start: bool, replacements: F) -> FileChange
311 where
312     F: FnOnce() -> Vec<String>,
313 {
314     let replace_it = replacements();
315     let mut in_old_region = false;
316     let mut found = false;
317     let mut new_lines = vec![];
318     let start = Regex::new(start).unwrap();
319     let end = Regex::new(end).unwrap();
320
321     for line in text.lines() {
322         if in_old_region {
323             if end.is_match(line) {
324                 in_old_region = false;
325                 new_lines.extend(replace_it.clone());
326                 new_lines.push(line.to_string());
327             }
328         } else if start.is_match(line) {
329             if !replace_start {
330                 new_lines.push(line.to_string());
331             }
332             in_old_region = true;
333             found = true;
334         } else {
335             new_lines.push(line.to_string());
336         }
337     }
338
339     if !found {
340         // This happens if the provided regex in `clippy_dev/src/main.rs` does not match in the
341         // given text or file. Most likely this is an error on the programmer's side and the Regex
342         // is incorrect.
343         eprintln!("error: regex \n{:?}\ndoesn't match. You may have to update it.", start);
344         std::process::exit(1);
345     }
346
347     let mut new_lines = new_lines.join("\n");
348     if text.ends_with('\n') {
349         new_lines.push('\n');
350     }
351     let changed = new_lines != text;
352     FileChange { changed, new_lines }
353 }
354
355 /// Returns the path to the Clippy project directory
356 ///
357 /// # Panics
358 ///
359 /// Panics if the current directory could not be retrieved, there was an error reading any of the
360 /// Cargo.toml files or ancestor directory is the clippy root directory
361 #[must_use]
362 pub fn clippy_project_root() -> PathBuf {
363     let current_dir = std::env::current_dir().unwrap();
364     for path in current_dir.ancestors() {
365         let result = std::fs::read_to_string(path.join("Cargo.toml"));
366         if let Err(err) = &result {
367             if err.kind() == std::io::ErrorKind::NotFound {
368                 continue;
369             }
370         }
371
372         let content = result.unwrap();
373         if content.contains("[package]\nname = \"clippy\"") {
374             return path.to_path_buf();
375         }
376     }
377     panic!("error: Can't determine root of project. Please run inside a Clippy working dir.");
378 }
379
380 #[test]
381 fn test_parse_contents() {
382     let result: Vec<Lint> = parse_contents(
383         r#"
384 declare_clippy_lint! {
385     pub PTR_ARG,
386     style,
387     "really long \
388      text"
389 }
390
391 declare_clippy_lint!{
392     pub DOC_MARKDOWN,
393     pedantic,
394     "single line"
395 }
396
397 /// some doc comment
398 declare_deprecated_lint! {
399     pub SHOULD_ASSERT_EQ,
400     "`assert!()` will be more flexible with RFC 2011"
401 }
402     "#,
403         "module_name",
404     )
405     .collect();
406
407     let expected = vec![
408         Lint::new("ptr_arg", "style", "really long text", None, "module_name"),
409         Lint::new("doc_markdown", "pedantic", "single line", None, "module_name"),
410         Lint::new(
411             "should_assert_eq",
412             "Deprecated",
413             "`assert!()` will be more flexible with RFC 2011",
414             Some("`assert!()` will be more flexible with RFC 2011"),
415             "module_name",
416         ),
417     ];
418     assert_eq!(expected, result);
419 }
420
421 #[test]
422 fn test_replace_region() {
423     let text = "\nabc\n123\n789\ndef\nghi";
424     let expected = FileChange {
425         changed: true,
426         new_lines: "\nabc\nhello world\ndef\nghi".to_string(),
427     };
428     let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, false, || {
429         vec!["hello world".to_string()]
430     });
431     assert_eq!(expected, result);
432 }
433
434 #[test]
435 fn test_replace_region_with_start() {
436     let text = "\nabc\n123\n789\ndef\nghi";
437     let expected = FileChange {
438         changed: true,
439         new_lines: "\nhello world\ndef\nghi".to_string(),
440     };
441     let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, true, || {
442         vec!["hello world".to_string()]
443     });
444     assert_eq!(expected, result);
445 }
446
447 #[test]
448 fn test_replace_region_no_changes() {
449     let text = "123\n456\n789";
450     let expected = FileChange {
451         changed: false,
452         new_lines: "123\n456\n789".to_string(),
453     };
454     let result = replace_region_in_text(text, r#"^\s*123$"#, r#"^\s*456"#, false, Vec::new);
455     assert_eq!(expected, result);
456 }
457
458 #[test]
459 fn test_usable_lints() {
460     let lints = vec![
461         Lint::new("should_assert_eq", "Deprecated", "abc", Some("Reason"), "module_name"),
462         Lint::new("should_assert_eq2", "Not Deprecated", "abc", None, "module_name"),
463         Lint::new("should_assert_eq2", "internal", "abc", None, "module_name"),
464         Lint::new("should_assert_eq2", "internal_style", "abc", None, "module_name"),
465     ];
466     let expected = vec![Lint::new(
467         "should_assert_eq2",
468         "Not Deprecated",
469         "abc",
470         None,
471         "module_name",
472     )];
473     assert_eq!(expected, Lint::usable_lints(&lints));
474 }
475
476 #[test]
477 fn test_by_lint_group() {
478     let lints = vec![
479         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
480         Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
481         Lint::new("incorrect_match", "group1", "abc", None, "module_name"),
482     ];
483     let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
484     expected.insert(
485         "group1".to_string(),
486         vec![
487             Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
488             Lint::new("incorrect_match", "group1", "abc", None, "module_name"),
489         ],
490     );
491     expected.insert(
492         "group2".to_string(),
493         vec![Lint::new("should_assert_eq2", "group2", "abc", None, "module_name")],
494     );
495     assert_eq!(expected, Lint::by_lint_group(lints.into_iter()));
496 }
497
498 #[test]
499 fn test_gen_changelog_lint_list() {
500     let lints = vec![
501         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
502         Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
503     ];
504     let expected = vec![
505         format!("[`should_assert_eq`]: {}#should_assert_eq", DOCS_LINK.to_string()),
506         format!("[`should_assert_eq2`]: {}#should_assert_eq2", DOCS_LINK.to_string()),
507     ];
508     assert_eq!(expected, gen_changelog_lint_list(lints.iter()));
509 }
510
511 #[test]
512 fn test_gen_deprecated() {
513     let lints = vec![
514         Lint::new(
515             "should_assert_eq",
516             "group1",
517             "abc",
518             Some("has been superseded by should_assert_eq2"),
519             "module_name",
520         ),
521         Lint::new(
522             "another_deprecated",
523             "group2",
524             "abc",
525             Some("will be removed"),
526             "module_name",
527         ),
528     ];
529
530     let expected = GENERATED_FILE_COMMENT.to_string()
531         + &[
532             "{",
533             "    store.register_removed(",
534             "        \"clippy::should_assert_eq\",",
535             "        \"has been superseded by should_assert_eq2\",",
536             "    );",
537             "    store.register_removed(",
538             "        \"clippy::another_deprecated\",",
539             "        \"will be removed\",",
540             "    );",
541             "}",
542         ]
543         .join("\n")
544         + "\n";
545
546     assert_eq!(expected, gen_deprecated(lints.iter()));
547 }
548
549 #[test]
550 #[should_panic]
551 fn test_gen_deprecated_fail() {
552     let lints = vec![Lint::new("should_assert_eq2", "group2", "abc", None, "module_name")];
553     let _deprecated_lints = gen_deprecated(lints.iter());
554 }
555
556 #[test]
557 fn test_gen_modules_list() {
558     let lints = vec![
559         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
560         Lint::new("incorrect_stuff", "group3", "abc", None, "another_module"),
561     ];
562     let expected = GENERATED_FILE_COMMENT.to_string() + &["mod another_module;", "mod module_name;"].join("\n") + "\n";
563     assert_eq!(expected, gen_modules_list(lints.iter()));
564 }
565
566 #[test]
567 fn test_gen_lint_group_list() {
568     let lints = vec![
569         Lint::new("abc", "group1", "abc", None, "module_name"),
570         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
571         Lint::new("internal", "internal_style", "abc", None, "module_name"),
572     ];
573     let expected = GENERATED_FILE_COMMENT.to_string()
574         + &[
575             "store.register_group(true, \"clippy::group1\", Some(\"clippy_group1\"), vec![",
576             "    LintId::of(module_name::ABC),",
577             "    LintId::of(module_name::INTERNAL),",
578             "    LintId::of(module_name::SHOULD_ASSERT_EQ),",
579             "])",
580         ]
581         .join("\n")
582         + "\n";
583
584     let result = gen_lint_group_list("group1", lints.iter());
585
586     assert_eq!(expected, result);
587 }