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