Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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