Skip to content

Commit 51cd5a8

Browse files
committed
Add --type flag to dev new_lint
1 parent d72e5f2 commit 51cd5a8

File tree

6 files changed

+295
-60
lines changed

6 files changed

+295
-60
lines changed

clippy_dev/src/main.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fn main() {
3737
matches.get_one::<String>("pass"),
3838
matches.get_one::<String>("name"),
3939
matches.get_one::<String>("category"),
40+
matches.get_one::<String>("type"),
4041
matches.contains_id("msrv"),
4142
) {
4243
Ok(_) => update_lints::update(update_lints::UpdateMode::Change),
@@ -157,7 +158,8 @@ fn get_clap_config() -> ArgMatches {
157158
.help("Specify whether the lint runs during the early or late pass")
158159
.takes_value(true)
159160
.value_parser([PossibleValue::new("early"), PossibleValue::new("late")])
160-
.required(true),
161+
.conflicts_with("type")
162+
.required_unless_present("type"),
161163
Arg::new("name")
162164
.short('n')
163165
.long("name")
@@ -183,6 +185,11 @@ fn get_clap_config() -> ArgMatches {
183185
PossibleValue::new("internal_warn"),
184186
])
185187
.takes_value(true),
188+
Arg::new("type")
189+
.long("type")
190+
.help("What directory the lint belongs in")
191+
.takes_value(true)
192+
.required(false),
186193
Arg::new("msrv").long("msrv").help("Add MSRV config code to the lint"),
187194
]),
188195
Command::new("setup")

clippy_dev/src/new_lint.rs

+260-34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::clippy_project_root;
2-
use indoc::indoc;
2+
use indoc::{indoc, writedoc};
33
use std::fmt::Write as _;
44
use std::fs::{self, OpenOptions};
55
use std::io::prelude::*;
@@ -10,6 +10,7 @@ struct LintData<'a> {
1010
pass: &'a str,
1111
name: &'a str,
1212
category: &'a str,
13+
ty: Option<&'a str>,
1314
project_root: PathBuf,
1415
}
1516

@@ -38,25 +39,35 @@ pub fn create(
3839
pass: Option<&String>,
3940
lint_name: Option<&String>,
4041
category: Option<&String>,
42+
ty: Option<&String>,
4143
msrv: bool,
4244
) -> io::Result<()> {
4345
let lint = LintData {
44-
pass: pass.expect("`pass` argument is validated by clap"),
46+
pass: pass.map_or("", String::as_str),
4547
name: lint_name.expect("`name` argument is validated by clap"),
4648
category: category.expect("`category` argument is validated by clap"),
49+
ty: ty.map(String::as_str),
4750
project_root: clippy_project_root(),
4851
};
4952

5053
create_lint(&lint, msrv).context("Unable to create lint implementation")?;
5154
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")
55+
56+
if lint.ty.is_none() {
57+
add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")?;
58+
}
59+
60+
Ok(())
5361
}
5462

5563
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())
64+
if let Some(ty) = lint.ty {
65+
generate_from_ty(lint, enable_msrv, ty)
66+
} else {
67+
let lint_contents = get_lint_file_contents(lint, enable_msrv);
68+
let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
69+
write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())
70+
}
6071
}
6172

6273
fn create_test(lint: &LintData<'_>) -> io::Result<()> {
@@ -204,7 +215,6 @@ fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
204215
},
205216
};
206217

207-
let version = get_stabilization_version();
208218
let lint_name = lint.name;
209219
let category = lint.category;
210220
let name_camel = to_camel_case(lint.name);
@@ -238,32 +248,7 @@ fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
238248
)
239249
});
240250

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-
);
251+
let _ = write!(result, "{}", get_lint_declaration(&name_upper, category));
267252

