Skip to content

Commit 780572b

Browse files
committed
Auto merge of rust-lang#5614 - ebroto:test_cargo_lints, r=flip1995
Test cargo lints changelog: Add infrastructure to test cargo lints Closes rust-lang#5603
2 parents 1831385 + f9013ff commit 780572b

File tree

22 files changed

+394
-109
lines changed

22 files changed

+394
-109
lines changed

clippy_dev/src/new_lint.rs

Lines changed: 106 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,110 @@
11
use crate::clippy_project_root;
2-
use std::fs::{File, OpenOptions};
3-
use std::io;
2+
use std::fs::{self, OpenOptions};
43
use std::io::prelude::*;
5-
use std::io::ErrorKind;
6-
use std::path::Path;
4+
use std::io::{self, ErrorKind};
5+
use std::path::{Path, PathBuf};
6+
7+
struct LintData<'a> {
8+
pass: &'a str,
9+
name: &'a str,
10+
category: &'a str,
11+
project_root: PathBuf,
12+
}
13+
14+
trait Context {
15+
fn context<C: AsRef<str>>(self, text: C) -> Self;
16+
}
717

8-
/// Creates files required to implement and test a new lint and runs `update_lints`.
18+
impl<T> Context for io::Result<T> {
19+
fn context<C: AsRef<str>>(self, text: C) -> Self {
20+
match self {
21+
Ok(t) => Ok(t),
22+
Err(e) => {
23+
let message = format!("{}: {}", text.as_ref(), e);
24+
Err(io::Error::new(ErrorKind::Other, message))
25+
},
26+
}
27+
}
28+
}
29+
30+
/// Creates the files required to implement and test a new lint and runs `update_lints`.
931
///
1032
/// # Errors
1133
///
12-
/// This function errors, if the files couldn't be created
13-
pub fn create(pass: Option<&str>, lint_name: Option<&str>, category: Option<&str>) -> Result<(), io::Error> {
14-
let pass = pass.expect("`pass` argument is validated by clap");
15-
let lint_name = lint_name.expect("`name` argument is validated by clap");
16-
let category = category.expect("`category` argument is validated by clap");
17-
18-
match open_files(lint_name) {
19-
Ok((mut test_file, mut lint_file)) => {
20-
let (pass_type, pass_lifetimes, pass_import, context_import) = match pass {
21-
"early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
22-
"late" => ("LateLintPass", "<'_, '_>", "use rustc_hir::*;", "LateContext"),
23-
_ => {
24-
unreachable!("`pass_type` should only ever be `early` or `late`!");
25-
},
26-
};
27-
28-
let camel_case_name = to_camel_case(lint_name);
29-
30-
if let Err(e) = test_file.write_all(get_test_file_contents(lint_name).as_bytes()) {
31-
return Err(io::Error::new(
32-
ErrorKind::Other,
33-
format!("Could not write to test file: {}", e),
34-
));
35-
};
36-
37-
if let Err(e) = lint_file.write_all(
38-
get_lint_file_contents(
39-
pass_type,
40-
pass_lifetimes,
41-
lint_name,
42-
&camel_case_name,
43-
category,
44-
pass_import,
45-
context_import,
46-
)
47-
.as_bytes(),
48-
) {
49-
return Err(io::Error::new(
50-
ErrorKind::Other,
51-
format!("Could not write to lint file: {}", e),
52-
));
53-
}
54-
Ok(())
34+
/// This function errors out if the files couldn't be created or written to.
35+
pub fn create(pass: Option<&str>, lint_name: Option<&str>, category: Option<&str>) -> io::Result<()> {
36+
let lint = LintData {
37+
pass: pass.expect("`pass` argument is validated by clap"),
38+
name: lint_name.expect("`name` argument is validated by clap"),
39+
category: category.expect("`category` argument is validated by clap"),
40+
project_root: clippy_project_root(),
41+
};
42+
43+
create_lint(&lint).context("Unable to create lint implementation")?;
44+
create_test(&lint).context("Unable to create a test for the new lint")
45+
}
46+
47+
fn create_lint(lint: &LintData) -> io::Result<()> {
48+
let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass {
49+
"early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
50+
"late" => ("LateLintPass", "<'_, '_>", "use rustc_hir::*;", "LateContext"),
51+
_ => {
52+
unreachable!("`pass_type` should only ever be `early` or `late`!");
5553
},
56-
Err(e) => Err(io::Error::new(
57-
ErrorKind::Other,
58-
format!("Unable to create lint: {}", e),
59-
)),
60-
}
54+
};
55+
56+
let camel_case_name = to_camel_case(lint.name);
57+
let lint_contents = get_lint_file_contents(
58+
pass_type,
59+
pass_lifetimes,
60+
lint.name,
61+
&camel_case_name,
62+
lint.category,
63+
pass_import,
64+
context_import,
65+
);
66+
67+
let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
68+
write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())
6169
}
6270

63-
fn open_files(lint_name: &str) -> Result<(File, File), io::Error> {
64-
let project_root = clippy_project_root();
71+
fn create_test(lint: &LintData) -> io::Result<()> {
72+
fn create_project_layout<P: Into<PathBuf>>(lint_name: &str, location: P, case: &str, hint: &str) -> io::Result<()> {
73+
let mut path = location.into().join(case);
74+
fs::create_dir(&path)?;
75+
write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?;
6576

66-
let test_file_path = project_root.join("tests").join("ui").join(format!("{}.rs", lint_name));
67-
let lint_file_path = project_root
68-
.join("clippy_lints")
69-
.join("src")
70-
.join(format!("{}.rs", lint_name));
77+
path.push("src");
78+
fs::create_dir(&path)?;
79+
write_file(path.join("main.rs"), get_test_file_contents(lint_name))?;
7180

72-
if Path::new(&test_file_path).exists() {
73-
return Err(io::Error::new(
74-
ErrorKind::AlreadyExists,
75-
format!("test file {:?} already exists", test_file_path),
76-
));
81+
Ok(())
7782
}
78-
if Path::new(&lint_file_path).exists() {
79-
return Err(io::Error::new(
80-
ErrorKind::AlreadyExists,
81-
format!("lint file {:?} already exists", lint_file_path),
82-
));
83+
84+
if lint.category == "cargo" {
85+
let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
86+
let test_dir = lint.project_root.join(relative_test_dir);
87+
fs::create_dir(&test_dir)?;
88+
89+
create_project_layout(lint.name, &test_dir, "fail", "Content that triggers the lint goes here")?;
90+
create_project_layout(lint.name, &test_dir, "pass", "This file should not trigger the lint")
91+
} else {
92+
let test_path = format!("tests/ui/{}.rs", lint.name);
93+
let test_contents = get_test_file_contents(lint.name);
94+
write_file(lint.project_root.join(test_path), test_contents)
8395
}
96+
}
8497

85-
let test_file = OpenOptions::new().write(true).create_new(true).open(test_file_path)?;
86-
let lint_file = OpenOptions::new().write(true).create_new(true).open(lint_file_path)?;
98+
fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
99+
fn inner(path: &Path, contents: &[u8]) -> io::Result<()> {
100+
OpenOptions::new()
101+
.write(true)
102+
.create_new(true)
103+
.open(path)?
104+
.write_all(contents)
105+
}
87106

88-
Ok((test_file, lint_file))
107+
inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
89108
}
90109

91110
fn to_camel_case(name: &str) -> String {
@@ -112,6 +131,20 @@ fn main() {{
112131
)
113132
}
114133

134+
fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
135+
format!(
136+
r#"
137+
# {}
138+
139+
[package]
140+
name = "{}"
141+
version = "0.1.0"
142+
publish = false
143+
"#,
144+
hint, lint_name
145+
)
146+
}
147+
115148
fn get_lint_file_contents(
116149
pass_type: &str,
117150
pass_lifetimes: &str,

clippy_lints/src/cargo_common_metadata.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ declare_clippy_lint! {
2323
/// [package]
2424
/// name = "clippy"
2525
/// version = "0.0.212"
26+
/// authors = ["Someone <[email protected]>"]
2627
/// description = "A bunch of helpful lints to avoid common pitfalls in Rust"
2728
/// repository = "https://github.com/rust-lang/rust-clippy"
2829
/// readme = "README.md"

clippy_lints/src/multiple_crate_versions.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ impl LateLintPass<'_, '_> for MultipleCrateVersions {
5454
let group: Vec<cargo_metadata::Package> = group.collect();
5555

5656
if group.len() > 1 {
57-
let versions = group.into_iter().map(|p| p.version).join(", ");
57+
let mut versions: Vec<_> = group.into_iter().map(|p| p.version).collect();
58+
versions.sort();
59+
let versions = versions.iter().join(", ");
5860

5961
span_lint(
6062
cx,

doc/adding_lints.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ case), and we don't need type information so it will have an early pass type
4242
`cargo dev new_lint --name=foo_functions --pass=early --category=pedantic`
4343
(category will default to nursery if not provided). This command will create
4444
two files: `tests/ui/foo_functions.rs` and `clippy_lints/src/foo_functions.rs`,
45-
as well as run `cargo dev update_lints` to register the new lint. Next, we'll
46-
open up these files and add our lint!
45+
as well as run `cargo dev update_lints` to register the new lint. For cargo lints,
46+
two project hierarchies (fail/pass) will be created by default under `tests/ui-cargo`.
47+
48+
Next, we'll open up these files and add our lint!
4749

4850
## Testing
4951

@@ -105,6 +107,24 @@ our lint, we need to commit the generated `.stderr` files, too. In general, you
105107
should only commit files changed by `tests/ui/update-all-references.sh` for the
106108
specific lint you are creating/editing.
107109

110+
### Cargo lints
111+
112+
For cargo lints, the process of testing differs in that we are interested in
113+
the `Cargo.toml` manifest file. We also need a minimal crate associated
114+
with that manifest.
115+
116+
If our new lint is named e.g. `foo_categories`, after running `cargo dev new_lint`
117+
we will find by default two new crates, each with its manifest file:
118+
119+
* `tests/ui-cargo/foo_categories/fail/Cargo.toml`: this file should cause the new lint to raise an error.
120+
* `tests/ui-cargo/foo_categories/pass/Cargo.toml`: this file should not trigger the lint.
121+
122+
If you need more cases, you can copy one of those crates (under `foo_categories`) and rename it.
123+
124+
The process of generating the `.stderr` file is the same, and prepending the `TESTNAME`
125+
variable to `cargo uitest` works too, but the script to update the references
126+
is in another path: `tests/ui-cargo/update-all-references.sh`.
127+
108128
## Rustfix tests
109129

110130
If the lint you are working on is making use of structured suggestions, the

0 commit comments

Comments
 (0)