Skip to content

Commit 8b72b72

Browse files
committed
Auto merge of #4994 - bradsherman:new_lint_gen, r=flip1995
Autogenerate new lints Add option in clippy_dev to automatically generate boilerplate code for adding new lints example: `./util/dev new_lint --name=iter_nth_zero --type=late` Fixes #4942 changelog: none
2 parents a8d90f6 + 32337a9 commit 8b72b72

File tree

7 files changed

+381
-32
lines changed

7 files changed

+381
-32
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+
}

clippy_lints/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10901090
LintId::of(&utils::internal_lints::LINT_WITHOUT_LINT_PASS),
10911091
LintId::of(&utils::internal_lints::OUTER_EXPN_EXPN_DATA),
10921092
LintId::of(&utils::internal_lints::PRODUCE_ICE),
1093+
LintId::of(&utils::internal_lints::DEFAULT_LINT),
10931094
]);
10941095

10951096
store.register_group(true, "clippy::all", Some("clippy"), vec![

clippy_lints/src/utils/internal_lints.rs

+49-4
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ use rustc_hir::*;
1313
use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass};
1414
use rustc_session::declare_tool_lint;
1515
use rustc_session::{declare_lint_pass, impl_lint_pass};
16-
use rustc_span::source_map::Span;
16+
use rustc_span::source_map::{Span, Spanned};
1717
use rustc_span::symbol::SymbolStr;
1818
use syntax::ast;
19-
use syntax::ast::{Crate as AstCrate, ItemKind, Name};
19+
use syntax::ast::{Crate as AstCrate, ItemKind, LitKind, Name};
2020
use syntax::visit::FnKind;
2121

2222
declare_clippy_lint! {
@@ -121,6 +121,29 @@ declare_clippy_lint! {
121121
"this message should not appear anywhere as we ICE before and don't emit the lint"
122122
}
123123

124+
declare_clippy_lint! {
125+
/// **What it does:** Checks for cases of an auto-generated lint without an updated description,
126+
/// i.e. `default lint description`.
127+
///
128+
/// **Why is this bad?** Indicates that the lint is not finished.
129+
///
130+
/// **Known problems:** None
131+
///
132+
/// **Example:**
133+
/// Bad:
134+
/// ```rust,ignore
135+
/// declare_lint! { pub COOL_LINT, nursery, "default lint description" }
136+
/// ```
137+
///
138+
/// Good:
139+
/// ```rust,ignore
140+
/// declare_lint! { pub COOL_LINT, nursery, "a great new lint" }
141+
/// ```
142+
pub DEFAULT_LINT,
143+
internal,
144+
"found 'default lint description' in a lint declaration"
145+
}
146+
124147
declare_lint_pass!(ClippyLintsInternal => [CLIPPY_LINTS_INTERNAL]);
125148

126149
impl EarlyLintPass for ClippyLintsInternal {
@@ -163,12 +186,34 @@ pub struct LintWithoutLintPass {
163186
registered_lints: FxHashSet<Name>,
164187
}
165188

166-
impl_lint_pass!(LintWithoutLintPass => [LINT_WITHOUT_LINT_PASS]);
189+
impl_lint_pass!(LintWithoutLintPass => [DEFAULT_LINT, LINT_WITHOUT_LINT_PASS]);
167190

168191
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for LintWithoutLintPass {
169192
fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx Item<'_>) {
170-
if let hir::ItemKind::Static(ref ty, Mutability::Not, _) = item.kind {
193+
if let hir::ItemKind::Static(ref ty, Mutability::Not, body_id) = item.kind {
171194
if is_lint_ref_type(cx, ty) {
195+
let expr = &cx.tcx.hir().body(body_id).value;
196+
if_chain! {
197+
if let ExprKind::AddrOf(_, _, ref inner_exp) = expr.kind;
198+
if let ExprKind::Struct(_, ref fields, _) = inner_exp.kind;
199+
let field = fields.iter()
200+
.find(|f| f.ident.as_str() == "desc")
201+
.expect("lints must have a description field");
202+
if let ExprKind::Lit(Spanned {
203+
node: LitKind::Str(ref sym, _),
204+
..
205+
}) = field.expr.kind;
206+
if sym.as_str() == "default lint description";
207+
208+
then {
209+
span_lint(
210+
cx,
211+
DEFAULT_LINT,
212+
item.span,
213+
&format!("the lint `{}` has the default lint description", item.ident.name),
214+
);
215+
}
216+
}
172217
self.declared_lints.insert(item.ident.name, item.span);
173218
}
174219
} else if is_expn_of(item.span, "impl_lint_pass").is_some()

0 commit comments

Comments
 (0)