Skip to content

Commit ea82cbe

Browse files
committed
Generate new lints easily
- Add option in clippy_dev to automatically generate boilerplate code for adding new lints
1 parent be09bb4 commit ea82cbe

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

clippy_dev/src/main.rs

+52
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use clap::{App, Arg, SubCommand};
44
use clippy_dev::*;
55

66
mod fmt;
7+
mod new_lint;
78
mod stderr_length_check;
89

910
#[derive(PartialEq)]
@@ -51,6 +52,47 @@ fn main() {
5152
.help("Checks that util/dev update_lints has been run. Used on CI."),
5253
),
5354
)
55+
.subcommand(
56+
SubCommand::with_name("new_lint")
57+
.about("Create new lint and run util/dev update_lints")
58+
.arg(
59+
Arg::with_name("pass")
60+
.short("p")
61+
.long("pass")
62+
.help("Specify whether the lint runs during the early or late pass")
63+
.takes_value(true)
64+
.possible_values(&["early", "late"])
65+
.required(true),
66+
)
67+
.arg(
68+
Arg::with_name("name")
69+
.short("n")
70+
.long("name")
71+
.help("Name of the new lint in snake case, ex: fn_too_long")
72+
.takes_value(true)
73+
.required(true),
74+
)
75+
.arg(
76+
Arg::with_name("category")
77+
.short("c")
78+
.long("category")
79+
.help("What category the lint belongs to")
80+
.default_value("nursery")
81+
.possible_values(&[
82+
"style",
83+
"correctness",
84+
"complexity",
85+
"perf",
86+
"pedantic",
87+
"restriction",
88+
"cargo",
89+
"nursery",
90+
"internal",
91+
"internal_warn",
92+
])
93+
.takes_value(true),
94+
),
95+
)
5496
.arg(
5597
Arg::with_name("limit-stderr-length")
5698
.long("limit-stderr-length")
@@ -75,6 +117,16 @@ fn main() {
75117
update_lints(&UpdateMode::Change);
76118
}
77119
},
120+
("new_lint", Some(matches)) => {
121+
match new_lint::create(
122+
matches.value_of("pass"),
123+
matches.value_of("name"),
124+
matches.value_of("category"),
125+
) {
126+
Ok(_) => update_lints(&UpdateMode::Change),
127+
Err(e) => eprintln!("Unable to create lint: {}", e),
128+
}
129+
},
78130
_ => {},
79131
}
80132
}

