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};
14 project_root: PathBuf,
18 fn context<C: AsRef<str>>(self, text: C) -> Self;
21 impl<T> Context for io::Result<T> {
22 fn context<C: AsRef<str>>(self, text: C) -> Self {
26 let message = format!("{}: {}", text.as_ref(), e);
27 Err(io::Error::new(ErrorKind::Other, message))
33 /// Creates the files required to implement and test a new lint and runs `update_lints`.
37 /// This function errors out if the files couldn't be created or written to.
39 pass: Option<&String>,
40 lint_name: Option<&String>,
41 category: Option<&str>,
45 if category == Some("cargo") && ty.is_none() {
46 // `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo`
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"),
55 project_root: clippy_project_root(),
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")?;
61 if lint.ty.is_none() {
62 add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")?;
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)
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);
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))?;
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)))?;
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)?;
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")?;
103 println!("Generated test directories: `{relative_test_dir}/pass`, `{relative_test_dir}/fail`");
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)?;
109 println!("Generated test file: `{}`", test_path);
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")?;
119 let comment_start = lib_rs.find("// add lints here,").expect("Couldn't find comment");
121 let new_lint = if enable_msrv {
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),
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),
137 lib_rs.insert_str(comment_start, &new_lint);
139 fs::write(path, lib_rs).context("writing")
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<()> {
151 inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
154 fn to_camel_case(name: &str) -> String {
160 [&s[0..1].to_uppercase(), &s[1..]].concat()
166 pub(crate) fn get_stabilization_version() -> String {
167 fn parse_manifest(contents: &str) -> Option<String> {
168 let version = contents
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 {
175 let (minor, patch) = version.split_once('.')?;
178 minor.parse::<u32>().ok()?,
179 patch.parse::<u32>().ok()?
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`")
186 fn get_test_file_contents(lint_name: &str, header_commands: Option<&str>) -> String {
187 let mut contents = format!(
192 // test code goes here
198 if let Some(header) = header_commands {
199 contents = format!("{}\n{}", header, contents);
205 fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
221 fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
222 let mut result = String::new();
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"),
228 unreachable!("`pass_type` should only ever be `early` or `late`!");
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();
237 result.push_str(&if enable_msrv {
240 use clippy_utils::msrvs;
242 use rustc_lint::{{{context_import}, {pass_type}, LintContext}};
243 use rustc_semver::RustcVersion;
244 use rustc_session::{{declare_tool_lint, impl_lint_pass}};
247 pass_type = pass_type,
248 pass_import = pass_import,
249 context_import = context_import,
255 use rustc_lint::{{{context_import}, {pass_type}}};
256 use rustc_session::{{declare_lint_pass, declare_tool_lint}};
259 pass_import = pass_import,
260 pass_type = pass_type,
261 context_import = context_import
265 let _ = write!(result, "{}", get_lint_declaration(&name_upper, category));
267 result.push_str(&if enable_msrv {
270 pub struct {name_camel} {{
271 msrv: Option<RustcVersion>,
276 pub fn new(msrv: Option<RustcVersion>) -> Self {{
281 impl_lint_pass!({name_camel} => [{name_upper}]);
283 impl {pass_type}{pass_lifetimes} for {name_camel} {{
284 extract_msrv_attr!({context_import});
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`
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,
300 declare_lint_pass!({name_camel} => [{name_upper}]);
302 impl {pass_type}{pass_lifetimes} for {name_camel} {{}}
304 pass_type = pass_type,
305 pass_lifetimes = pass_lifetimes,
306 name_upper = name_upper,
307 name_camel = name_camel,
314 fn get_lint_declaration(name_upper: &str, category: &str) -> String {
317 declare_clippy_lint! {{
320 /// ### Why is this bad?
324 /// // example code where clippy issues a warning
328 /// // example code which does not raise clippy warning
330 #[clippy::version = "{version}"]
333 "default lint description"
336 version = get_stabilization_version(),
337 name_upper = name_upper,
342 fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::Result<()> {
344 "cargo" => assert_eq!(
345 lint.category, "cargo",
346 "Lints of type `cargo` must have the `cargo` category"
348 _ if lint.category == "cargo" => panic!("Lints of category `cargo` must have the `cargo` type"),
352 let ty_dir = lint.project_root.join(format!("clippy_lints/src/{}", ty));
354 ty_dir.exists() && ty_dir.is_dir(),
355 "Directory `{}` does not exist!",
359 let lint_file_path = ty_dir.join(format!("{}.rs", lint.name));
361 !lint_file_path.exists(),
362 "File `{}` already exists",
363 lint_file_path.display()
366 let mod_file_path = ty_dir.join("mod.rs");
367 let context_import = setup_mod_file(&mod_file_path, lint)?;
369 let name_upper = lint.name.to_uppercase();
370 let mut lint_file_contents = String::new();
376 use clippy_utils::{{meets_msrv, msrvs}};
377 use rustc_lint::{{{context_import}, LintContext}};
378 use rustc_semver::RustcVersion;
380 use super::{name_upper};
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`")) {{
390 context_import = context_import,
391 name_upper = name_upper,
397 use rustc_lint::{{{context_import}, LintContext}};
399 use super::{name_upper};
401 // TODO: Adjust the parameters as necessary
402 pub(super) fn check(cx: &{context_import}) {{
406 context_import = context_import,
407 name_upper = name_upper,
411 write_file(lint_file_path.as_path(), lint_file_contents)?;
412 println!("Generated lint file: `clippy_lints/src/{}/{}.rs`", ty, lint.name);
414 "Be sure to add a call to `{}::check` in `clippy_lints/src/{}/mod.rs`!",
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;
426 let lint_name_upper = lint.name.to_uppercase();
428 let mut file_contents = fs::read_to_string(path)?;
430 !file_contents.contains(&lint_name_upper),
431 "Lint `{}` already defined in `{}`",
436 let mut offset = 0usize;
437 let mut last_decl_curly_offset = None;
438 let mut lint_context = None;
440 let mut iter = rustc_lexer::tokenize(&file_contents).map(|t| {
441 let range = offset..offset + t.len as usize;
444 LintDeclSearchResult {
446 content: &file_contents[range.clone()],
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) {
455 .filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
458 "declare_clippy_lint" => {
460 match_tokens!(iter, Bang OpenBrace);
461 if let Some(LintDeclSearchResult { range, .. }) =
462 iter.find(|result| result.token_kind == TokenKind::CloseBrace)
464 last_decl_curly_offset = Some(range.end);
468 let mut token = iter.next();
471 Some(LintDeclSearchResult {
472 token_kind: TokenKind::Lt,
475 match_tokens!(iter, Lifetime { .. } Gt);
482 if let Some(LintDeclSearchResult {
483 token_kind: TokenKind::Ident,
488 // Get the appropriate lint context struct
489 lint_context = match content {
490 "LateLintPass" => Some("LateContext"),
491 "EarlyLintPass" => Some("EarlyContext"),
502 let last_decl_curly_offset =
503 last_decl_curly_offset.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display()));
505 lint_context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display()));
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)),
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(|| {
517 .find("declare_lint_pass!")
518 .unwrap_or_else(|| panic!("failed to find `impl_lint_pass`/`declare_lint_pass`"))
521 let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| {
522 panic!("malformed `impl_lint_pass`/`declare_lint_pass`");
525 arr_start += impl_lint_pass_start;
527 let mut arr_end = file_contents[arr_start..]
529 .expect("failed to find `impl_lint_pass` terminator");
531 arr_end += arr_start;
533 let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();
534 arr_content.retain(|c| !c.is_whitespace());
536 let mut new_arr_content = String::new();
537 for ident in arr_content
539 .chain(std::iter::once(&*lint_name_upper))
540 .filter(|s| !s.is_empty())
542 let _ = write!(new_arr_content, "\n {},", ident);
544 new_arr_content.push('\n');
546 file_contents.replace_range(arr_start + 1..arr_end, &new_arr_content);
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));
551 let mut file = OpenOptions::new()
555 .context(format!("trying to open: `{}`", path.display()))?;
556 file.write_all(file_contents.as_bytes())
557 .context(format!("writing to file: `{}`", path.display()))?;
563 fn test_camel_case() {
565 let s2 = to_camel_case(s);
566 assert_eq!(s2, "ALint");
568 let name = "a_really_long_new_lint";
569 let name2 = to_camel_case(name);
570 assert_eq!(name2, "AReallyLongNewLint");
572 let name3 = "lint__name";
573 let name4 = to_camel_case(name3);
574 assert_eq!(name4, "LintName");