Skip to content

Commit e772081

Browse files
committed
Add --type flag to dev new_lint
1 parent 0f5a38f commit e772081

File tree

6 files changed

+289
-58
lines changed

6 files changed

+289
-58
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

+256-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,243 @@ 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+
if lint_file_path.exists() {
345+
panic!("File `{}` already exists", lint_file_path.display());
346+
}
347+
348+
let mod_file_path = ty_dir.join("mod.rs");
349+
let context_import = setup_mod_file(&mod_file_path, lint)?;
350+
351+
let name_upper = lint.name.to_uppercase();
352+
353+
let lint_file_path = ty_dir.join(format!("{}.rs", lint.name));
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+
fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> {
401+
use super::update_lints::{match_tokens, LintDeclSearchResult};
402+
use rustc_lexer::TokenKind;
403+
404+
let lint_name_upper = lint.name.to_uppercase();
405+
406+
let mut file_contents = fs::read_to_string(path)?;
407+
if file_contents.contains(&lint_name_upper) {
408+
panic!("Lint `{}` already defined in `{}`", lint.name, path.display());
409+
}
410+
411+
let mut offset = 0usize;
412+
let mut last_decl_curly_offset = None;
413+
let mut lint_context = None;
414+
415+
let mut iter = rustc_lexer::tokenize(&file_contents).map(|t| {
416+
let range = offset..offset + t.len;
417+
offset = range.end;
418+
419+
LintDeclSearchResult {
420+
token_kind: t.kind,
421+
content: &file_contents[range.clone()],
422+
range,
423+
}
424+
});
425+
426+
// Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl
427+
while let Some(LintDeclSearchResult { content, .. }) = iter.find(|result| result.token_kind == TokenKind::Ident) {
428+
let mut iter = iter
429+
.by_ref()
430+
.filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
431+
432+
match content {
433+
"declare_clippy_lint" => {
434+
// matches `!{`
435+
match_tokens!(iter, Bang OpenBrace);
436+
if let Some(LintDeclSearchResult { range, .. }) =
437+
iter.find(|result| result.token_kind == TokenKind::CloseBrace)
438+
{
439+
last_decl_curly_offset = Some(range.end);
440+
}
441+
},
442+
"impl" => {
443+
let mut token = iter.next();
444+
match token {
445+
// matches <'foo>
446+
Some(LintDeclSearchResult {
447+
token_kind: TokenKind::Lt,
448+
..
449+
}) => {
450+
match_tokens!(iter, Lifetime { .. } Gt);
451+
token = iter.next();
452+
},
453+
None => break,
454+
_ => {},
455+
}
456+
457+
if let Some(LintDeclSearchResult {
458+
token_kind: TokenKind::Ident,
459+
content,
460+
..
461+
}) = token
462+
{
463+
// Get the appropriate lint context struct
464+
lint_context = match content {
465+
"LateLintPass" => Some("LateContext"),
466+
"EarlyLintPass" => Some("EarlyContext"),
467+
_ => continue,
468+
};
469+
}
470+
},
471+
_ => {},
472+
}
473+
}
474+
475+
drop(iter);
476+
477+
let last_decl_curly_offset =
478+
last_decl_curly_offset.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display()));
479+
let lint_context =
480+
lint_context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display()));
481+
482+
// Add the lint declaration to `mod.rs`
483+
file_contents.replace_range(
484+
// Remove the trailing newline, which should always be present
485+
last_decl_curly_offset..=last_decl_curly_offset,
486+
&format!("\n\n{}", get_lint_declaration(&lint_name_upper, lint.category)),
487+
);
488+
489+
// Add the lint to `impl_lint_pass`/`declare_lint_pass`
490+
let impl_lint_pass_start = file_contents.find("impl_lint_pass!").unwrap_or_else(|| {
491+
file_contents
492+
.find("declare_lint_pass!")
493+
.unwrap_or_else(|| panic!("failed to find `impl_lint_pass`/`declare_lint_pass`"))
494+
});
495+
496+
let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| {
497+
panic!("malformed `impl_lint_pass`/`declare_lint_pass`");
498+
});
499+
500+
arr_start += impl_lint_pass_start;
501+
502+
let mut arr_end = file_contents[arr_start..]
503+
.find(']')
504+
.expect("failed to find `impl_lint_pass` terminator");
505+
506+
arr_end += arr_start;
507+
508+
let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();
509+
arr_content.retain(|c| !c.is_whitespace());
510+
511+
let mut new_arr_content = String::new();
512+
for ident in arr_content
513+
.split(',')
514+
.chain(std::iter::once(&*lint_name_upper))
515+
.filter(|s| !s.is_empty())
516+
{
517+
let _ = write!(new_arr_content, "\n {},", ident);
518+
}
519+
new_arr_content.push('\n');
520+
521+
file_contents.replace_range(arr_start + 1..arr_end, &new_arr_content);
522+
523+
// Just add the mod declaration at the top, it'll be fixed by rustfmt
524+
file_contents.insert_str(0, &format!("mod {};\n", &lint.name));
525+
526+
let mut file = OpenOptions::new()
527+
.write(true)
528+
.truncate(true)
529+
.open(path)
530+
.context(format!("trying to open: `{}`", path.display()))?;
531+
file.write_all(file_contents.as_bytes())
532+
.context(format!("writing to file: `{}`", path.display()))?;
533+
534+
Ok(lint_context)
535+
}
536+
315537
#[test]
316538
fn test_camel_case() {
317539
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)