clippy_dev/src/new_lint.rs

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
use std::fs::{File, OpenOptions};
2+
use std::io;
3+
use std::io::prelude::*;
4+
use std::io::ErrorKind;
5+
use std::path::{Path, PathBuf};
6+
7+
pub fn create(pass: Option<&str>, lint_name: Option<&str>, category: Option<&str>) -> Result<(), io::Error> {
8+
let pass = pass.expect("`pass` argument is validated by clap");
9+
let lint_name = lint_name.expect("`name` argument is validated by clap");
10+
let category = category.expect("`category` argument is validated by clap");
11+
12+
match open_files(lint_name) {
13+
Ok((mut test_file, mut lint_file)) => {
14+
let (pass_type, pass_import, context_import) = match pass {
15+
"early" => ("EarlyLintPass", "use syntax::ast::*;", "EarlyContext"),
16+
"late" => ("LateLintPass", "use rustc_hir::*;", "LateContext"),
17+
_ => {
18+
unreachable!("`pass_type` should only ever be `early` or `late`!");
19+
},
20+
};
21+
22+
let camel_case_name = to_camel_case(lint_name);
23+
24+
if let Err(e) = test_file.write_all(get_test_file_contents(lint_name).as_bytes()) {
25+
return Err(io::Error::new(
26+
ErrorKind::Other,
27+
format!("Could not write to test file: {}", e),
28+
));
29+
};
30+
31+
if let Err(e) = lint_file.write_all(
32+
get_lint_file_contents(
33+
pass_type,
34+
lint_name,
35+
&camel_case_name,
36+
category,
37+
pass_import,
38+
context_import,
39+
)
40+
.as_bytes(),
41+
) {
42+
return Err(io::Error::new(
43+
ErrorKind::Other,
44+
format!("Could not write to lint file: {}", e),
45+
));
46+
}
47+
Ok(())
48+
},
49+
Err(e) => Err(io::Error::new(
50+
ErrorKind::Other,
51+
format!("Unable to create lint: {}", e),
52+
)),
53+
}
54+
}
55+
56+
fn open_files(lint_name: &str) -> Result<(File, File), io::Error> {
57+
let project_root = project_root()?;
58+
59+
let test_file_path = project_root.join("tests").join("ui").join(format!("{}.rs", lint_name));
60+
let lint_file_path = project_root
61+
.join("clippy_lints")
62+
.join("src")
63+
.join(format!("{}.rs", lint_name));
64+
65+
if Path::new(&test_file_path).exists() {
66+
return Err(io::Error::new(
67+
ErrorKind::AlreadyExists,
68+
format!("test file {:?} already exists", test_file_path),
69+
));
70+
}
71+
if Path::new(&lint_file_path).exists() {
72+
return Err(io::Error::new(
73+
ErrorKind::AlreadyExists,
74+
format!("lint file {:?} already exists", lint_file_path),
75+
));
76+
}
77+
78+
let test_file = OpenOptions::new().write(true).create_new(true).open(test_file_path)?;
79+
let lint_file = OpenOptions::new().write(true).create_new(true).open(lint_file_path)?;
80+
81+
Ok((test_file, lint_file))
82+
}
83+
84+
fn project_root() -> Result<PathBuf, io::Error> {
85+
let current_dir = std::env::current_dir()?;
86+
for path in current_dir.ancestors() {
87+
let result = std::fs::read_to_string(path.join("Cargo.toml"));
88+
if let Err(err) = &result {
89+
if err.kind() == io::ErrorKind::NotFound {
90+
continue;
91+
}
92+
}
93+
94+
let content = result?;
95+
if content.contains("[package]\nname = \"clippy\"") {
96+
return Ok(path.to_path_buf());
97+
}
98+
}
99+
Err(io::Error::new(ErrorKind::Other, "Unable to find project root"))
100+
}
101+
102+
fn to_camel_case(name: &str) -> String {
103+
name.split('_')
104+
.map(|s| {
105+
if s.is_empty() {
106+
String::from("")
107+
} else {
108+
[&s[0..1].to_uppercase(), &s[1..]].concat()
109+
}
110+
})
111+
.collect()
112+
}
113+
114+
fn get_test_file_contents(lint_name: &str) -> String {
115+
format!(
116+
"#![warn(clippy::{})]
117+
118+
fn main() {{
119+
// test code goes here
120+
}}
121+
",
122+
lint_name
123+
)
124+
}
125+
126+
fn get_lint_file_contents(
127+
pass_type: &str,
128+
lint_name: &str,
129+
camel_case_name: &str,
130+
category: &str,
131+
pass_import: &str,
132+
context_import: &str,
133+
) -> String {
134+
format!(
135+
"use rustc::lint::{{LintArray, LintPass, {type}, {context_import}}};
136+
use rustc_session::{{declare_lint_pass, declare_tool_lint}};
137+
{pass_import}
138+
139+
declare_clippy_lint! {{
140+
/// **What it does:**
141+
///
142+
/// **Why is this bad?**
143+
///
144+
/// **Known problems:** None.
145+
///
146+
/// **Example:**
147+
///
148+
/// ```rust
149+
/// // example code
150+
/// ```
151+
pub {name_upper},
152+
{category},
153+
\"default lint description\"
154+
}}
155+
156+
declare_lint_pass!({name_camel} => [{name_upper}]);
157+
158+
impl {type} for {name_camel} {{}}
159+
",
160+
type=pass_type,
161+
name_upper=lint_name.to_uppercase(),
162+
name_camel=camel_case_name,
163+
category=category,
164+
pass_import=pass_import,
165+
context_import=context_import
166+
)
167+
}
168+
169+
#[test]
170+
fn test_camel_case() {
171+
let s = "a_lint";
172+
let s2 = to_camel_case(s);
173+
assert_eq!(s2, "ALint");
174+
175+
let name = "a_really_long_new_lint";
176+
let name2 = to_camel_case(name);
177+
assert_eq!(name2, "AReallyLongNewLint");
178+
179+
let name3 = "lint__name";
180+
let name4 = to_camel_case(name3);
181+
assert_eq!(name4, "LintName");
182+
}

0 commit comments

Comments
 (0)