]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/new_lint.rs
Auto merge of #8250 - pr2502:fix_repeat_underflow, r=giraffate
[rust.git] / clippy_dev / src / new_lint.rs
1 use crate::clippy_project_root;
2 use indoc::indoc;
3 use std::fs::{self, OpenOptions};
4 use std::io::prelude::*;
5 use std::io::{self, ErrorKind};
6 use std::path::{Path, PathBuf};
7
8 struct LintData<'a> {
9     pass: &'a str,
10     name: &'a str,
11     category: &'a str,
12     project_root: PathBuf,
13 }
14
15 trait Context {
16     fn context<C: AsRef<str>>(self, text: C) -> Self;
17 }
18
19 impl<T> Context for io::Result<T> {
20     fn context<C: AsRef<str>>(self, text: C) -> Self {
21         match self {
22             Ok(t) => Ok(t),
23             Err(e) => {
24                 let message = format!("{}: {}", text.as_ref(), e);
25                 Err(io::Error::new(ErrorKind::Other, message))
26             },
27         }
28     }
29 }
30
31 /// Creates the files required to implement and test a new lint and runs `update_lints`.
32 ///
33 /// # Errors
34 ///
35 /// This function errors out if the files couldn't be created or written to.
36 pub fn create(pass: Option<&str>, lint_name: Option<&str>, category: Option<&str>, msrv: bool) -> io::Result<()> {
37     let lint = LintData {
38         pass: pass.expect("`pass` argument is validated by clap"),
39         name: lint_name.expect("`name` argument is validated by clap"),
40         category: category.expect("`category` argument is validated by clap"),
41         project_root: clippy_project_root(),
42     };
43
44     create_lint(&lint, msrv).context("Unable to create lint implementation")?;
45     create_test(&lint).context("Unable to create a test for the new lint")?;
46     add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")
47 }
48
49 fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
50     let lint_contents = get_lint_file_contents(lint, enable_msrv);
51
52     let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
53     write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())
54 }
55
56 fn create_test(lint: &LintData<'_>) -> io::Result<()> {
57     fn create_project_layout<P: Into<PathBuf>>(lint_name: &str, location: P, case: &str, hint: &str) -> io::Result<()> {
58         let mut path = location.into().join(case);
59         fs::create_dir(&path)?;
60         write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?;
61
62         path.push("src");
63         fs::create_dir(&path)?;
64         let header = format!("// compile-flags: --crate-name={}", lint_name);
65         write_file(path.join("main.rs"), get_test_file_contents(lint_name, Some(&header)))?;
66
67         Ok(())
68     }
69
70     if lint.category == "cargo" {
71         let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
72         let test_dir = lint.project_root.join(relative_test_dir);
73         fs::create_dir(&test_dir)?;
74
75         create_project_layout(lint.name, &test_dir, "fail", "Content that triggers the lint goes here")?;
76         create_project_layout(lint.name, &test_dir, "pass", "This file should not trigger the lint")
77     } else {
78         let test_path = format!("tests/ui/{}.rs", lint.name);
79         let test_contents = get_test_file_contents(lint.name, None);
80         write_file(lint.project_root.join(test_path), test_contents)
81     }
82 }
83
84 fn add_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
85     let path = "clippy_lints/src/lib.rs";
86     let mut lib_rs = fs::read_to_string(path).context("reading")?;
87
88     let comment_start = lib_rs.find("// add lints here,").expect("Couldn't find comment");
89
90     let new_lint = if enable_msrv {
91         format!(
92             "store.register_{lint_pass}_pass(move || Box::new({module_name}::{camel_name}::new(msrv)));\n    ",
93             lint_pass = lint.pass,
94             module_name = lint.name,
95             camel_name = to_camel_case(lint.name),
96         )
97     } else {
98         format!(
99             "store.register_{lint_pass}_pass(|| Box::new({module_name}::{camel_name}));\n    ",
100             lint_pass = lint.pass,
101             module_name = lint.name,
102             camel_name = to_camel_case(lint.name),
103         )
104     };
105
106     lib_rs.insert_str(comment_start, &new_lint);
107
108     fs::write(path, lib_rs).context("writing")
109 }
110
111 fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
112     fn inner(path: &Path, contents: &[u8]) -> io::Result<()> {
113         OpenOptions::new()
114             .write(true)
115             .create_new(true)
116             .open(path)?
117             .write_all(contents)
118     }
119
120     inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
121 }
122
123 fn to_camel_case(name: &str) -> String {
124     name.split('_')
125         .map(|s| {
126             if s.is_empty() {
127                 String::from("")
128             } else {
129                 [&s[0..1].to_uppercase(), &s[1..]].concat()
130             }
131         })
132         .collect()
133 }
134
135 fn get_stabilisation_version() -> String {
136     let mut command = cargo_metadata::MetadataCommand::new();
137     command.no_deps();
138     if let Ok(metadata) = command.exec() {
139         if let Some(pkg) = metadata.packages.iter().find(|pkg| pkg.name == "clippy") {
140             return format!("{}.{}.0", pkg.version.minor, pkg.version.patch);
141         }
142     }
143
144     String::from("<TODO set version(see doc/adding_lints.md)>")
145 }
146
147 fn get_test_file_contents(lint_name: &str, header_commands: Option<&str>) -> String {
148     let mut contents = format!(
149         indoc! {"
150             #![warn(clippy::{})]
151
152             fn main() {{
153                 // test code goes here
154             }}
155         "},
156         lint_name
157     );
158
159     if let Some(header) = header_commands {
160         contents = format!("{}\n{}", header, contents);
161     }
162
163     contents
164 }
165
166 fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
167     format!(
168         indoc! {r#"
169             # {}
170
171             [package]
172             name = "{}"
173             version = "0.1.0"
174             publish = false
175
176             [workspace]
177         "#},
178         hint, lint_name
179     )
180 }
181
182 fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
183     let mut result = String::new();
184
185     let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass {
186         "early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
187         "late" => ("LateLintPass", "<'_>", "use rustc_hir::*;", "LateContext"),
188         _ => {
189             unreachable!("`pass_type` should only ever be `early` or `late`!");
190         },
191     };
192
193     let version = get_stabilisation_version();
194     let lint_name = lint.name;
195     let category = lint.category;
196     let name_camel = to_camel_case(lint.name);
197     let name_upper = lint_name.to_uppercase();
198
199     result.push_str(&if enable_msrv {
200         format!(
201             indoc! {"
202                 use clippy_utils::msrvs;
203                 {pass_import}
204                 use rustc_lint::{{{context_import}, {pass_type}, LintContext}};
205                 use rustc_semver::RustcVersion;
206                 use rustc_session::{{declare_tool_lint, impl_lint_pass}};
207
208             "},
209             pass_type = pass_type,
210             pass_import = pass_import,
211             context_import = context_import,
212         )
213     } else {
214         format!(
215             indoc! {"
216                 {pass_import}
217                 use rustc_lint::{{{context_import}, {pass_type}}};
218                 use rustc_session::{{declare_lint_pass, declare_tool_lint}};
219
220             "},
221             pass_import = pass_import,
222             pass_type = pass_type,
223             context_import = context_import
224         )
225     });
226
227     result.push_str(&format!(
228         indoc! {r#"
229             declare_clippy_lint! {{
230                 /// ### What it does
231                 ///
232                 /// ### Why is this bad?
233                 ///
234                 /// ### Example
235                 /// ```rust
236                 /// // example code where clippy issues a warning
237                 /// ```
238                 /// Use instead:
239                 /// ```rust
240                 /// // example code which does not raise clippy warning
241                 /// ```
242                 #[clippy::version = "{version}"]
243                 pub {name_upper},
244                 {category},
245                 "default lint description"
246             }}
247         "#},
248         version = version,
249         name_upper = name_upper,
250         category = category,
251     ));
252
253     result.push_str(&if enable_msrv {
254         format!(
255             indoc! {"
256                 pub struct {name_camel} {{
257                     msrv: Option<RustcVersion>,
258                 }}
259
260                 impl {name_camel} {{
261                     #[must_use]
262                     pub fn new(msrv: Option<RustcVersion>) -> Self {{
263                         Self {{ msrv }}
264                     }}
265                 }}
266
267                 impl_lint_pass!({name_camel} => [{name_upper}]);
268
269                 impl {pass_type}{pass_lifetimes} for {name_camel} {{
270                     extract_msrv_attr!({context_import});
271                 }}
272
273                 // TODO: Add MSRV level to `clippy_utils/src/msrvs.rs` if needed.
274                 // TODO: Add MSRV test to `tests/ui/min_rust_version_attr.rs`.
275                 // TODO: Update msrv config comment in `clippy_lints/src/utils/conf.rs`
276             "},
277             pass_type = pass_type,
278             pass_lifetimes = pass_lifetimes,
279             name_upper = name_upper,
280             name_camel = name_camel,
281             context_import = context_import,
282         )
283     } else {
284         format!(
285             indoc! {"
286                 declare_lint_pass!({name_camel} => [{name_upper}]);
287
288                 impl {pass_type}{pass_lifetimes} for {name_camel} {{}}
289             "},
290             pass_type = pass_type,
291             pass_lifetimes = pass_lifetimes,
292             name_upper = name_upper,
293             name_camel = name_camel,
294         )
295     });
296
297     result
298 }
299
300 #[test]
301 fn test_camel_case() {
302     let s = "a_lint";
303     let s2 = to_camel_case(s);
304     assert_eq!(s2, "ALint");
305
306     let name = "a_really_long_new_lint";
307     let name2 = to_camel_case(name);
308     assert_eq!(name2, "AReallyLongNewLint");
309
310     let name3 = "lint__name";
311     let name4 = to_camel_case(name3);
312     assert_eq!(name4, "LintName");
313 }