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