]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_dev/src/new_lint.rs
Rollup merge of #90162 - WaffleLapkin:const_array_slice_from_ref_mut, r=oli-obk
[rust.git] / src / tools / clippy / 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 }
47
48 fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
49     let lint_contents = get_lint_file_contents(lint, enable_msrv);
50
51     let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
52     write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())
53 }
54
55 fn create_test(lint: &LintData<'_>) -> io::Result<()> {
56     fn create_project_layout<P: Into<PathBuf>>(lint_name: &str, location: P, case: &str, hint: &str) -> io::Result<()> {
57         let mut path = location.into().join(case);
58         fs::create_dir(&path)?;
59         write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?;
60
61         path.push("src");
62         fs::create_dir(&path)?;
63         let header = format!("// compile-flags: --crate-name={}", lint_name);
64         write_file(path.join("main.rs"), get_test_file_contents(lint_name, Some(&header)))?;
65
66         Ok(())
67     }
68
69     if lint.category == "cargo" {
70         let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
71         let test_dir = lint.project_root.join(relative_test_dir);
72         fs::create_dir(&test_dir)?;
73
74         create_project_layout(lint.name, &test_dir, "fail", "Content that triggers the lint goes here")?;
75         create_project_layout(lint.name, &test_dir, "pass", "This file should not trigger the lint")
76     } else {
77         let test_path = format!("tests/ui/{}.rs", lint.name);
78         let test_contents = get_test_file_contents(lint.name, None);
79         write_file(lint.project_root.join(test_path), test_contents)
80     }
81 }
82
83 fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
84     fn inner(path: &Path, contents: &[u8]) -> io::Result<()> {
85         OpenOptions::new()
86             .write(true)
87             .create_new(true)
88             .open(path)?
89             .write_all(contents)
90     }
91
92     inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
93 }
94
95 fn to_camel_case(name: &str) -> String {
96     name.split('_')
97         .map(|s| {
98             if s.is_empty() {
99                 String::from("")
100             } else {
101                 [&s[0..1].to_uppercase(), &s[1..]].concat()
102             }
103         })
104         .collect()
105 }
106
107 fn get_test_file_contents(lint_name: &str, header_commands: Option<&str>) -> String {
108     let mut contents = format!(
109         indoc! {"
110             #![warn(clippy::{})]
111
112             fn main() {{
113                 // test code goes here
114             }}
115         "},
116         lint_name
117     );
118
119     if let Some(header) = header_commands {
120         contents = format!("{}\n{}", header, contents);
121     }
122
123     contents
124 }
125
126 fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
127     format!(
128         indoc! {r#"
129             # {}
130
131             [package]
132             name = "{}"
133             version = "0.1.0"
134             publish = false
135
136             [workspace]
137         "#},
138         hint, lint_name
139     )
140 }
141
142 fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
143     let mut result = String::new();
144
145     let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass {
146         "early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
147         "late" => ("LateLintPass", "<'_>", "use rustc_hir::*;", "LateContext"),
148         _ => {
149             unreachable!("`pass_type` should only ever be `early` or `late`!");
150         },
151     };
152
153     let lint_name = lint.name;
154     let pass_name = lint.pass;
155     let category = lint.category;
156     let name_camel = to_camel_case(lint.name);
157     let name_upper = lint_name.to_uppercase();
158
159     result.push_str(&if enable_msrv {
160         format!(
161             indoc! {"
162                 use clippy_utils::msrvs;
163                 {pass_import}
164                 use rustc_lint::{{{context_import}, {pass_type}, LintContext}};
165                 use rustc_semver::RustcVersion;
166                 use rustc_session::{{declare_tool_lint, impl_lint_pass}};
167
168             "},
169             pass_type = pass_type,
170             pass_import = pass_import,
171             context_import = context_import,
172         )
173     } else {
174         format!(
175             indoc! {"
176                 {pass_import}
177                 use rustc_lint::{{{context_import}, {pass_type}}};
178                 use rustc_session::{{declare_lint_pass, declare_tool_lint}};
179
180             "},
181             pass_import = pass_import,
182             pass_type = pass_type,
183             context_import = context_import
184         )
185     });
186
187     result.push_str(&format!(
188         indoc! {"
189             declare_clippy_lint! {{
190                 /// ### What it does
191                 ///
192                 /// ### Why is this bad?
193                 ///
194                 /// ### Example
195                 /// ```rust
196                 /// // example code where clippy issues a warning
197                 /// ```
198                 /// Use instead:
199                 /// ```rust
200                 /// // example code which does not raise clippy warning
201                 /// ```
202                 pub {name_upper},
203                 {category},
204                 \"default lint description\"
205             }}
206         "},
207         name_upper = name_upper,
208         category = category,
209     ));
210
211     result.push_str(&if enable_msrv {
212         format!(
213             indoc! {"
214                 pub struct {name_camel} {{
215                     msrv: Option<RustcVersion>,
216                 }}
217
218                 impl {name_camel} {{
219                     #[must_use]
220                     pub fn new(msrv: Option<RustcVersion>) -> Self {{
221                         Self {{ msrv }}
222                     }}
223                 }}
224
225                 impl_lint_pass!({name_camel} => [{name_upper}]);
226
227                 impl {pass_type}{pass_lifetimes} for {name_camel} {{
228                     extract_msrv_attr!({context_import});
229                 }}
230
231                 // TODO: Register the lint pass in `clippy_lints/src/lib.rs`,
232                 //       e.g. store.register_{pass_name}_pass(move || Box::new({module_name}::{name_camel}::new(msrv)));
233                 // TODO: Add MSRV level to `clippy_utils/src/msrvs.rs` if needed.
234                 // TODO: Add MSRV test to `tests/ui/min_rust_version_attr.rs`.
235                 // TODO: Update msrv config comment in `clippy_lints/src/utils/conf.rs`
236             "},
237             pass_type = pass_type,
238             pass_lifetimes = pass_lifetimes,
239             pass_name = pass_name,
240             name_upper = name_upper,
241             name_camel = name_camel,
242             module_name = lint_name,
243             context_import = context_import,
244         )
245     } else {
246         format!(
247             indoc! {"
248                 declare_lint_pass!({name_camel} => [{name_upper}]);
249
250                 impl {pass_type}{pass_lifetimes} for {name_camel} {{}}
251                 //
252                 // TODO: Register the lint pass in `clippy_lints/src/lib.rs`,
253                 //       e.g. store.register_{pass_name}_pass(|| Box::new({module_name}::{name_camel}));
254             "},
255             pass_type = pass_type,
256             pass_lifetimes = pass_lifetimes,
257             pass_name = pass_name,
258             name_upper = name_upper,
259             name_camel = name_camel,
260             module_name = lint_name,
261         )
262     });
263
264     result
265 }
266
267 #[test]
268 fn test_camel_case() {
269     let s = "a_lint";
270     let s2 = to_camel_case(s);
271     assert_eq!(s2, "ALint");
272
273     let name = "a_really_long_new_lint";
274     let name2 = to_camel_case(name);
275     assert_eq!(name2, "AReallyLongNewLint");
276
277     let name3 = "lint__name";
278     let name4 = to_camel_case(name3);
279     assert_eq!(name4, "LintName");
280 }