Skip to content

Commit 9566021

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

File tree

2 files changed

+236
-0
lines changed

2 files changed

+236
-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, defaults to `nursery`")
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

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

0 commit comments

Comments
 (0)