]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/lib.rs
Make Lint::by_lint_group take impl Iterator as argument
[rust.git] / clippy_dev / src / lib.rs
1 #![cfg_attr(feature = "deny-warnings", deny(warnings))]
2
3 use itertools::Itertools;
4 use lazy_static::lazy_static;
5 use regex::Regex;
6 use std::collections::HashMap;
7 use std::ffi::OsStr;
8 use std::fs;
9 use std::path::{Path, PathBuf};
10 use walkdir::WalkDir;
11
12 lazy_static! {
13     static ref DEC_CLIPPY_LINT_RE: Regex = Regex::new(
14         r#"(?x)
15         declare_clippy_lint!\s*[\{(]
16         (?:\s+///.*)*
17         \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s*
18         (?P<cat>[a-z_]+)\s*,\s*
19         "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})]
20     "#
21     )
22     .unwrap();
23     static ref DEC_DEPRECATED_LINT_RE: Regex = Regex::new(
24         r#"(?x)
25         declare_deprecated_lint!\s*[{(]\s*
26         (?:\s+///.*)*
27         \s+pub\s+(?P<name>[A-Z_][A-Z_0-9]*)\s*,\s*
28         "(?P<desc>(?:[^"\\]+|\\(?s).(?-s))*)"\s*[})]
29     "#
30     )
31     .unwrap();
32     static ref NL_ESCAPE_RE: Regex = Regex::new(r#"\\\n\s*"#).unwrap();
33 }
34
35 pub static DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
36
37 /// Lint data parsed from the Clippy source code.
38 #[derive(Clone, PartialEq, Debug)]
39 pub struct Lint {
40     pub name: String,
41     pub group: String,
42     pub desc: String,
43     pub deprecation: Option<String>,
44     pub module: String,
45 }
46
47 impl Lint {
48     #[must_use]
49     pub fn new(name: &str, group: &str, desc: &str, deprecation: Option<&str>, module: &str) -> Self {
50         Self {
51             name: name.to_lowercase(),
52             group: group.to_string(),
53             desc: NL_ESCAPE_RE.replace(&desc.replace("\\\"", "\""), "").to_string(),
54             deprecation: deprecation.map(ToString::to_string),
55             module: module.to_string(),
56         }
57     }
58
59     /// Returns all non-deprecated lints and non-internal lints
60     pub fn usable_lints(lints: impl Iterator<Item = Self>) -> impl Iterator<Item = Self> {
61         lints.filter(|l| l.deprecation.is_none() && !l.is_internal())
62     }
63
64     /// Returns the lints in a `HashMap`, grouped by the different lint groups
65     #[must_use]
66     pub fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>> {
67         lints.map(|lint| (lint.group.to_string(), lint)).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, 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 content = fs::read_to_string(dir_entry.path()).unwrap();
173     let mut filename = dir_entry.path().file_stem().unwrap().to_str().unwrap();
174     // If the lints are stored in mod.rs, we get the module name from
175     // the containing directory:
176     if filename == "mod" {
177         filename = dir_entry
178             .path()
179             .parent()
180             .unwrap()
181             .file_stem()
182             .unwrap()
183             .to_str()
184             .unwrap()
185     }
186     parse_contents(&content, filename)
187 }
188
189 fn parse_contents(content: &str, filename: &str) -> impl Iterator<Item = Lint> {
190     let lints = DEC_CLIPPY_LINT_RE
191         .captures_iter(content)
192         .map(|m| Lint::new(&m["name"], &m["cat"], &m["desc"], None, filename));
193     let deprecated = DEC_DEPRECATED_LINT_RE
194         .captures_iter(content)
195         .map(|m| Lint::new(&m["name"], "Deprecated", &m["desc"], Some(&m["desc"]), filename));
196     // Removing the `.collect::<Vec<Lint>>().into_iter()` causes some lifetime issues due to the map
197     lints.chain(deprecated).collect::<Vec<Lint>>().into_iter()
198 }
199
200 /// Collects all .rs files in the `clippy_lints/src` directory
201 fn lint_files() -> impl Iterator<Item = walkdir::DirEntry> {
202     // We use `WalkDir` instead of `fs::read_dir` here in order to recurse into subdirectories.
203     // Otherwise we would not collect all the lints, for example in `clippy_lints/src/methods/`.
204     let path = clippy_project_root().join("clippy_lints/src");
205     WalkDir::new(path)
206         .into_iter()
207         .filter_map(Result::ok)
208         .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
209 }
210
211 /// Whether a file has had its text changed or not
212 #[derive(PartialEq, Debug)]
213 pub struct FileChange {
214     pub changed: bool,
215     pub new_lines: String,
216 }
217
218 /// Replaces a region in a file delimited by two lines matching regexes.
219 ///
220 /// `path` is the relative path to the file on which you want to perform the replacement.
221 ///
222 /// See `replace_region_in_text` for documentation of the other options.
223 pub fn replace_region_in_file<F>(
224     path: &Path,
225     start: &str,
226     end: &str,
227     replace_start: bool,
228     write_back: bool,
229     replacements: F,
230 ) -> FileChange
231 where
232     F: FnOnce() -> Vec<String>,
233 {
234     let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from {}: {}", path.display(), e));
235     let file_change = replace_region_in_text(&contents, start, end, replace_start, replacements);
236
237     if write_back {
238         if let Err(e) = fs::write(path, file_change.new_lines.as_bytes()) {
239             panic!("Cannot write to {}: {}", path.display(), e);
240         }
241     }
242     file_change
243 }
244
245 /// Replaces a region in a text delimited by two lines matching regexes.
246 ///
247 /// * `text` is the input text on which you want to perform the replacement
248 /// * `start` is a `&str` that describes the delimiter line before the region you want to replace.
249 ///   As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
250 /// * `end` is a `&str` that describes the delimiter line until where the replacement should happen.
251 ///   As the `&str` will be converted to a `Regex`, this can contain regex syntax, too.
252 /// * If `replace_start` is true, the `start` delimiter line is replaced as well. The `end`
253 ///   delimiter line is never replaced.
254 /// * `replacements` is a closure that has to return a `Vec<String>` which contains the new text.
255 ///
256 /// If you want to perform the replacement on files instead of already parsed text,
257 /// use `replace_region_in_file`.
258 ///
259 /// # Example
260 ///
261 /// ```
262 /// let the_text = "replace_start\nsome text\nthat will be replaced\nreplace_end";
263 /// let result =
264 ///     clippy_dev::replace_region_in_text(the_text, "replace_start", "replace_end", false, || {
265 ///         vec!["a different".to_string(), "text".to_string()]
266 ///     })
267 ///     .new_lines;
268 /// assert_eq!("replace_start\na different\ntext\nreplace_end", result);
269 /// ```
270 pub fn replace_region_in_text<F>(text: &str, start: &str, end: &str, replace_start: bool, replacements: F) -> FileChange
271 where
272     F: FnOnce() -> Vec<String>,
273 {
274     let replace_it = replacements();
275     let mut in_old_region = false;
276     let mut found = false;
277     let mut new_lines = vec![];
278     let start = Regex::new(start).unwrap();
279     let end = Regex::new(end).unwrap();
280
281     for line in text.lines() {
282         if in_old_region {
283             if end.is_match(line) {
284                 in_old_region = false;
285                 new_lines.extend(replace_it.clone());
286                 new_lines.push(line.to_string());
287             }
288         } else if start.is_match(line) {
289             if !replace_start {
290                 new_lines.push(line.to_string());
291             }
292             in_old_region = true;
293             found = true;
294         } else {
295             new_lines.push(line.to_string());
296         }
297     }
298
299     if !found {
300         // This happens if the provided regex in `clippy_dev/src/main.rs` is not found in the
301         // given text or file. Most likely this is an error on the programmer's side and the Regex
302         // is incorrect.
303         eprintln!("error: regex `{:?}` not found. You may have to update it.", start);
304     }
305
306     let mut new_lines = new_lines.join("\n");
307     if text.ends_with('\n') {
308         new_lines.push('\n');
309     }
310     let changed = new_lines != text;
311     FileChange { changed, new_lines }
312 }
313
314 /// Returns the path to the Clippy project directory
315 #[must_use]
316 pub fn clippy_project_root() -> PathBuf {
317     let current_dir = std::env::current_dir().unwrap();
318     for path in current_dir.ancestors() {
319         let result = std::fs::read_to_string(path.join("Cargo.toml"));
320         if let Err(err) = &result {
321             if err.kind() == std::io::ErrorKind::NotFound {
322                 continue;
323             }
324         }
325
326         let content = result.unwrap();
327         if content.contains("[package]\nname = \"clippy\"") {
328             return path.to_path_buf();
329         }
330     }
331     panic!("error: Can't determine root of project. Please run inside a Clippy working dir.");
332 }
333
334 #[test]
335 fn test_parse_contents() {
336     let result: Vec<Lint> = parse_contents(
337         r#"
338 declare_clippy_lint! {
339     pub PTR_ARG,
340     style,
341     "really long \
342      text"
343 }
344
345 declare_clippy_lint!{
346     pub DOC_MARKDOWN,
347     pedantic,
348     "single line"
349 }
350
351 /// some doc comment
352 declare_deprecated_lint! {
353     pub SHOULD_ASSERT_EQ,
354     "`assert!()` will be more flexible with RFC 2011"
355 }
356     "#,
357         "module_name",
358     )
359     .collect();
360
361     let expected = vec![
362         Lint::new("ptr_arg", "style", "really long text", None, "module_name"),
363         Lint::new("doc_markdown", "pedantic", "single line", None, "module_name"),
364         Lint::new(
365             "should_assert_eq",
366             "Deprecated",
367             "`assert!()` will be more flexible with RFC 2011",
368             Some("`assert!()` will be more flexible with RFC 2011"),
369             "module_name",
370         ),
371     ];
372     assert_eq!(expected, result);
373 }
374
375 #[test]
376 fn test_replace_region() {
377     let text = "\nabc\n123\n789\ndef\nghi";
378     let expected = FileChange {
379         changed: true,
380         new_lines: "\nabc\nhello world\ndef\nghi".to_string(),
381     };
382     let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, false, || {
383         vec!["hello world".to_string()]
384     });
385     assert_eq!(expected, result);
386 }
387
388 #[test]
389 fn test_replace_region_with_start() {
390     let text = "\nabc\n123\n789\ndef\nghi";
391     let expected = FileChange {
392         changed: true,
393         new_lines: "\nhello world\ndef\nghi".to_string(),
394     };
395     let result = replace_region_in_text(text, r#"^\s*abc$"#, r#"^\s*def"#, true, || {
396         vec!["hello world".to_string()]
397     });
398     assert_eq!(expected, result);
399 }
400
401 #[test]
402 fn test_replace_region_no_changes() {
403     let text = "123\n456\n789";
404     let expected = FileChange {
405         changed: false,
406         new_lines: "123\n456\n789".to_string(),
407     };
408     let result = replace_region_in_text(text, r#"^\s*123$"#, r#"^\s*456"#, false, || vec![]);
409     assert_eq!(expected, result);
410 }
411
412 #[test]
413 fn test_usable_lints() {
414     let lints = vec![
415         Lint::new("should_assert_eq", "Deprecated", "abc", Some("Reason"), "module_name"),
416         Lint::new("should_assert_eq2", "Not Deprecated", "abc", None, "module_name"),
417         Lint::new("should_assert_eq2", "internal", "abc", None, "module_name"),
418         Lint::new("should_assert_eq2", "internal_style", "abc", None, "module_name"),
419     ];
420     let expected = vec![Lint::new(
421         "should_assert_eq2",
422         "Not Deprecated",
423         "abc",
424         None,
425         "module_name",
426     )];
427     assert_eq!(expected, Lint::usable_lints(lints.into_iter()).collect::<Vec<Lint>>());
428 }
429
430 #[test]
431 fn test_by_lint_group() {
432     let lints = vec![
433         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
434         Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
435         Lint::new("incorrect_match", "group1", "abc", None, "module_name"),
436     ];
437     let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
438     expected.insert(
439         "group1".to_string(),
440         vec![
441             Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
442             Lint::new("incorrect_match", "group1", "abc", None, "module_name"),
443         ],
444     );
445     expected.insert(
446         "group2".to_string(),
447         vec![Lint::new("should_assert_eq2", "group2", "abc", None, "module_name")],
448     );
449     assert_eq!(expected, Lint::by_lint_group(lints.into_iter()));
450 }
451
452 #[test]
453 fn test_gen_changelog_lint_list() {
454     let lints = vec![
455         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
456         Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
457         Lint::new("incorrect_internal", "internal_style", "abc", None, "module_name"),
458     ];
459     let expected = vec![
460         format!("[`should_assert_eq`]: {}#should_assert_eq", DOCS_LINK.to_string()),
461         format!("[`should_assert_eq2`]: {}#should_assert_eq2", DOCS_LINK.to_string()),
462     ];
463     assert_eq!(expected, gen_changelog_lint_list(lints));
464 }
465
466 #[test]
467 fn test_gen_deprecated() {
468     let lints = vec![
469         Lint::new(
470             "should_assert_eq",
471             "group1",
472             "abc",
473             Some("has been superseded by should_assert_eq2"),
474             "module_name",
475         ),
476         Lint::new(
477             "another_deprecated",
478             "group2",
479             "abc",
480             Some("will be removed"),
481             "module_name",
482         ),
483         Lint::new("should_assert_eq2", "group2", "abc", None, "module_name"),
484     ];
485     let expected: Vec<String> = vec![
486         "    store.register_removed(",
487         "        \"clippy::should_assert_eq\",",
488         "        \"has been superseded by should_assert_eq2\",",
489         "    );",
490         "    store.register_removed(",
491         "        \"clippy::another_deprecated\",",
492         "        \"will be removed\",",
493         "    );",
494     ]
495     .into_iter()
496     .map(String::from)
497     .collect();
498     assert_eq!(expected, gen_deprecated(&lints));
499 }
500
501 #[test]
502 fn test_gen_modules_list() {
503     let lints = vec![
504         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
505         Lint::new("should_assert_eq2", "group2", "abc", Some("abc"), "deprecated"),
506         Lint::new("incorrect_stuff", "group3", "abc", None, "another_module"),
507         Lint::new("incorrect_internal", "internal_style", "abc", None, "module_name"),
508     ];
509     let expected = vec![
510         "pub mod another_module;".to_string(),
511         "pub mod module_name;".to_string(),
512     ];
513     assert_eq!(expected, gen_modules_list(lints));
514 }
515
516 #[test]
517 fn test_gen_lint_group_list() {
518     let lints = vec![
519         Lint::new("abc", "group1", "abc", None, "module_name"),
520         Lint::new("should_assert_eq", "group1", "abc", None, "module_name"),
521         Lint::new("should_assert_eq2", "group2", "abc", Some("abc"), "deprecated"),
522         Lint::new("incorrect_internal", "internal_style", "abc", None, "module_name"),
523     ];
524     let expected = vec![
525         "        LintId::of(&module_name::ABC),".to_string(),
526         "        LintId::of(&module_name::SHOULD_ASSERT_EQ),".to_string(),
527     ];
528     assert_eq!(expected, gen_lint_group_list(lints));
529 }