Skip to content

Commit

Permalink
Auto merge of #5614 - ebroto:test_cargo_lints, r=flip1995
Browse files Browse the repository at this point in the history
Test cargo lints

changelog: Add infrastructure to test cargo lints

Closes #5603
  • Loading branch information
bors committed May 21, 2020
2 parents 1831385 + f9013ff commit 780572b
Show file tree
Hide file tree
Showing 22 changed files with 394 additions and 109 deletions.
179 changes: 106 additions & 73 deletions clippy_dev/src/new_lint.rs
Original file line number Diff line number Diff line change
@@ -1,91 +1,110 @@
use crate::clippy_project_root;
use std::fs::{File, OpenOptions};
use std::io;
use std::fs::{self, OpenOptions};
use std::io::prelude::*;
use std::io::ErrorKind;
use std::path::Path;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};

struct LintData<'a> {
pass: &'a str,
name: &'a str,
category: &'a str,
project_root: PathBuf,
}

trait Context {
fn context<C: AsRef<str>>(self, text: C) -> Self;
}

/// Creates files required to implement and test a new lint and runs `update_lints`.
impl<T> Context for io::Result<T> {
fn context<C: AsRef<str>>(self, text: C) -> Self {
match self {
Ok(t) => Ok(t),
Err(e) => {
let message = format!("{}: {}", text.as_ref(), e);
Err(io::Error::new(ErrorKind::Other, message))
},
}
}
}

/// Creates the files required to implement and test a new lint and runs `update_lints`.
///
/// # Errors
///
/// This function errors, if the files couldn't be created
pub fn create(pass: Option<&str>, lint_name: Option<&str>, category: Option<&str>) -> Result<(), io::Error> {
let pass = pass.expect("`pass` argument is validated by clap");
let lint_name = lint_name.expect("`name` argument is validated by clap");
let category = category.expect("`category` argument is validated by clap");

match open_files(lint_name) {
Ok((mut test_file, mut lint_file)) => {
let (pass_type, pass_lifetimes, pass_import, context_import) = match pass {
"early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
"late" => ("LateLintPass", "<'_, '_>", "use rustc_hir::*;", "LateContext"),
_ => {
unreachable!("`pass_type` should only ever be `early` or `late`!");
},
};

let camel_case_name = to_camel_case(lint_name);

if let Err(e) = test_file.write_all(get_test_file_contents(lint_name).as_bytes()) {
return Err(io::Error::new(
ErrorKind::Other,
format!("Could not write to test file: {}", e),
));
};

if let Err(e) = lint_file.write_all(
get_lint_file_contents(
pass_type,
pass_lifetimes,
lint_name,
&camel_case_name,
category,
pass_import,
context_import,
)
.as_bytes(),
) {
return Err(io::Error::new(
ErrorKind::Other,
format!("Could not write to lint file: {}", e),
));
}
Ok(())
/// This function errors out if the files couldn't be created or written to.
pub fn create(pass: Option<&str>, lint_name: Option<&str>, category: Option<&str>) -> io::Result<()> {
let lint = LintData {
pass: pass.expect("`pass` argument is validated by clap"),
name: lint_name.expect("`name` argument is validated by clap"),
category: category.expect("`category` argument is validated by clap"),
project_root: clippy_project_root(),
};

create_lint(&lint).context("Unable to create lint implementation")?;
create_test(&lint).context("Unable to create a test for the new lint")
}

fn create_lint(lint: &LintData) -> io::Result<()> {
let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass {
"early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
"late" => ("LateLintPass", "<'_, '_>", "use rustc_hir::*;", "LateContext"),
_ => {
unreachable!("`pass_type` should only ever be `early` or `late`!");
},
Err(e) => Err(io::Error::new(
ErrorKind::Other,
format!("Unable to create lint: {}", e),
)),
}
};

let camel_case_name = to_camel_case(lint.name);
let lint_contents = get_lint_file_contents(
pass_type,
pass_lifetimes,
lint.name,
&camel_case_name,
lint.category,
pass_import,
context_import,
);

let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())
}

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

let test_file_path = project_root.join("tests").join("ui").join(format!("{}.rs", lint_name));
let lint_file_path = project_root
.join("clippy_lints")
.join("src")
.join(format!("{}.rs", lint_name));
path.push("src");
fs::create_dir(&path)?;
write_file(path.join("main.rs"), get_test_file_contents(lint_name))?;

if Path::new(&test_file_path).exists() {
return Err(io::Error::new(
ErrorKind::AlreadyExists,
format!("test file {:?} already exists", test_file_path),
));
Ok(())
}
if Path::new(&lint_file_path).exists() {
return Err(io::Error::new(
ErrorKind::AlreadyExists,
format!("lint file {:?} already exists", lint_file_path),
));

if lint.category == "cargo" {
let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
let test_dir = lint.project_root.join(relative_test_dir);
fs::create_dir(&test_dir)?;

create_project_layout(lint.name, &test_dir, "fail", "Content that triggers the lint goes here")?;
create_project_layout(lint.name, &test_dir, "pass", "This file should not trigger the lint")
} else {
let test_path = format!("tests/ui/{}.rs", lint.name);
let test_contents = get_test_file_contents(lint.name);
write_file(lint.project_root.join(test_path), test_contents)
}
}

let test_file = OpenOptions::new().write(true).create_new(true).open(test_file_path)?;
let lint_file = OpenOptions::new().write(true).create_new(true).open(lint_file_path)?;
fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
fn inner(path: &Path, contents: &[u8]) -> io::Result<()> {
OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?
.write_all(contents)
}

Ok((test_file, lint_file))
inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
}

