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