560 lines
17 KiB
Rust
560 lines
17 KiB
Rust
use crate::clippy_project_root;
|
|
use indoc::{formatdoc, writedoc};
|
|
use std::fmt;
|
|
use std::fmt::Write as _;
|
|
use std::fs::{self, OpenOptions};
|
|
use std::io::prelude::*;
|
|
use std::io::{self, ErrorKind};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
struct LintData<'a> {
|
|
pass: &'a str,
|
|
name: &'a str,
|
|
category: &'a str,
|
|
ty: Option<&'a str>,
|
|
project_root: PathBuf,
|
|
}
|
|
|
|
trait Context {
|
|
fn context<C: AsRef<str>>(self, text: C) -> Self;
|
|
}
|
|
|
|
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!("{}: {e}", text.as_ref());
|
|
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 out if the files couldn't be created or written to.
|
|
#[allow(clippy::missing_panics_doc)]
|
|
pub fn create(
|
|
pass: &String,
|
|
lint_name: Option<&String>,
|
|
category: Option<&str>,
|
|
mut ty: Option<&str>,
|
|
msrv: bool,
|
|
) -> io::Result<()> {
|
|
if category == Some("cargo") && ty.is_none() {
|
|
// `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo`
|
|
ty = Some("cargo");
|
|
}
|
|
|
|
let lint = LintData {
|
|
pass,
|
|
name: lint_name.expect("`name` argument is validated by clap"),
|
|
category: category.expect("`category` argument is validated by clap"),
|
|
ty,
|
|
project_root: clippy_project_root(),
|
|
};
|
|
|
|
create_lint(&lint, msrv).context("Unable to create lint implementation")?;
|
|
create_test(&lint).context("Unable to create a test for the new lint")?;
|
|
|
|
if lint.ty.is_none() {
|
|
add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")?;
|
|
}
|
|
|
|
if pass == "early" {
|
|
println!(
|
|
"\n\
|
|
NOTE: Use a late pass unless you need something specific from\
|
|
an early pass, as they lack many features and utilities"
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
|
|
if let Some(ty) = lint.ty {
|
|
create_lint_for_ty(lint, enable_msrv, ty)
|
|
} else {
|
|
let lint_contents = get_lint_file_contents(lint, enable_msrv);
|
|
let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
|
|
write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())?;
|
|
println!("Generated lint file: `{lint_path}`");
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
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))?;
|
|
|
|
path.push("src");
|
|
fs::create_dir(&path)?;
|
|
write_file(path.join("main.rs"), get_test_file_contents(lint_name))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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")?;
|
|
|
|
println!("Generated test directories: `{relative_test_dir}/pass`, `{relative_test_dir}/fail`");
|
|
} 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)?;
|
|
|
|
println!("Generated test file: `{test_path}`");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn add_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
|
|
let path = "clippy_lints/src/lib.rs";
|
|
let mut lib_rs = fs::read_to_string(path).context("reading")?;
|
|
|
|
let comment_start = lib_rs.find("// add lints here,").expect("Couldn't find comment");
|
|
|
|
let new_lint = if enable_msrv {
|
|
format!(
|
|
"store.register_{lint_pass}_pass(move |{ctor_arg}| Box::new({module_name}::{camel_name}::new(msrv())));\n ",
|
|
lint_pass = lint.pass,
|
|
ctor_arg = if lint.pass == "late" { "_" } else { "" },
|
|
module_name = lint.name,
|
|
camel_name = to_camel_case(lint.name),
|
|
)
|
|
} else {
|
|
format!(
|
|
"store.register_{lint_pass}_pass(|{ctor_arg}| Box::new({module_name}::{camel_name}));\n ",
|
|
lint_pass = lint.pass,
|
|
ctor_arg = if lint.pass == "late" { "_" } else { "" },
|
|
module_name = lint.name,
|
|
camel_name = to_camel_case(lint.name),
|
|
)
|
|
};
|
|
|
|
lib_rs.insert_str(comment_start, &new_lint);
|
|
|
|
fs::write(path, lib_rs).context("writing")
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
|
|
}
|
|
|
|
fn to_camel_case(name: &str) -> String {
|
|
name.split('_')
|
|
.map(|s| {
|
|
if s.is_empty() {
|
|
String::new()
|
|
} else {
|
|
[&s[0..1].to_uppercase(), &s[1..]].concat()
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn get_stabilization_version() -> String {
|
|
fn parse_manifest(contents: &str) -> Option<String> {
|
|
let version = contents
|
|
.lines()
|
|
.filter_map(|l| l.split_once('='))
|
|
.find_map(|(k, v)| (k.trim() == "version").then(|| v.trim()))?;
|
|
let Some(("0", version)) = version.get(1..version.len() - 1)?.split_once('.') else {
|
|
return None;
|
|
};
|
|
let (minor, patch) = version.split_once('.')?;
|
|
Some(format!(
|
|
"{}.{}.0",
|
|
minor.parse::<u32>().ok()?,
|
|
patch.parse::<u32>().ok()?
|
|
))
|
|
}
|
|
let contents = fs::read_to_string("Cargo.toml").expect("Unable to read `Cargo.toml`");
|
|
parse_manifest(&contents).expect("Unable to find package version in `Cargo.toml`")
|
|
}
|
|
|
|
fn get_test_file_contents(lint_name: &str) -> String {
|
|
formatdoc!(
|
|
r#"
|
|
#![warn(clippy::{lint_name})]
|
|
|
|
fn main() {{
|
|
// test code goes here
|
|
}}
|
|
"#
|
|
)
|
|
}
|
|
|
|
fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
|
|
formatdoc!(
|
|
r#"
|
|
# {hint}
|
|
|
|
[package]
|
|
name = "{lint_name}"
|
|
version = "0.1.0"
|
|
publish = false
|
|
|
|
[workspace]
|
|
"#
|
|
)
|
|
}
|
|
|
|
fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
|
|
let mut result = String::new();
|
|
|
|
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`!");
|
|
},
|
|
};
|
|
|
|
let lint_name = lint.name;
|
|
let category = lint.category;
|
|
let name_camel = to_camel_case(lint.name);
|
|
let name_upper = lint_name.to_uppercase();
|
|
|
|
result.push_str(&if enable_msrv {
|
|
formatdoc!(
|
|
r#"
|
|
use clippy_utils::msrvs::{{self, Msrv}};
|
|
{pass_import}
|
|
use rustc_lint::{{{context_import}, {pass_type}, LintContext}};
|
|
use rustc_session::{{declare_tool_lint, impl_lint_pass}};
|
|
|
|
"#
|
|
)
|
|
} else {
|
|
formatdoc!(
|
|
r#"
|
|
{pass_import}
|
|
use rustc_lint::{{{context_import}, {pass_type}}};
|
|
use rustc_session::{{declare_lint_pass, declare_tool_lint}};
|
|
|
|
"#
|
|
)
|
|
});
|
|
|
|
let _: fmt::Result = write!(result, "{}", get_lint_declaration(&name_upper, category));
|
|
|
|
result.push_str(&if enable_msrv {
|
|
formatdoc!(
|
|
r#"
|
|
pub struct {name_camel} {{
|
|
msrv: Msrv,
|
|
}}
|
|
|
|
impl {name_camel} {{
|
|
#[must_use]
|
|
pub fn new(msrv: Msrv) -> Self {{
|
|
Self {{ msrv }}
|
|
}}
|
|
}}
|
|
|
|
impl_lint_pass!({name_camel} => [{name_upper}]);
|
|
|
|
impl {pass_type}{pass_lifetimes} for {name_camel} {{
|
|
extract_msrv_attr!({context_import});
|
|
}}
|
|
|
|
// TODO: Add MSRV level to `clippy_utils/src/msrvs.rs` if needed.
|
|
// TODO: Add MSRV test to `tests/ui/min_rust_version_attr.rs`.
|
|
// TODO: Update msrv config comment in `clippy_lints/src/utils/conf.rs`
|
|
"#
|
|
)
|
|
} else {
|
|
formatdoc!(
|
|
r#"
|
|
declare_lint_pass!({name_camel} => [{name_upper}]);
|
|
|
|
impl {pass_type}{pass_lifetimes} for {name_camel} {{}}
|
|
"#
|
|
)
|
|
});
|
|
|
|
result
|
|
}
|
|
|
|
fn get_lint_declaration(name_upper: &str, category: &str) -> String {
|
|
formatdoc!(
|
|
r#"
|
|
declare_clippy_lint! {{
|
|
/// ### What it does
|
|
///
|
|
/// ### Why is this bad?
|
|
///
|
|
/// ### Example
|
|
/// ```rust
|
|
/// // example code where clippy issues a warning
|
|
/// ```
|
|
/// Use instead:
|
|
/// ```rust
|
|
/// // example code which does not raise clippy warning
|
|
/// ```
|
|
#[clippy::version = "{}"]
|
|
pub {name_upper},
|
|
{category},
|
|
"default lint description"
|
|
}}
|
|
"#,
|
|
get_stabilization_version(),
|
|
)
|
|
}
|
|
|
|
fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::Result<()> {
|
|
match ty {
|
|
"cargo" => assert_eq!(
|
|
lint.category, "cargo",
|
|
"Lints of type `cargo` must have the `cargo` category"
|
|
),
|
|
_ if lint.category == "cargo" => panic!("Lints of category `cargo` must have the `cargo` type"),
|
|
_ => {},
|
|
}
|
|
|
|
let ty_dir = lint.project_root.join(format!("clippy_lints/src/{ty}"));
|
|
assert!(
|
|
ty_dir.exists() && ty_dir.is_dir(),
|
|
"Directory `{}` does not exist!",
|
|
ty_dir.display()
|
|
);
|
|
|
|
let lint_file_path = ty_dir.join(format!("{}.rs", lint.name));
|
|
assert!(
|
|
!lint_file_path.exists(),
|
|
"File `{}` already exists",
|
|
lint_file_path.display()
|
|
);
|
|
|
|
let mod_file_path = ty_dir.join("mod.rs");
|
|
let context_import = setup_mod_file(&mod_file_path, lint)?;
|
|
let pass_lifetimes = match context_import {
|
|
"LateContext" => "<'_>",
|
|
_ => "",
|
|
};
|
|
|
|
let name_upper = lint.name.to_uppercase();
|
|
let mut lint_file_contents = String::new();
|
|
|
|
if enable_msrv {
|
|
let _: fmt::Result = writedoc!(
|
|
lint_file_contents,
|
|
r#"
|
|
use clippy_utils::msrvs::{{self, Msrv}};
|
|
use rustc_lint::{{{context_import}, LintContext}};
|
|
|
|
use super::{name_upper};
|
|
|
|
// TODO: Adjust the parameters as necessary
|
|
pub(super) fn check(cx: &{context_import}{pass_lifetimes}, msrv: &Msrv) {{
|
|
if !msrv.meets(todo!("Add a new entry in `clippy_utils/src/msrvs`")) {{
|
|
return;
|
|
}}
|
|
todo!();
|
|
}}
|
|
"#
|
|
);
|
|
} else {
|
|
let _: fmt::Result = writedoc!(
|
|
lint_file_contents,
|
|
r#"
|
|
use rustc_lint::{{{context_import}, LintContext}};
|
|
|
|
use super::{name_upper};
|
|
|
|
// TODO: Adjust the parameters as necessary
|
|
pub(super) fn check(cx: &{context_import}{pass_lifetimes}) {{
|
|
todo!();
|
|
}}
|
|
"#
|
|
);
|
|
}
|
|
|
|
write_file(lint_file_path.as_path(), lint_file_contents)?;
|
|
println!("Generated lint file: `clippy_lints/src/{ty}/{}.rs`", lint.name);
|
|
println!(
|
|
"Be sure to add a call to `{}::check` in `clippy_lints/src/{ty}/mod.rs`!",
|
|
lint.name
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_lines)]
|
|
fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> {
|
|
use super::update_lints::{match_tokens, LintDeclSearchResult};
|
|
use rustc_lexer::TokenKind;
|
|
|
|
let lint_name_upper = lint.name.to_uppercase();
|
|
|
|
let mut file_contents = fs::read_to_string(path)?;
|
|
assert!(
|
|
!file_contents.contains(&lint_name_upper),
|
|
"Lint `{}` already defined in `{}`",
|
|
lint.name,
|
|
path.display()
|
|
);
|
|
|
|
let mut offset = 0usize;
|
|
let mut last_decl_curly_offset = None;
|
|
let mut lint_context = None;
|
|
|
|
let mut iter = rustc_lexer::tokenize(&file_contents).map(|t| {
|
|
let range = offset..offset + t.len as usize;
|
|
offset = range.end;
|
|
|
|
LintDeclSearchResult {
|
|
token_kind: t.kind,
|
|
content: &file_contents[range.clone()],
|
|
range,
|
|
}
|
|
});
|
|
|
|
// Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl
|
|
while let Some(LintDeclSearchResult { content, .. }) = iter.find(|result| result.token_kind == TokenKind::Ident) {
|
|
let mut iter = iter
|
|
.by_ref()
|
|
.filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
|
|
|
|
match content {
|
|
"declare_clippy_lint" => {
|
|
// matches `!{`
|
|
match_tokens!(iter, Bang OpenBrace);
|
|
if let Some(LintDeclSearchResult { range, .. }) =
|
|
iter.find(|result| result.token_kind == TokenKind::CloseBrace)
|
|
{
|
|
last_decl_curly_offset = Some(range.end);
|
|
}
|
|
},
|
|
"impl" => {
|
|
let mut token = iter.next();
|
|
match token {
|
|
// matches <'foo>
|
|
Some(LintDeclSearchResult {
|
|
token_kind: TokenKind::Lt,
|
|
..
|
|
}) => {
|
|
match_tokens!(iter, Lifetime { .. } Gt);
|
|
token = iter.next();
|
|
},
|
|
None => break,
|
|
_ => {},
|
|
}
|
|
|
|
if let Some(LintDeclSearchResult {
|
|
token_kind: TokenKind::Ident,
|
|
content,
|
|
..
|
|
}) = token
|
|
{
|
|
// Get the appropriate lint context struct
|
|
lint_context = match content {
|
|
"LateLintPass" => Some("LateContext"),
|
|
"EarlyLintPass" => Some("EarlyContext"),
|
|
_ => continue,
|
|
};
|
|
}
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
|
|
drop(iter);
|
|
|
|
let last_decl_curly_offset =
|
|
last_decl_curly_offset.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display()));
|
|
let lint_context =
|
|
lint_context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display()));
|
|
|
|
// Add the lint declaration to `mod.rs`
|
|
file_contents.replace_range(
|
|
// Remove the trailing newline, which should always be present
|
|
last_decl_curly_offset..=last_decl_curly_offset,
|
|
&format!("\n\n{}", get_lint_declaration(&lint_name_upper, lint.category)),
|
|
);
|
|
|
|
// Add the lint to `impl_lint_pass`/`declare_lint_pass`
|
|
let impl_lint_pass_start = file_contents.find("impl_lint_pass!").unwrap_or_else(|| {
|
|
file_contents
|
|
.find("declare_lint_pass!")
|
|
.unwrap_or_else(|| panic!("failed to find `impl_lint_pass`/`declare_lint_pass`"))
|
|
});
|
|
|
|
let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| {
|
|
panic!("malformed `impl_lint_pass`/`declare_lint_pass`");
|
|
});
|
|
|
|
arr_start += impl_lint_pass_start;
|
|
|
|
let mut arr_end = file_contents[arr_start..]
|
|
.find(']')
|
|
.expect("failed to find `impl_lint_pass` terminator");
|
|
|
|
arr_end += arr_start;
|
|
|
|
let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();
|
|
arr_content.retain(|c| !c.is_whitespace());
|
|
|
|
let mut new_arr_content = String::new();
|
|
for ident in arr_content
|
|
.split(',')
|
|
.chain(std::iter::once(&*lint_name_upper))
|
|
.filter(|s| !s.is_empty())
|
|
{
|
|
let _: fmt::Result = write!(new_arr_content, "\n {ident},");
|
|
}
|
|
new_arr_content.push('\n');
|
|
|
|
file_contents.replace_range(arr_start + 1..arr_end, &new_arr_content);
|
|
|
|
// Just add the mod declaration at the top, it'll be fixed by rustfmt
|
|
file_contents.insert_str(0, &format!("mod {};\n", &lint.name));
|
|
|
|
let mut file = OpenOptions::new()
|
|
.write(true)
|
|
.truncate(true)
|
|
.open(path)
|
|
.context(format!("trying to open: `{}`", path.display()))?;
|
|
file.write_all(file_contents.as_bytes())
|
|
.context(format!("writing to file: `{}`", path.display()))?;
|
|
|
|
Ok(lint_context)
|
|
}
|
|
|
|
#[test]
|
|
fn test_camel_case() {
|
|
let s = "a_lint";
|
|
let s2 = to_camel_case(s);
|
|
assert_eq!(s2, "ALint");
|
|
|
|
let name = "a_really_long_new_lint";
|
|
let name2 = to_camel_case(name);
|
|
assert_eq!(name2, "AReallyLongNewLint");
|
|
|
|
let name3 = "lint__name";
|
|
let name4 = to_camel_case(name3);
|
|
assert_eq!(name4, "LintName");
|
|
}
|