]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/new_lint.rs
Auto merge of #99743 - compiler-errors:fulfillment-context-cleanups, r=jackh726
[rust.git] / clippy_dev / src / new_lint.rs
1 use crate::clippy_project_root;
2 use indoc::{indoc, writedoc};
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     ty: Option<&'a str>,
14     project_root: PathBuf,
15 }
16
17 trait Context {
18     fn context<C: AsRef<str>>(self, text: C) -> Self;
19 }
20
21 impl<T> Context for io::Result<T> {
22     fn context<C: AsRef<str>>(self, text: C) -> Self {
23         match self {
24             Ok(t) => Ok(t),
25             Err(e) => {
26                 let message = format!("{}: {}", text.as_ref(), e);
27                 Err(io::Error::new(ErrorKind::Other, message))
28             },
29         }
30     }
31 }
32
33 /// Creates the files required to implement and test a new lint and runs `update_lints`.
34 ///
35 /// # Errors
36 ///
37 /// This function errors out if the files couldn't be created or written to.
38 pub fn create(
39     pass: Option<&String>,
40     lint_name: Option<&String>,
41     category: Option<&str>,
42     mut ty: Option<&str>,
43     msrv: bool,
44 ) -> io::Result<()> {
45     if category == Some("cargo") && ty.is_none() {
46         // `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo`
47         ty = Some("cargo");
48     }
49
50     let lint = LintData {
51         pass: pass.map_or("", String::as_str),
52         name: lint_name.expect("`name` argument is validated by clap"),
53         category: category.expect("`category` argument is validated by clap"),
54         ty,
55         project_root: clippy_project_root(),
56     };
57
58     create_lint(&lint, msrv).context("Unable to create lint implementation")?;
59     create_test(&lint).context("Unable to create a test for the new lint")?;
60
61     if lint.ty.is_none() {
62         add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")?;
63     }
64
65     Ok(())
66 }
67
68 fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
69     if let Some(ty) = lint.ty {
70         create_lint_for_ty(lint, enable_msrv, ty)
71     } else {
72         let lint_contents = get_lint_file_contents(lint, enable_msrv);
73         let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
74         write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())?;
75         println!("Generated lint file: `{}`", lint_path);
76
77         Ok(())
78     }
79 }
80
81 fn create_test(lint: &LintData<'_>) -> io::Result<()> {
82     fn create_project_layout<P: Into<PathBuf>>(lint_name: &str, location: P, case: &str, hint: &str) -> io::Result<()> {
83         let mut path = location.into().join(case);
84         fs::create_dir(&path)?;
85         write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?;
86
87         path.push("src");
88         fs::create_dir(&path)?;
89         let header = format!("// compile-flags: --crate-name={}", lint_name);
90         write_file(path.join("main.rs"), get_test_file_contents(lint_name, Some(&header)))?;
91
92         Ok(())
93     }
94
95     if lint.category == "cargo" {
96         let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
97         let test_dir = lint.project_root.join(&relative_test_dir);
98         fs::create_dir(&test_dir)?;
99
100         create_project_layout(lint.name, &test_dir, "fail", "Content that triggers the lint goes here")?;
101         create_project_layout(lint.name, &test_dir, "pass", "This file should not trigger the lint")?;
102
103         println!("Generated test directories: `{relative_test_dir}/pass`, `{relative_test_dir}/fail`");
104     } else {
105         let test_path = format!("tests/ui/{}.rs", lint.name);
106         let test_contents = get_test_file_contents(lint.name, None);
107         write_file(lint.project_root.join(&test_path), test_contents)?;
108
109         println!("Generated test file: `{}`", test_path);
110     }
111
112     Ok(())
113 }
114
115 fn add_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
116     let path = "clippy_lints/src/lib.rs";
117     let mut lib_rs = fs::read_to_string(path).context("reading")?;
118
119     let comment_start = lib_rs.find("// add lints here,").expect("Couldn't find comment");
120
121     let new_lint = if enable_msrv {
122         format!(
123             "store.register_{lint_pass}_pass(move || Box::new({module_name}::{camel_name}::new(msrv)));\n    ",
124             lint_pass = lint.pass,
125             module_name = lint.name,
126             camel_name = to_camel_case(lint.name),
127         )
128     } else {
129         format!(
130             "store.register_{lint_pass}_pass(|| Box::new({module_name}::{camel_name}));\n    ",
131             lint_pass = lint.pass,
132             module_name = lint.name,
133             camel_name = to_camel_case(lint.name),
134         )
135     };
136
137     lib_rs.insert_str(comment_start, &new_lint);
138
139     fs::write(path, lib_rs).context("writing")
140 }
141
142 fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
143     fn inner(path: &Path, contents: &[u8]) -> io::Result<()> {
144         OpenOptions::new()
145             .write(true)
146             .create_new(true)
147             .open(path)?
148             .write_all(contents)
149     }
150
151     inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
152 }
153
154 fn to_camel_case(name: &str) -> String {
155     name.split('_')
156         .map(|s| {
157             if s.is_empty() {
158                 String::from("")
159             } else {
160                 [&s[0..1].to_uppercase(), &s[1..]].concat()
161             }
162         })
163         .collect()
164 }
165
166 pub(crate) fn get_stabilization_version() -> String {
167     fn parse_manifest(contents: &str) -> Option<String> {
168         let version = contents
169             .lines()
170             .filter_map(|l| l.split_once('='))
171             .find_map(|(k, v)| (k.trim() == "version").then(|| v.trim()))?;
172         let Some(("0", version)) = version.get(1..version.len() - 1)?.split_once('.') else {
173             return None;
174         };
175         let (minor, patch) = version.split_once('.')?;
176         Some(format!(
177             "{}.{}.0",
178             minor.parse::<u32>().ok()?,
179             patch.parse::<u32>().ok()?
180         ))
181     }
182     let contents = fs::read_to_string("Cargo.toml").expect("Unable to read `Cargo.toml`");
183     parse_manifest(&contents).expect("Unable to find package version in `Cargo.toml`")
184 }
185
186 fn get_test_file_contents(lint_name: &str, header_commands: Option<&str>) -> String {
187     let mut contents = format!(
188         indoc! {"
189             #![warn(clippy::{})]
190
191             fn main() {{
192                 // test code goes here
193             }}
194         "},
195         lint_name
196     );
197
198     if let Some(header) = header_commands {
199         contents = format!("{}\n{}", header, contents);
200     }
201
202     contents
203 }
204
205 fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
206     format!(
207         indoc! {r#"
208             # {}
209
210             [package]
211             name = "{}"
212             version = "0.1.0"
213             publish = false
214
215             [workspace]
216         "#},
217         hint, lint_name
218     )
219 }
220
221 fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
222     let mut result = String::new();
223
224     let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass {
225         "early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
226         "late" => ("LateLintPass", "<'_>", "use rustc_hir::*;", "LateContext"),
227         _ => {
228             unreachable!("`pass_type` should only ever be `early` or `late`!");
229         },
230     };
231
232     let lint_name = lint.name;
233     let category = lint.category;
234     let name_camel = to_camel_case(lint.name);
235     let name_upper = lint_name.to_uppercase();
236
237     result.push_str(&if enable_msrv {
238         format!(
239             indoc! {"
240                 use clippy_utils::msrvs;
241                 {pass_import}
242                 use rustc_lint::{{{context_import}, {pass_type}, LintContext}};
243                 use rustc_semver::RustcVersion;
244                 use rustc_session::{{declare_tool_lint, impl_lint_pass}};
245
246             "},
247             pass_type = pass_type,
248             pass_import = pass_import,
249             context_import = context_import,
250         )
251     } else {
252         format!(
253             indoc! {"
254                 {pass_import}
255                 use rustc_lint::{{{context_import}, {pass_type}}};
256                 use rustc_session::{{declare_lint_pass, declare_tool_lint}};
257
258             "},
259             pass_import = pass_import,
260             pass_type = pass_type,
261             context_import = context_import
262         )
263     });
264
265     let _ = write!(result, "{}", get_lint_declaration(&name_upper, category));
266
267     result.push_str(&if enable_msrv {
268         format!(
269             indoc! {"
270                 pub struct {name_camel} {{
271                     msrv: Option<RustcVersion>,
272                 }}
273
274                 impl {name_camel} {{
275                     #[must_use]
276                     pub fn new(msrv: Option<RustcVersion>) -> Self {{
277                         Self {{ msrv }}
278                     }}
279                 }}
280
281                 impl_lint_pass!({name_camel} => [{name_upper}]);
282
283                 impl {pass_type}{pass_lifetimes} for {name_camel} {{
284                     extract_msrv_attr!({context_import});
285                 }}
286
287                 // TODO: Add MSRV level to `clippy_utils/src/msrvs.rs` if needed.
288                 // TODO: Add MSRV test to `tests/ui/min_rust_version_attr.rs`.
289                 // TODO: Update msrv config comment in `clippy_lints/src/utils/conf.rs`
290             "},
291             pass_type = pass_type,
292             pass_lifetimes = pass_lifetimes,
293             name_upper = name_upper,
294             name_camel = name_camel,
295             context_import = context_import,
296         )
297     } else {
298         format!(
299             indoc! {"
300                 declare_lint_pass!({name_camel} => [{name_upper}]);
301
302                 impl {pass_type}{pass_lifetimes} for {name_camel} {{}}
303             "},
304             pass_type = pass_type,
305             pass_lifetimes = pass_lifetimes,
306             name_upper = name_upper,
307             name_camel = name_camel,
308         )
309     });
310
311     result
312 }
313
314 fn get_lint_declaration(name_upper: &str, category: &str) -> String {
315     format!(
316         indoc! {r#"
317             declare_clippy_lint! {{
318                 /// ### What it does
319                 ///
320                 /// ### Why is this bad?
321                 ///
322                 /// ### Example
323                 /// ```rust
324                 /// // example code where clippy issues a warning
325                 /// ```
326                 /// Use instead:
327                 /// ```rust
328                 /// // example code which does not raise clippy warning
329                 /// ```
330                 #[clippy::version = "{version}"]
331                 pub {name_upper},
332                 {category},
333                 "default lint description"
334             }}
335         "#},
336         version = get_stabilization_version(),
337         name_upper = name_upper,
338         category = category,
339     )
340 }
341
342 fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::Result<()> {
343     match ty {
344         "cargo" => assert_eq!(
345             lint.category, "cargo",
346             "Lints of type `cargo` must have the `cargo` category"
347         ),
348         _ if lint.category == "cargo" => panic!("Lints of category `cargo` must have the `cargo` type"),
349         _ => {},
350     }
351
352     let ty_dir = lint.project_root.join(format!("clippy_lints/src/{}", ty));
353     assert!(
354         ty_dir.exists() && ty_dir.is_dir(),
355         "Directory `{}` does not exist!",
356         ty_dir.display()
357     );
358
359     let lint_file_path = ty_dir.join(format!("{}.rs", lint.name));
360     assert!(
361         !lint_file_path.exists(),
362         "File `{}` already exists",
363         lint_file_path.display()
364     );
365
366     let mod_file_path = ty_dir.join("mod.rs");
367     let context_import = setup_mod_file(&mod_file_path, lint)?;
368
369     let name_upper = lint.name.to_uppercase();
370     let mut lint_file_contents = String::new();
371
372     if enable_msrv {
373         let _ = writedoc!(
374             lint_file_contents,
375             r#"
376                 use clippy_utils::{{meets_msrv, msrvs}};
377                 use rustc_lint::{{{context_import}, LintContext}};
378                 use rustc_semver::RustcVersion;
379
380                 use super::{name_upper};
381
382                 // TODO: Adjust the parameters as necessary
383                 pub(super) fn check(cx: &{context_import}, msrv: Option<RustcVersion>) {{
384                     if !meets_msrv(msrv, todo!("Add a new entry in `clippy_utils/src/msrvs`")) {{
385                         return;
386                     }}
387                     todo!();
388                 }}
389            "#,
390             context_import = context_import,
391             name_upper = name_upper,
392         );
393     } else {
394         let _ = writedoc!(
395             lint_file_contents,
396             r#"
397                 use rustc_lint::{{{context_import}, LintContext}};
398
399                 use super::{name_upper};
400
401                 // TODO: Adjust the parameters as necessary
402                 pub(super) fn check(cx: &{context_import}) {{
403                     todo!();
404                 }}
405            "#,
406             context_import = context_import,
407             name_upper = name_upper,
408         );
409     }
410
411     write_file(lint_file_path.as_path(), lint_file_contents)?;
412     println!("Generated lint file: `clippy_lints/src/{}/{}.rs`", ty, lint.name);
413     println!(
414         "Be sure to add a call to `{}::check` in `clippy_lints/src/{}/mod.rs`!",
415         lint.name, ty
416     );
417
418     Ok(())
419 }
420
421 #[allow(clippy::too_many_lines)]
422 fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> {
423     use super::update_lints::{match_tokens, LintDeclSearchResult};
424     use rustc_lexer::TokenKind;
425
426     let lint_name_upper = lint.name.to_uppercase();
427
428     let mut file_contents = fs::read_to_string(path)?;
429     assert!(
430         !file_contents.contains(&lint_name_upper),
431         "Lint `{}` already defined in `{}`",
432         lint.name,
433         path.display()
434     );
435
436     let mut offset = 0usize;
437     let mut last_decl_curly_offset = None;
438     let mut lint_context = None;
439
440     let mut iter = rustc_lexer::tokenize(&file_contents).map(|t| {
441         let range = offset..offset + t.len;
442         offset = range.end;
443
444         LintDeclSearchResult {
445             token_kind: t.kind,
446             content: &file_contents[range.clone()],
447             range,
448         }
449     });
450
451     // Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl
452     while let Some(LintDeclSearchResult { content, .. }) = iter.find(|result| result.token_kind == TokenKind::Ident) {
453         let mut iter = iter
454             .by_ref()
455             .filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
456
457         match content {
458             "declare_clippy_lint" => {
459                 // matches `!{`
460                 match_tokens!(iter, Bang OpenBrace);
461                 if let Some(LintDeclSearchResult { range, .. }) =
462                     iter.find(|result| result.token_kind == TokenKind::CloseBrace)
463                 {
464                     last_decl_curly_offset = Some(range.end);
465                 }
466             },
467             "impl" => {
468                 let mut token = iter.next();
469                 match token {
470                     // matches <'foo>
471                     Some(LintDeclSearchResult {
472                         token_kind: TokenKind::Lt,
473                         ..
474                     }) => {
475                         match_tokens!(iter, Lifetime { .. } Gt);
476                         token = iter.next();
477                     },
478                     None => break,
479                     _ => {},
480                 }
481
482                 if let Some(LintDeclSearchResult {
483                     token_kind: TokenKind::Ident,
484                     content,
485                     ..
486                 }) = token
487                 {
488                     // Get the appropriate lint context struct
489                     lint_context = match content {
490                         "LateLintPass" => Some("LateContext"),
491                         "EarlyLintPass" => Some("EarlyContext"),
492                         _ => continue,
493                     };
494                 }
495             },
496             _ => {},
497         }
498     }
499
500     drop(iter);
501
502     let last_decl_curly_offset =
503         last_decl_curly_offset.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display()));
504     let lint_context =
505         lint_context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display()));
506
507     // Add the lint declaration to `mod.rs`
508     file_contents.replace_range(
509         // Remove the trailing newline, which should always be present
510         last_decl_curly_offset..=last_decl_curly_offset,
511         &format!("\n\n{}", get_lint_declaration(&lint_name_upper, lint.category)),
512     );
513
514     // Add the lint to `impl_lint_pass`/`declare_lint_pass`
515     let impl_lint_pass_start = file_contents.find("impl_lint_pass!").unwrap_or_else(|| {
516         file_contents
517             .find("declare_lint_pass!")
518             .unwrap_or_else(|| panic!("failed to find `impl_lint_pass`/`declare_lint_pass`"))
519     });
520
521     let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| {
522         panic!("malformed `impl_lint_pass`/`declare_lint_pass`");
523     });
524
525     arr_start += impl_lint_pass_start;
526
527     let mut arr_end = file_contents[arr_start..]
528         .find(']')
529         .expect("failed to find `impl_lint_pass` terminator");
530
531     arr_end += arr_start;
532
533     let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();
534     arr_content.retain(|c| !c.is_whitespace());
535
536     let mut new_arr_content = String::new();
537     for ident in arr_content
538         .split(',')
539         .chain(std::iter::once(&*lint_name_upper))
540         .filter(|s| !s.is_empty())
541     {
542         let _ = write!(new_arr_content, "\n    {},", ident);
543     }
544     new_arr_content.push('\n');
545
546     file_contents.replace_range(arr_start + 1..arr_end, &new_arr_content);
547
548     // Just add the mod declaration at the top, it'll be fixed by rustfmt
549     file_contents.insert_str(0, &format!("mod {};\n", &lint.name));
550
551     let mut file = OpenOptions::new()
552         .write(true)
553         .truncate(true)
554         .open(path)
555         .context(format!("trying to open: `{}`", path.display()))?;
556     file.write_all(file_contents.as_bytes())
557         .context(format!("writing to file: `{}`", path.display()))?;
558
559     Ok(lint_context)
560 }
561
562 #[test]
563 fn test_camel_case() {
564     let s = "a_lint";
565     let s2 = to_camel_case(s);
566     assert_eq!(s2, "ALint");
567
568     let name = "a_really_long_new_lint";
569     let name2 = to_camel_case(name);
570     assert_eq!(name2, "AReallyLongNewLint");
571
572     let name3 = "lint__name";
573     let name4 = to_camel_case(name3);
574     assert_eq!(name4, "LintName");
575 }