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