Skip to content

Commit 2a5160c

Browse files
committed
Generate new lints easily
- Add option in clippy_dev to automatically generate boilerplate code for adding new lints
1 parent 43ac941 commit 2a5160c

File tree

2 files changed

+235
-0
lines changed

2 files changed

+235
-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

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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::declare_lint_pass;
137+
use rustc_session::declare_tool_lint;
138+
{pass_import}
139+
140+
declare_clippy_lint! {{
141+
/// **What it does:**
142+
///
143+
/// **Why is this bad?**
144+
///
145+
/// **Known problems:** None.
146+
///
147+
/// **Example:**
148+
///
149+
/// ```rust
150+
/// // example code
151+
/// ```
152+
pub {name_upper},
153+
{category},
154+
\"default lint description\"
155+
}}
156+
157+
declare_lint_pass!({name_camel} => [{name_upper}]);
158+
159+
impl {type} for {name_camel} {{}}
160+
",
161+
type=pass_type,
162+
name_upper=lint_name.to_uppercase(),
163+
name_camel=camel_case_name,
164+
category=category,
165+
pass_import=pass_import,
166+
context_import=context_import
167+
)
168+
}
169+
170+
#[test]
171+
fn test_camel_case() {
172+
let s = "a_lint";
173+
let s2 = to_camel_case(s);
174+
assert_eq!(s2, "ALint");
175+
176+
let name = "a_really_long_new_lint";
177+
let name2 = to_camel_case(name);
178+
assert_eq!(name2, "AReallyLongNewLint");
179+
180+
let name3 = "lint__name";
181+
let name4 = to_camel_case(name3);
182+
assert_eq!(name4, "LintName");
183+
}

0 commit comments

Comments
 (0)