fn to_camel_case(name: &str) -> String {
Expand All @@ -112,6 +131,20 @@ fn main() {{
)
}

fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
format!(
r#"
# {}
[package]
name = "{}"
version = "0.1.0"
publish = false
"#,
hint, lint_name
)
}

fn get_lint_file_contents(
pass_type: &str,
pass_lifetimes: &str,
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/cargo_common_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ declare_clippy_lint! {
/// [package]
/// name = "clippy"
/// version = "0.0.212"
/// authors = ["Someone <[email protected]>"]
/// description = "A bunch of helpful lints to avoid common pitfalls in Rust"
/// repository = "https://github.com/rust-lang/rust-clippy"
/// readme = "README.md"
Expand Down
4 changes: 3 additions & 1 deletion clippy_lints/src/multiple_crate_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ impl LateLintPass<'_, '_> for MultipleCrateVersions {
let group: Vec<cargo_metadata::Package> = group.collect();

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

span_lint(
cx,
Expand Down
24 changes: 22 additions & 2 deletions doc/adding_lints.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ case), and we don't need type information so it will have an early pass type
`cargo dev new_lint --name=foo_functions --pass=early --category=pedantic`
(category will default to nursery if not provided). This command will create
two files: `tests/ui/foo_functions.rs` and `clippy_lints/src/foo_functions.rs`,
as well as run `cargo dev update_lints` to register the new lint. Next, we'll
open up these files and add our lint!
as well as run `cargo dev update_lints` to register the new lint. For cargo lints,
two project hierarchies (fail/pass) will be created by default under `tests/ui-cargo`.

Next, we'll open up these files and add our lint!

## Testing

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

### Cargo lints

For cargo lints, the process of testing differs in that we are interested in
the `Cargo.toml` manifest file. We also need a minimal crate associated
with that manifest.

If our new lint is named e.g. `foo_categories`, after running `cargo dev new_lint`
we will find by default two new crates, each with its manifest file:

* `tests/ui-cargo/foo_categories/fail/Cargo.toml`: this file should cause the new lint to raise an error.
* `tests/ui-cargo/foo_categories/pass/Cargo.toml`: this file should not trigger the lint.

If you need more cases, you can copy one of those crates (under `foo_categories`) and rename it.

The process of generating the `.stderr` file is the same, and prepending the `TESTNAME`
variable to `cargo uitest` works too, but the script to update the references
is in another path: `tests/ui-cargo/update-all-references.sh`.

## Rustfix tests

If the lint you are working on is making use of structured suggestions, the
Expand Down
Loading

0 comments on commit 780572b

Please sign in to comment.