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