]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/new_lint.rs
Rollup merge of #88782 - asquared31415:issue-79559, r=cjgillot
[rust.git] / clippy_dev / src / new_lint.rs
1 use crate::clippy_project_root;
2 use std::fs::{self, OpenOptions};
3 use std::io::prelude::*;
4 use std::io::{self, ErrorKind};
5 use std::path::{Path, PathBuf};
6
7 struct LintData<'a> {
8     pass: &'a str,
9     name: &'a str,
10     category: &'a str,
11     project_root: PathBuf,
12 }
13
14 trait Context {
15     fn context<C: AsRef<str>>(self, text: C) -> Self;
16 }
17
18 impl<T> Context for io::Result<T> {
19     fn context<C: AsRef<str>>(self, text: C) -> Self {
20         match self {
21             Ok(t) => Ok(t),
22             Err(e) => {
23                 let message = format!("{}: {}", text.as_ref(), e);
24                 Err(io::Error::new(ErrorKind::Other, message))
25             },
26         }
27     }
28 }
29
30 /// Creates the files required to implement and test a new lint and runs `update_lints`.
31 ///
32 /// # Errors
33 ///
34 /// This function errors out if the files couldn't be created or written to.
35 pub fn create(pass: Option<&str>, lint_name: Option<&str>, category: Option<&str>) -> io::Result<()> {
36     let lint = LintData {
37         pass: pass.expect("`pass` argument is validated by clap"),
38         name: lint_name.expect("`name` argument is validated by clap"),
39         category: category.expect("`category` argument is validated by clap"),
40         project_root: clippy_project_root(),
41     };
42
43     create_lint(&lint).context("Unable to create lint implementation")?;
44     create_test(&lint).context("Unable to create a test for the new lint")
45 }
46
47 fn create_lint(lint: &LintData<'_>) -> io::Result<()> {
48     let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass {
49         "early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
50         "late" => ("LateLintPass", "<'_>", "use rustc_hir::*;", "LateContext"),
51         _ => {
52             unreachable!("`pass_type` should only ever be `early` or `late`!");
53         },
54     };
55
56     let camel_case_name = to_camel_case(lint.name);
57     let lint_contents = get_lint_file_contents(
58         pass_type,
59         pass_lifetimes,
60         lint.name,
61         &camel_case_name,
62         lint.category,
63         pass_import,
64         context_import,
65     );
66
67     let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
68     write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())
69 }
70
71 fn create_test(lint: &LintData<'_>) -> io::Result<()> {
72     fn create_project_layout<P: Into<PathBuf>>(lint_name: &str, location: P, case: &str, hint: &str) -> io::Result<()> {
73         let mut path = location.into().join(case);
74         fs::create_dir(&path)?;
75         write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?;
76
77         path.push("src");
78         fs::create_dir(&path)?;
79         let header = format!("// compile-flags: --crate-name={}", lint_name);
80         write_file(path.join("main.rs"), get_test_file_contents(lint_name, Some(&header)))?;
81
82         Ok(())
83     }
84
85     if lint.category == "cargo" {
86         let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
87         let test_dir = lint.project_root.join(relative_test_dir);
88         fs::create_dir(&test_dir)?;
89
90         create_project_layout(lint.name, &test_dir, "fail", "Content that triggers the lint goes here")?;
91         create_project_layout(lint.name, &test_dir, "pass", "This file should not trigger the lint")
92     } else {
93         let test_path = format!("tests/ui/{}.rs", lint.name);
94         let test_contents = get_test_file_contents(lint.name, None);
95         write_file(lint.project_root.join(test_path), test_contents)
96     }
97 }
98
99 fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
100     fn inner(path: &Path, contents: &[u8]) -> io::Result<()> {
101         OpenOptions::new()
102             .write(true)
103             .create_new(true)
104             .open(path)?
105             .write_all(contents)
106     }
107
108     inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
109 }
110
111 fn to_camel_case(name: &str) -> String {
112     name.split('_')
113         .map(|s| {
114             if s.is_empty() {
115                 String::from("")
116             } else {
117                 [&s[0..1].to_uppercase(), &s[1..]].concat()
118             }
119         })
120         .collect()
121 }
122
123 fn get_test_file_contents(lint_name: &str, header_commands: Option<&str>) -> String {
124     let mut contents = format!(
125         "#![warn(clippy::{})]
126
127 fn main() {{
128     // test code goes here
129 }}
130 ",
131         lint_name
132     );
133
134     if let Some(header) = header_commands {
135         contents = format!("{}\n{}", header, contents);
136     }
137
138     contents
139 }
140
141 fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
142     format!(
143         r#"
144 # {}
145
146 [package]
147 name = "{}"
148 version = "0.1.0"
149 publish = false
150
151 [workspace]
152 "#,
153         hint, lint_name
154     )
155 }
156
157 fn get_lint_file_contents(
158     pass_type: &str,
159     pass_lifetimes: &str,
160     lint_name: &str,
161     camel_case_name: &str,
162     category: &str,
163     pass_import: &str,
164     context_import: &str,
165 ) -> String {
166     format!(
167         "use rustc_lint::{{{type}, {context_import}}};
168 use rustc_session::{{declare_lint_pass, declare_tool_lint}};
169 {pass_import}
170
171 declare_clippy_lint! {{
172     /// ### What it does
173     ///
174     /// ### Why is this bad?
175     ///
176     /// ### Example
177     /// ```rust
178     /// // example code where clippy issues a warning
179     /// ```
180     /// Use instead:
181     /// ```rust
182     /// // example code which does not raise clippy warning
183     /// ```
184     pub {name_upper},
185     {category},
186     \"default lint description\"
187 }}
188
189 declare_lint_pass!({name_camel} => [{name_upper}]);
190
191 impl {type}{lifetimes} for {name_camel} {{}}
192 ",
193         type=pass_type,
194         lifetimes=pass_lifetimes,
195         name_upper=lint_name.to_uppercase(),
196         name_camel=camel_case_name,
197         category=category,
198         pass_import=pass_import,
199         context_import=context_import
200     )
201 }
202
203 #[test]
204 fn test_camel_case() {
205     let s = "a_lint";
206     let s2 = to_camel_case(s);
207     assert_eq!(s2, "ALint");
208
209     let name = "a_really_long_new_lint";
210     let name2 = to_camel_case(name);
211     assert_eq!(name2, "AReallyLongNewLint");
212
213     let name3 = "lint__name";
214     let name4 = to_camel_case(name3);
215     assert_eq!(name4, "LintName");
216 }