268253
result.push_str(&if enable_msrv {
269254
format!(
@@ -312,6 +297,247 @@ fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
312297
result
313298
}
314299

300+
fn get_lint_declaration(name_upper: &str, category: &str) -> String {
301+
format!(
302+
indoc! {r#"
303+
declare_clippy_lint! {{
304+
/// ### What it does
305+
///
306+
/// ### Why is this bad?
307+
///
308+
/// ### Example
309+
/// ```rust
310+
/// // example code where clippy issues a warning
311+
/// ```
312+
/// Use instead:
313+
/// ```rust
314+
/// // example code which does not raise clippy warning
315+
/// ```
316+
#[clippy::version = "{version}"]
317+
pub {name_upper},
318+
{category},
319+
"default lint description"
320+
}}
321+
"#},
322+
version = get_stabilization_version(),
323+
name_upper = name_upper,
324+
category = category,
325+
)
326+
}
327+
328+
fn generate_from_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::Result<()> {
329+
if ty == "cargo" {
330+
assert_eq!(
331+
lint.category, "cargo",
332+
"Lints of type `cargo` must have the `cargo` category"
333+
);
334+
}
335+
336+
let ty_dir = lint.project_root.join(format!("clippy_lints/src/{}", ty));
337+
assert!(
338+
ty_dir.exists() && ty_dir.is_dir(),
339+
"Directory `{}` does not exist!",
340+
ty_dir.display()
341+
);
342+
343+
let lint_file_path = ty_dir.join(format!("{}.rs", lint.name));
344+
assert!(
345+
!lint_file_path.exists(),
346+
"File `{}` already exists",
347+
lint_file_path.display()
348+
);
349+
350+
let mod_file_path = ty_dir.join("mod.rs");
351+
let context_import = setup_mod_file(&mod_file_path, lint)?;
352+
353+
let name_upper = lint.name.to_uppercase();
354+
let mut lint_file_contents = String::new();
355+
356+
if enable_msrv {
357+
let _ = writedoc!(
358+
lint_file_contents,
359+
r#"
360+
use clippy_utils::{{meets_msrv, msrvs}};
361+
use rustc_lint::{{{context_import}, LintContext}};
362+
use rustc_semver::RustcVersion;
363+
364+
use super::{name_upper};
365+
366+
// TODO: Adjust the parameters as necessary
367+
pub(super) fn check(cx: &{context_import}, msrv: Option<RustcVersion>) {{
368+
if !meets_msrv(msrv, todo!("Add a new entry in `clippy_utils/src/msrvs`")) {{
369+
return;
370+
}}
371+
todo!();
372+
}}
373+
"#,
374+
context_import = context_import,
375+
name_upper = name_upper,
376+
);
377+
} else {
378+
let _ = writedoc!(
379+
lint_file_contents,
380+
r#"
381+
use rustc_lint::{{{context_import}, LintContext}};
382+
383+
use super::{name_upper};
384+
385+
// TODO: Adjust the parameters as necessary
386+
pub(super) fn check(cx: &{context_import}) {{
387+
todo!();
388+
}}
389+
"#,
390+
context_import = context_import,
391+
name_upper = name_upper,
392+
);
393+
}
394+
395+
write_file(lint_file_path, lint_file_contents)?;
396+
397+
Ok(())
398+
}
399+
400+
#[allow(clippy::too_many_lines)]
401+
fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> {
402+
use super::update_lints::{match_tokens, LintDeclSearchResult};
403+
use rustc_lexer::TokenKind;
404+
405+
let lint_name_upper = lint.name.to_uppercase();
406+
407+
let mut file_contents = fs::read_to_string(path)?;
408+
assert!(
409+
!file_contents.contains(&lint_name_upper),
410+
"Lint `{}` already defined in `{}`",
411+
lint.name,
412+
path.display()
413+
);
414+
415+
let mut offset = 0usize;
416+
let mut last_decl_curly_offset = None;
417+
let mut lint_context = None;
418+
419+
let mut iter = rustc_lexer::tokenize(&file_contents).map(|t| {
420+
let range = offset..offset + t.len;
421+
offset = range.end;
422+
423+
LintDeclSearchResult {
424+
token_kind: t.kind,
425+
content: &file_contents[range.clone()],
426+
range,
427+
}
428+
});
429+
430+
// Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl
431+
while let Some(LintDeclSearchResult { content, .. }) = iter.find(|result| result.token_kind == TokenKind::Ident) {
432+
let mut iter = iter
433+
.by_ref()
434+
.filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
435+
436+
match content {
437+
"declare_clippy_lint" => {
438+
// matches `!{`
439+
match_tokens!(iter, Bang OpenBrace);
440+
if let Some(LintDeclSearchResult { range, .. }) =
441+
iter.find(|result| result.token_kind == TokenKind::CloseBrace)
442+
{
443+
last_decl_curly_offset = Some(range.end);
444+
}
445+
},
446+
"impl" => {
447+
let mut token = iter.next();
448+
match token {
449+
// matches <'foo>
450+
Some(LintDeclSearchResult {
451+
token_kind: TokenKind::Lt,
452+
..
453+
}) => {
454+
match_tokens!(iter, Lifetime { .. } Gt);
455+
token = iter.next();
456+
},
457+
None => break,
458+
_ => {},
459+
}
460+
461+
if let Some(LintDeclSearchResult {
462+
token_kind: TokenKind::Ident,
463+
content,
464+
..
465+
}) = token
466+
{
467+
// Get the appropriate lint context struct
468+
lint_context = match content {
469+
"LateLintPass" => Some("LateContext"),
470+
"EarlyLintPass" => Some("EarlyContext"),
471+
_ => continue,
472+
};
473+
}
474+
},
475+
_ => {},
476+
}
477+
}
478+
479+
drop(iter);
480+
481+
let last_decl_curly_offset =
482+
last_decl_curly_offset.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display()));
483+
let lint_context =
484+
lint_context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display()));
485+
486+
// Add the lint declaration to `mod.rs`
487+
file_contents.replace_range(
488+
// Remove the trailing newline, which should always be present
489+
last_decl_curly_offset..=last_decl_curly_offset,
490+
&format!("\n\n{}", get_lint_declaration(&lint_name_upper, lint.category)),
491+
);
492+
493+
// Add the lint to `impl_lint_pass`/`declare_lint_pass`
494+
let impl_lint_pass_start = file_contents.find("impl_lint_pass!").unwrap_or_else(|| {
495+
file_contents
496+
.find("declare_lint_pass!")
497+
.unwrap_or_else(|| panic!("failed to find `impl_lint_pass`/`declare_lint_pass`"))
498+
});
499+
500+
let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| {
501+
panic!("malformed `impl_lint_pass`/`declare_lint_pass`");
502+
});
503+
504+
arr_start += impl_lint_pass_start;
505+
506+
let mut arr_end = file_contents[arr_start..]
507+
.find(']')
508+
.expect("failed to find `impl_lint_pass` terminator");
509+
510+
arr_end += arr_start;
511+
512+
let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();
513+
arr_content.retain(|c| !c.is_whitespace());
514+
515+
let mut new_arr_content = String::new();
516+
for ident in arr_content
517+
.split(',')
518+
.chain(std::iter::once(&*lint_name_upper))
519+
.filter(|s| !s.is_empty())
520+
{
521+
let _ = write!(new_arr_content, "\n {},", ident);
522+
}
523+
new_arr_content.push('\n');
524+
525+
file_contents.replace_range(arr_start + 1..arr_end, &new_arr_content);
526+
527+
// Just add the mod declaration at the top, it'll be fixed by rustfmt
528+
file_contents.insert_str(0, &format!("mod {};\n", &lint.name));
529+
530+
let mut file = OpenOptions::new()
531+
.write(true)
532+
.truncate(true)
533+
.open(path)
534+
.context(format!("trying to open: `{}`", path.display()))?;
535+
file.write_all(file_contents.as_bytes())
536+
.context(format!("writing to file: `{}`", path.display()))?;
537+
538+
Ok(lint_context)
539+
}
540+
315541
#[test]
316542
fn test_camel_case() {
317543
let s = "a_lint";

clippy_dev/src/update_lints.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -824,10 +824,12 @@ macro_rules! match_tokens {
824824
}
825825
}
826826

827-
struct LintDeclSearchResult<'a> {
828-
token_kind: TokenKind,
829-
content: &'a str,
830-
range: Range<usize>,
827+
pub(crate) use match_tokens;
828+
829+
pub(crate) struct LintDeclSearchResult<'a> {
830+
pub token_kind: TokenKind,
831+
pub content: &'a str,
832+
pub range: Range<usize>,
831833
}
832834

833835
/// Parse a source file looking for `declare_clippy_lint` macro invocations.

0 commit comments

Comments
 (0)