]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/lib.rs
Auto merge of #4650 - Mark-Simulacrum:clippy-up-lintstore-lockless, r=phansch
[rust.git] / clippy_dev / src / lib.rs
1 use itertools::Itertools;
2 use lazy_static::lazy_static;
3 use regex::Regex;
4 use std::collections::HashMap;
5 use std::ffi::OsStr;
6 use std::fs;
7 use std::io::prelude::*;
8 use walkdir::WalkDir;
9
10 lazy_static! {
11     static ref DEC_CLIPPY_LINT_RE: Regex = Regex::new(
12         r#"(?x)
13         declare_clippy_lint!\s*[\{(]
14         (?:\s+///.*)*
15         \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s*
16         (?P<cat>[a-z_]+)\s*,\s*
17         "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})]
18     "#
19     )
20     .unwrap();
21     static ref DEC_DEPRECATED_LINT_RE: Regex = Regex::new(
22         r#"(?x)
23         declare_deprecated_lint!\s*[{(]\s*
24         (?:\s+///.*)*
25         \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s*
26         "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})]
27     "#
28     )
29     .unwrap();
30     static ref NL_ESCAPE_RE: Regex = Regex::new(r#"\\\n\s*"#).unwrap();
31     pub static ref DOCS_LINK: String = "https://rust-lang.github.io/rust-clippy/master/index.html".to_string();
32 }
33
34 /// Lint data parsed from the Clippy source code.
35 #[derive(Clone, PartialEq, Debug)]
36 pub struct Lint {
37     pub name: String,
38     pub group: String,
39     pub desc: String,
40     pub deprecation: Option<String>,
41     pub module: String,
42 }
43
44 impl Lint {
45     #[must_use]
46     pub fn new(name: &str, group: &str, desc: &str, deprecation: Option<&str>, module: &str) -> Self {
47         Self {
48             name: name.to_lowercase(),
49             group: group.to_string(),
50             desc: NL_ESCAPE_RE.replace(&desc.replace("\\\"", "\""), "").to_string(),
51             deprecation: deprecation.map(std::string::ToString::to_string),
52             module: module.to_string(),
53         }
54     }
55
56     /// Returns all non-deprecated lints and non-internal lints
57     pub fn usable_lints(lints: impl Iterator<Item = Self>) -> impl Iterator<Item = Self> {
58         lints.filter(|l| l.deprecation.is_none() && !l.is_internal())
59     }
60
61     /// Returns the lints in a `HashMap`, grouped by the different lint groups
62     #[must_use]
63     pub fn by_lint_group(lints: &[Self]) -> HashMap<String, Vec<Self>> {
64         lints
65             .iter()
66             .map(|lint| (lint.group.to_string(), lint.clone()))
67             .into_group_map()
68     }
69
70     #[must_use]
71     pub fn is_internal(&self) -> bool {
72         self.group.starts_with("internal")
73     }
74 }
75
76 /// Generates the Vec items for `register_lint_group` calls in `clippy_lints/src/lib.rs`.
77 #[must_use]
78 pub fn gen_lint_group_list(lints: Vec<Lint>) -> Vec<String> {
79     lints
80         .into_iter()
81         .filter_map(|l| {
82             if l.is_internal() || l.deprecation.is_some() {
83                 None
84             } else {
85                 Some(format!("        LintId::of(&{}::{}),", l.module, l.name.to_uppercase()))
86             }
87         })
88         .sorted()
89         .collect::<Vec<String>>()
90 }
91
92 /// Generates the `pub mod module_name` list in `clippy_lints/src/lib.rs`.
93 #[must_use]
94 pub fn gen_modules_list(lints: Vec<Lint>) -> Vec<String> {
95     lints
96         .into_iter()
97         .filter_map(|l| {
98             if l.is_internal() || l.deprecation.is_some() {
99                 None
100             } else {
101                 Some(l.module)
102             }
103         })
104         .unique()
105         .map(|module| format!("pub mod {};", module))
106         .sorted()
107         .collect::<Vec<String>>()
108 }
109
110 /// Generates the list of lint links at the bottom of the README
111 #[must_use]
112 pub fn gen_changelog_lint_list(lints: Vec<Lint>) -> Vec<String> {
113     let mut lint_list_sorted: Vec<Lint> = lints;
114     lint_list_sorted.sort_by_key(|l| l.name.clone());
115     lint_list_sorted
116         .iter()
117         .filter_map(|l| {
118             if l.is_internal() {
119                 None
120             } else {
121                 Some(format!("[`{}`]: {}#{}", l.name, DOCS_LINK.clone(), l.name))
122             }
123         })
124         .collect()
125 }
126
127 /// Generates the `register_removed` code in `./clippy_lints/src/lib.rs`.
128 #[must_use]
129 pub fn gen_deprecated(lints: &[Lint]) -> Vec<String> {
130     lints
131         .iter()
132         .filter_map(|l| {
133             l.clone().deprecation.map(|depr_text| {
134                 vec![
135                     "    store.register_removed(".to_string(),
136                     format!("        \"clippy::{}\",", l.name),
137                     format!("        \"{}\",", depr_text),
138                     "    );".to_string(),
139                 ]
140             })
141         })
142         .flatten()
143         .collect::<Vec<String>>()
144 }
145
146 #[must_use]
147 pub fn gen_register_lint_list(lints: &[Lint]) -> Vec<String> {
148     let pre = "    store.register_lints(&[".to_string();
149     let post = "    ]);".to_string();
150     let mut inner = lints
151         .iter()
152         .filter_map(|l| {
153             if !l.is_internal() && l.deprecation.is_none() {
154                 Some(format!("        &{}::{},", l.module, l.name.to_uppercase()))
155             } else {
156                 None
157             }
158         })
159         .sorted()
160         .collect::<Vec<String>>();
161     inner.insert(0, pre);
162     inner.push(post);
163     inner
164 }
165
166 /// Gathers all files in `src/clippy_lints` and gathers all lints inside
167 pub fn gather_all() -> impl Iterator<Item = Lint> {
168     lint_files().flat_map(|f| gather_from_file(&f))
169 }
170
171 fn gather_from_file(dir_entry: &walkdir::DirEntry) -> impl Iterator<Item = Lint> {
172     let mut file = fs::File::open(dir_entry.path()).unwrap();
173     let mut content = String::new();
174     file.read_to_string(&mut content).unwrap();
175     let mut filename = dir_entry.path().file_stem().unwrap().to_str().unwrap();
176     // If the lints are stored in mod.rs, we get the module name from
177     // the containing directory:
178     if filename == "mod" {
179         filename = dir_entry
180             .path()
181             .parent()
182             .unwrap()
183             .file_stem()
184             .unwrap()
185             .to_str()
186             .unwrap()
187     }
188     parse_contents(&content, filename)
189 }
190
191 fn parse_contents(content: &str, filename: &str) -> impl Iterator<Item = Lint> {
192     let lints = DEC_CLIPPY_LINT_RE
193         .captures_iter(content)
194         .map(|m| Lint::new(&m["name"], &m["cat"], &m["desc"], None, filename));
195     let deprecated = DEC_DEPRECATED_LINT_RE
196         .captures_iter(content)
197         .map(|m| Lint::new(&m["name"], "Deprecated", &m["desc"], Some(&m["desc"]), filename));
198     // Removing the `.collect::<Vec<Lint>>().into_iter()` causes some lifetime issues due to the map
199     lints.chain(deprecated).collect::<Vec<Lint>>().into_iter()
200 }
201
202 /// Collects all .rs files in the `clippy_lints/src` directory
203 fn lint_files() -> impl Iterator<Item = walkdir::DirEntry> {
204     // We use `WalkDir` instead of `fs::read_dir` here in order to recurse into subdirectories.
205     // Otherwise we would not collect all the lints, for example in `clippy_lints/src/methods/`.
206     WalkDir::new("../clippy_lints/src")
207         .into_iter()
208         .filter_map(std::result::Result::ok)
209         .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
210 }
211
212 /// Whether a file has had its text changed or not
213 #[derive(PartialEq, Debug)]
214 pub struct FileChange {
215     pub changed: bool,
216     pub new_lines: String,
217 }
218
219 /// Replaces a region in a file delimited by two lines matching regexes.
220 ///
221 /// `path` is the relative path to the file on which you want to perform the replacement.
222 ///
223 /// See `replace_region_in_text` for documentation of the other options.
224 #[allow(clippy::expect_fun_call)]
225 pub fn replace_region_in_file<F>(
226     path: &str,
227     start: &str,
228     end: &str,
229     replace_start: bool,
230     write_back: bool,
231     replacements: F,
232 ) -> FileChange
233 where
234     F: Fn() -> Vec<String>,
235 {
236     let mut f = fs::File::open(path).expect(&format!("File not found: {}", path));
237     let mut contents = String::new();
238     f.read_to_string(&mut contents)
239         .expect("Something went wrong reading the file");
240     let file_change = replace_region_in_text(&contents, start, end, replace_start, replacements);
241
242     if write_back {
243         let mut f = fs::File::create(path).expect(&format!("File not found: {}", path));
244         f.write_all(file_change.new_lines.as_bytes())
245             .expect("Unable to write file");
246         // Ensure we write the changes with a trailing newline so that
247         // the file has the proper line endings.
248         f.write_all(b"\n").expect("Unable to write file");
249     }
250     file_change
251 }
252
253 /// Replaces a region in a text delimited by two lines matching regexes.
254 ///
255 /// * `text` is the input text on which you want to perform the replacement
256 /// * `start` is a `&str` that describes the delimiter line before the region you want to replace.
257 ///   As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
258 /// * `end` is a `&str` that describes the delimiter line until where the replacement should happen.
259 ///   As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
260 /// * If `replace_start` is true, the `start` delimiter line is replaced as well. The `end`
261 ///   delimiter line is never replaced.
262 /// * `replacements` is a closure that has to return a `Vec<String>` which contains the new text.
263 ///
264 /// If you want to perform the replacement on files instead of already parsed text,
265 /// use `replace_region_in_file`.
266 ///
267 /// # Example
268 ///
269 /// ```
270 /// let the_text = "replace_start\nsome text\nthat will be replaced\nreplace_end";
271 /// let result = clippy_dev::replace_region_in_text(the_text, r#"replace_start"#, r#"replace_end"#, false, || {
272 ///     vec!["a different".to_string(), "text".to_string()]
273 /// })
274 /// .new_lines;
275 /// assert_eq!("replace_start\na different\ntext\nreplace_end", result);
276 /// ```
277 pub fn replace_region_in_text<F>(text: &str, start: &str, end: &str, replace_start: bool, replacements: F) -> FileChange
278 where
279     F: Fn() -> Vec<String>,
280 {
281     let lines = text.lines();
282     let mut in_old_region = false;
283     let mut found = false;
284     let mut new_lines = vec![];
285     let start = Regex::new(start).unwrap();
286     let end = Regex::new(end).unwrap();
287
288     for line in lines.clone() {
289         if in_old_region {
290             if end.is_match(&line) {
291                 in_old_region = false;
292                 new_lines.extend(replacements());
293                 new_lines.push(line.to_string());
294             }
295         } else if start.is_match(&line) {
296             if !replace_start {
297                 new_lines.push(line.to_string());
298             }
299             in_old_region = true;
300             found = true;
301         } else {
302             new_lines.push(line.to_string());
303         }
304     }
305
306     if !found {
307         // This happens if the provided regex in `clippy_dev/src/main.rs` is not found in the
308         // given text or file. Most likely this is an error on the programmer's side and the Regex
309         // is incorrect.
310         eprintln!("error: regex `{:?}` not found. You may have to update it.", start);
311     }
312
313     FileChange {
314         changed: lines.ne(new_lines.clone()),
315         new_lines: new_lines.join("\n"),
316     }
317 }
318
319 #[test]
320 fn test_parse_contents() {
321     let result: Vec<Lint> = parse_contents(
322         r#"
323 declare_clippy_lint! {
324     pub PTR_ARG,
325     style,
326     "really long \
327      text"
328 }
329
330 declare_clippy_lint!{
331     pub DOC_MARKDOWN,
332     pedantic,
333     "single line"
334 }
335
336 /// some doc comment
337 declare_deprecated_lint! {
338     pub SHOULD_ASSERT_EQ,
339     "`assert!()` will be more flexible with RFC 2011"
340 }
341     "#,
342         "module_name",
343     )
344     .collect();
345
346     let expected = vec![
347         Lint::new("ptr_arg", "style", "really long text", None, "module_name"),
348         Lint::new("doc_markdown", "pedantic", "single line", None, "module_name"),
349         Lint::new(
350             "should_assert_eq",
351             "Deprecated",
352             "`assert!()` will be more flexible with RFC 2011",
353             Some("`assert!()` will be more flexible with RFC 2011"),
354             "module_name",
355         ),
356     ];
357     assert_eq!(expected, result);
358 }
359
360 #[test]
361 fn test_replace_region() {
362     let text = "\nabc\n123\n789\ndef\nghi";
363     let expected = FileChange {
364         changed: true,
365         new_lines: "\nabc\nhello world\ndef\nghi".to_string(),
366     };
367     let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, false, || {
368         vec!["hello world".to_string()]
369     });
370     assert_eq!(expected, result);
371 }
372
373 #[test]
374 fn test_replace_region_with_start() {
375     let text = "\nabc\n123\n789\ndef\nghi";
376     let expected = FileChange {
377         changed: true,
378         new_lines: "\nhello world\ndef\nghi".to_string(),
379     };
380     let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, true, || {
381         vec!["hello world".to_string()]
382     });
383     assert_eq!(expected, result);
384 }
385
386 #[test]
387 fn test_replace_region_no_changes() {
388     let text = "123\n456\n789";
389     let expected = FileChange {
390         changed: false,
391         new_lines: "123\n456\n789".to_string(),
392     };
393     let result = replace_region_in_text(text, r#"^\s*123$"#, r#"^\s*456"#, false, || vec![]);
394     assert_eq!(expected, result);
395 }
396
397 #[test]
398 fn test_usable_lints() {
399     let lints = vec![
400         Lint::new("should_assert_eq", "Deprecated", "abc", Some("Reason"), "module_name"),
401         Lint::new("should_assert_eq2", "Not Deprecated", "abc", None, "module_name"),
402         Lint::new("should_assert_eq2", "internal", "abc", None, "module_name"),
403         Lint::new("should_assert_eq2", "internal_style", "abc", None, "module_name"),
404     ];
405     let expected = vec![Lint::new(
406         "should_assert_eq2",
407         "Not Deprecated",
408         "abc",
409         None,
410         "module_name",
411     )];
412     assert_eq!(expected, Lint::usable_lints(lints.into_iter()).collect::<Vec<Lint>>());
413 }
414
415 #[test]
416 fn test_by_lint_group() {
417     let lints = vec![
418         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
419         Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
420         Lint::new("incorrect_match", "group1", "abc", None, "module_name"),
421     ];
422     let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
423     expected.insert(
424         "group1".to_string(),
425         vec![
426             Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
427             Lint::new("incorrect_match", "group1", "abc", None, "module_name"),
428         ],
429     );
430     expected.insert(
431         "group2".to_string(),
432         vec![Lint::new("should_assert_eq2", "group2", "abc", None, "module_name")],
433     );
434     assert_eq!(expected, Lint::by_lint_group(&lints));
435 }
436
437 #[test]
438 fn test_gen_changelog_lint_list() {
439     let lints = vec![
440         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
441         Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
442         Lint::new("incorrect_internal", "internal_style", "abc", None, "module_name"),
443     ];
444     let expected = vec![
445         format!("[`should_assert_eq`]: {}#should_assert_eq", DOCS_LINK.to_string()),
446         format!("[`should_assert_eq2`]: {}#should_assert_eq2", DOCS_LINK.to_string()),
447     ];
448     assert_eq!(expected, gen_changelog_lint_list(lints));
449 }
450
451 #[test]
452 fn test_gen_deprecated() {
453     let lints = vec![
454         Lint::new(
455             "should_assert_eq",
456             "group1",
457             "abc",
458             Some("has been superseded by should_assert_eq2"),
459             "module_name",
460         ),
461         Lint::new(
462             "another_deprecated",
463             "group2",
464             "abc",
465             Some("will be removed"),
466             "module_name",
467         ),
468         Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
469     ];
470     let expected: Vec<String> = vec![
471         "    store.register_removed(",
472         "        \"clippy::should_assert_eq\",",
473         "        \"has been superseded by should_assert_eq2\",",
474         "    );",
475         "    store.register_removed(",
476         "        \"clippy::another_deprecated\",",
477         "        \"will be removed\",",
478         "    );",
479     ]
480     .into_iter()
481     .map(String::from)
482     .collect();
483     assert_eq!(expected, gen_deprecated(&lints));
484 }
485
486 #[test]
487 fn test_gen_modules_list() {
488     let lints = vec![
489         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
490         Lint::new("should_assert_eq2", "group2", "abc", Some("abc"), "deprecated"),
491         Lint::new("incorrect_stuff", "group3", "abc", None, "another_module"),
492         Lint::new("incorrect_internal", "internal_style", "abc", None, "module_name"),
493     ];
494     let expected = vec![
495         "pub mod another_module;".to_string(),
496         "pub mod module_name;".to_string(),
497     ];
498     assert_eq!(expected, gen_modules_list(lints));
499 }
500
501 #[test]
502 fn test_gen_lint_group_list() {
503     let lints = vec![
504         Lint::new("abc", "group1", "abc", None, "module_name"),
505         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
506         Lint::new("should_assert_eq2", "group2", "abc", Some("abc"), "deprecated"),
507         Lint::new("incorrect_internal", "internal_style", "abc", None, "module_name"),
508     ];
509     let expected = vec![
510         "        LintId::of(&module_name::ABC),".to_string(),
511         "        LintId::of(&module_name::SHOULD_ASSERT_EQ),".to_string(),
512     ];
513     assert_eq!(expected, gen_lint_group_list(lints));
514 }