459 lines
16 KiB
Rust
459 lines
16 KiB
Rust
use crate::utils::{
|
|
ErrAction, File, FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, expect_action, update_text_region_fn,
|
|
};
|
|
use itertools::Itertools;
|
|
use std::collections::HashSet;
|
|
use std::fmt::Write;
|
|
use std::fs;
|
|
use std::ops::Range;
|
|
use std::path::{self, Path, PathBuf};
|
|
use walkdir::{DirEntry, WalkDir};
|
|
|
|
const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
|
|
// Use that command to update this file and do not edit by hand.\n\
|
|
// Manual edits will be overwritten.\n\n";
|
|
|
|
const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
|
|
|
|
/// Runs the `update_lints` command.
|
|
///
|
|
/// This updates various generated values from the lint source code.
|
|
///
|
|
/// `update_mode` indicates if the files should be updated or if updates should be checked for.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if a file path could not read from or then written to
|
|
pub fn update(update_mode: UpdateMode) {
|
|
let lints = find_lint_decls();
|
|
let (deprecated, renamed) = read_deprecated_lints();
|
|
generate_lint_files(update_mode, &lints, &deprecated, &renamed);
|
|
}
|
|
|
|
#[expect(clippy::too_many_lines)]
|
|
pub fn generate_lint_files(
|
|
update_mode: UpdateMode,
|
|
lints: &[Lint],
|
|
deprecated: &[DeprecatedLint],
|
|
renamed: &[RenamedLint],
|
|
) {
|
|
let mut updater = FileUpdater::default();
|
|
updater.update_file_checked(
|
|
"cargo dev update_lints",
|
|
update_mode,
|
|
"README.md",
|
|
&mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
|
|
write!(dst, "{}", round_to_fifty(lints.len())).unwrap();
|
|
}),
|
|
);
|
|
updater.update_file_checked(
|
|
"cargo dev update_lints",
|
|
update_mode,
|
|
"book/src/README.md",
|
|
&mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
|
|
write!(dst, "{}", round_to_fifty(lints.len())).unwrap();
|
|
}),
|
|
);
|
|
updater.update_file_checked(
|
|
"cargo dev update_lints",
|
|
update_mode,
|
|
"CHANGELOG.md",
|
|
&mut update_text_region_fn(
|
|
"<!-- begin autogenerated links to lint list -->\n",
|
|
"<!-- end autogenerated links to lint list -->",
|
|
|dst| {
|
|
for lint in lints
|
|
.iter()
|
|
.map(|l| &*l.name)
|
|
.chain(deprecated.iter().filter_map(|l| l.name.strip_prefix("clippy::")))
|
|
.chain(renamed.iter().filter_map(|l| l.old_name.strip_prefix("clippy::")))
|
|
.sorted()
|
|
{
|
|
writeln!(dst, "[`{lint}`]: {DOCS_LINK}#{lint}").unwrap();
|
|
}
|
|
},
|
|
),
|
|
);
|
|
updater.update_file_checked(
|
|
"cargo dev update_lints",
|
|
update_mode,
|
|
"clippy_lints/src/deprecated_lints.rs",
|
|
&mut |_, src, dst| {
|
|
let mut searcher = RustSearcher::new(src);
|
|
assert!(
|
|
searcher.find_token(Token::Ident("declare_with_version"))
|
|
&& searcher.find_token(Token::Ident("declare_with_version")),
|
|
"error reading deprecated lints"
|
|
);
|
|
dst.push_str(&src[..searcher.pos() as usize]);
|
|
dst.push_str("! { DEPRECATED(DEPRECATED_VERSION) = [\n");
|
|
for lint in deprecated {
|
|
write!(
|
|
dst,
|
|
" #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n",
|
|
lint.version, lint.name, lint.reason,
|
|
)
|
|
.unwrap();
|
|
}
|
|
dst.push_str(
|
|
"]}\n\n\
|
|
#[rustfmt::skip]\n\
|
|
declare_with_version! { RENAMED(RENAMED_VERSION) = [\n\
|
|
",
|
|
);
|
|
for lint in renamed {
|
|
write!(
|
|
dst,
|
|
" #[clippy::version = \"{}\"]\n (\"{}\", \"{}\"),\n",
|
|
lint.version, lint.old_name, lint.new_name,
|
|
)
|
|
.unwrap();
|
|
}
|
|
dst.push_str("]}\n");
|
|
UpdateStatus::from_changed(src != dst)
|
|
},
|
|
);
|
|
updater.update_file_checked(
|
|
"cargo dev update_lints",
|
|
update_mode,
|
|
"tests/ui/deprecated.rs",
|
|
&mut |_, src, dst| {
|
|
dst.push_str(GENERATED_FILE_COMMENT);
|
|
for lint in deprecated {
|
|
writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.name, lint.name).unwrap();
|
|
}
|
|
dst.push_str("\nfn main() {}\n");
|
|
UpdateStatus::from_changed(src != dst)
|
|
},
|
|
);
|
|
updater.update_file_checked(
|
|
"cargo dev update_lints",
|
|
update_mode,
|
|
"tests/ui/rename.rs",
|
|
&mut move |_, src, dst| {
|
|
let mut seen_lints = HashSet::new();
|
|
dst.push_str(GENERATED_FILE_COMMENT);
|
|
dst.push_str("#![allow(clippy::duplicated_attributes)]\n");
|
|
for lint in renamed {
|
|
if seen_lints.insert(&lint.new_name) {
|
|
writeln!(dst, "#![allow({})]", lint.new_name).unwrap();
|
|
}
|
|
}
|
|
seen_lints.clear();
|
|
for lint in renamed {
|
|
if seen_lints.insert(&lint.old_name) {
|
|
writeln!(dst, "#![warn({})] //~ ERROR: lint `{}`", lint.old_name, lint.old_name).unwrap();
|
|
}
|
|
}
|
|
dst.push_str("\nfn main() {}\n");
|
|
UpdateStatus::from_changed(src != dst)
|
|
},
|
|
);
|
|
for (crate_name, lints) in lints.iter().into_group_map_by(|&l| {
|
|
let Some(path::Component::Normal(name)) = l.path.components().next() else {
|
|
// All paths should start with `{crate_name}/src` when parsed from `find_lint_decls`
|
|
panic!("internal error: can't read crate name from path `{}`", l.path.display());
|
|
};
|
|
name
|
|
}) {
|
|
updater.update_file_checked(
|
|
"cargo dev update_lints",
|
|
update_mode,
|
|
Path::new(crate_name).join("src/lib.rs"),
|
|
&mut update_text_region_fn(
|
|
"// begin lints modules, do not remove this comment, it's used in `update_lints`\n",
|
|
"// end lints modules, do not remove this comment, it's used in `update_lints`",
|
|
|dst| {
|
|
for lint_mod in lints
|
|
.iter()
|
|
.filter(|l| !l.module.is_empty())
|
|
.map(|l| l.module.split_once("::").map_or(&*l.module, |x| x.0))
|
|
.sorted()
|
|
.dedup()
|
|
{
|
|
writeln!(dst, "mod {lint_mod};").unwrap();
|
|
}
|
|
},
|
|
),
|
|
);
|
|
updater.update_file_checked(
|
|
"cargo dev update_lints",
|
|
update_mode,
|
|
Path::new(crate_name).join("src/declared_lints.rs"),
|
|
&mut |_, src, dst| {
|
|
dst.push_str(GENERATED_FILE_COMMENT);
|
|
dst.push_str("pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[\n");
|
|
for (module_path, lint_name) in lints.iter().map(|l| (&l.module, l.name.to_uppercase())).sorted() {
|
|
if module_path.is_empty() {
|
|
writeln!(dst, " crate::{lint_name}_INFO,").unwrap();
|
|
} else {
|
|
writeln!(dst, " crate::{module_path}::{lint_name}_INFO,").unwrap();
|
|
}
|
|
}
|
|
dst.push_str("];\n");
|
|
UpdateStatus::from_changed(src != dst)
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
fn round_to_fifty(count: usize) -> usize {
|
|
count / 50 * 50
|
|
}
|
|
|
|
/// Lint data parsed from the Clippy source code.
|
|
#[derive(PartialEq, Eq, Debug)]
|
|
pub struct Lint {
|
|
pub name: String,
|
|
pub group: String,
|
|
pub module: String,
|
|
pub path: PathBuf,
|
|
pub declaration_range: Range<usize>,
|
|
}
|
|
|
|
pub struct DeprecatedLint {
|
|
pub name: String,
|
|
pub reason: String,
|
|
pub version: String,
|
|
}
|
|
|
|
pub struct RenamedLint {
|
|
pub old_name: String,
|
|
pub new_name: String,
|
|
pub version: String,
|
|
}
|
|
|
|
/// Finds all lint declarations (`declare_clippy_lint!`)
|
|
#[must_use]
|
|
pub fn find_lint_decls() -> Vec<Lint> {
|
|
let mut lints = Vec::with_capacity(1000);
|
|
let mut contents = String::new();
|
|
for e in expect_action(fs::read_dir("."), ErrAction::Read, ".") {
|
|
let e = expect_action(e, ErrAction::Read, ".");
|
|
if !expect_action(e.file_type(), ErrAction::Read, ".").is_dir() {
|
|
continue;
|
|
}
|
|
let Ok(mut name) = e.file_name().into_string() else {
|
|
continue;
|
|
};
|
|
if name.starts_with("clippy_lints") && name != "clippy_lints_internal" {
|
|
name.push_str("/src");
|
|
for (file, module) in read_src_with_module(name.as_ref()) {
|
|
parse_clippy_lint_decls(
|
|
file.path(),
|
|
File::open_read_to_cleared_string(file.path(), &mut contents),
|
|
&module,
|
|
&mut lints,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
lints.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
|
|
lints
|
|
}
|
|
|
|
/// Reads the source files from the given root directory
|
|
fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator<Item = (DirEntry, String)> {
|
|
WalkDir::new(src_root).into_iter().filter_map(move |e| {
|
|
let e = expect_action(e, ErrAction::Read, src_root);
|
|
let path = e.path().as_os_str().as_encoded_bytes();
|
|
if let Some(path) = path.strip_suffix(b".rs")
|
|
&& let Some(path) = path.get(src_root.as_os_str().len() + 1..)
|
|
{
|
|
if path == b"lib" {
|
|
Some((e, String::new()))
|
|
} else {
|
|
let path = if let Some(path) = path.strip_suffix(b"mod")
|
|
&& let Some(path) = path.strip_suffix(b"/").or_else(|| path.strip_suffix(b"\\"))
|
|
{
|
|
path
|
|
} else {
|
|
path
|
|
};
|
|
if let Ok(path) = str::from_utf8(path) {
|
|
let path = path.replace(['/', '\\'], "::");
|
|
Some((e, path))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Parse a source file looking for `declare_clippy_lint` macro invocations.
|
|
fn parse_clippy_lint_decls(path: &Path, contents: &str, module: &str, lints: &mut Vec<Lint>) {
|
|
#[allow(clippy::enum_glob_use)]
|
|
use Token::*;
|
|
#[rustfmt::skip]
|
|
static DECL_TOKENS: &[Token<'_>] = &[
|
|
// !{ /// docs
|
|
Bang, OpenBrace, AnyComment,
|
|
// #[clippy::version = "version"]
|
|
Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, LitStr, CloseBracket,
|
|
// pub NAME, GROUP,
|
|
Ident("pub"), CaptureIdent, Comma, AnyComment, CaptureIdent, Comma,
|
|
];
|
|
|
|
let mut searcher = RustSearcher::new(contents);
|
|
while searcher.find_token(Ident("declare_clippy_lint")) {
|
|
let start = searcher.pos() as usize - "declare_clippy_lint".len();
|
|
let (mut name, mut group) = ("", "");
|
|
if searcher.match_tokens(DECL_TOKENS, &mut [&mut name, &mut group]) && searcher.find_token(CloseBrace) {
|
|
lints.push(Lint {
|
|
name: name.to_lowercase(),
|
|
group: group.into(),
|
|
module: module.into(),
|
|
path: path.into(),
|
|
declaration_range: start..searcher.pos() as usize,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn read_deprecated_lints() -> (Vec<DeprecatedLint>, Vec<RenamedLint>) {
|
|
#[allow(clippy::enum_glob_use)]
|
|
use Token::*;
|
|
#[rustfmt::skip]
|
|
static DECL_TOKENS: &[Token<'_>] = &[
|
|
// #[clippy::version = "version"]
|
|
Pound, OpenBracket, Ident("clippy"), DoubleColon, Ident("version"), Eq, CaptureLitStr, CloseBracket,
|
|
// ("first", "second"),
|
|
OpenParen, CaptureLitStr, Comma, CaptureLitStr, CloseParen, Comma,
|
|
];
|
|
#[rustfmt::skip]
|
|
static DEPRECATED_TOKENS: &[Token<'_>] = &[
|
|
// !{ DEPRECATED(DEPRECATED_VERSION) = [
|
|
Bang, OpenBrace, Ident("DEPRECATED"), OpenParen, Ident("DEPRECATED_VERSION"), CloseParen, Eq, OpenBracket,
|
|
];
|
|
#[rustfmt::skip]
|
|
static RENAMED_TOKENS: &[Token<'_>] = &[
|
|
// !{ RENAMED(RENAMED_VERSION) = [
|
|
Bang, OpenBrace, Ident("RENAMED"), OpenParen, Ident("RENAMED_VERSION"), CloseParen, Eq, OpenBracket,
|
|
];
|
|
|
|
let path = "clippy_lints/src/deprecated_lints.rs";
|
|
let mut deprecated = Vec::with_capacity(30);
|
|
let mut renamed = Vec::with_capacity(80);
|
|
let mut contents = String::new();
|
|
File::open_read_to_cleared_string(path, &mut contents);
|
|
|
|
let mut searcher = RustSearcher::new(&contents);
|
|
|
|
// First instance is the macro definition.
|
|
assert!(
|
|
searcher.find_token(Ident("declare_with_version")),
|
|
"error reading deprecated lints"
|
|
);
|
|
|
|
if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(DEPRECATED_TOKENS, &mut []) {
|
|
let mut version = "";
|
|
let mut name = "";
|
|
let mut reason = "";
|
|
while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut name, &mut reason]) {
|
|
deprecated.push(DeprecatedLint {
|
|
name: parse_str_single_line(path.as_ref(), name),
|
|
reason: parse_str_single_line(path.as_ref(), reason),
|
|
version: parse_str_single_line(path.as_ref(), version),
|
|
});
|
|
}
|
|
} else {
|
|
panic!("error reading deprecated lints");
|
|
}
|
|
|
|
if searcher.find_token(Ident("declare_with_version")) && searcher.match_tokens(RENAMED_TOKENS, &mut []) {
|
|
let mut version = "";
|
|
let mut old_name = "";
|
|
let mut new_name = "";
|
|
while searcher.match_tokens(DECL_TOKENS, &mut [&mut version, &mut old_name, &mut new_name]) {
|
|
renamed.push(RenamedLint {
|
|
old_name: parse_str_single_line(path.as_ref(), old_name),
|
|
new_name: parse_str_single_line(path.as_ref(), new_name),
|
|
version: parse_str_single_line(path.as_ref(), version),
|
|
});
|
|
}
|
|
} else {
|
|
panic!("error reading renamed lints");
|
|
}
|
|
|
|
deprecated.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
|
|
renamed.sort_by(|lhs, rhs| lhs.old_name.cmp(&rhs.old_name));
|
|
(deprecated, renamed)
|
|
}
|
|
|
|
/// Removes the line splices and surrounding quotes from a string literal
|
|
fn parse_str_lit(s: &str) -> String {
|
|
let s = s.strip_prefix("r").unwrap_or(s).trim_matches('#');
|
|
let s = s
|
|
.strip_prefix('"')
|
|
.and_then(|s| s.strip_suffix('"'))
|
|
.unwrap_or_else(|| panic!("expected quoted string, found `{s}`"));
|
|
let mut res = String::with_capacity(s.len());
|
|
rustc_literal_escaper::unescape_str(s, &mut |_, ch| {
|
|
if let Ok(ch) = ch {
|
|
res.push(ch);
|
|
}
|
|
});
|
|
res
|
|
}
|
|
|
|
fn parse_str_single_line(path: &Path, s: &str) -> String {
|
|
let value = parse_str_lit(s);
|
|
assert!(
|
|
!value.contains('\n'),
|
|
"error parsing `{}`: `{s}` should be a single line string",
|
|
path.display(),
|
|
);
|
|
value
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_clippy_lint_decls() {
|
|
static CONTENTS: &str = r#"
|
|
declare_clippy_lint! {
|
|
#[clippy::version = "Hello Clippy!"]
|
|
pub PTR_ARG,
|
|
style,
|
|
"really long \
|
|
text"
|
|
}
|
|
|
|
declare_clippy_lint!{
|
|
#[clippy::version = "Test version"]
|
|
pub DOC_MARKDOWN,
|
|
pedantic,
|
|
"single line"
|
|
}
|
|
"#;
|
|
let mut result = Vec::new();
|
|
parse_clippy_lint_decls("".as_ref(), CONTENTS, "module_name", &mut result);
|
|
for r in &mut result {
|
|
r.declaration_range = Range::default();
|
|
}
|
|
|
|
let expected = vec![
|
|
Lint {
|
|
name: "ptr_arg".into(),
|
|
group: "style".into(),
|
|
module: "module_name".into(),
|
|
path: PathBuf::new(),
|
|
declaration_range: Range::default(),
|
|
},
|
|
Lint {
|
|
name: "doc_markdown".into(),
|
|
group: "pedantic".into(),
|
|
module: "module_name".into(),
|
|
path: PathBuf::new(),
|
|
declaration_range: Range::default(),
|
|
},
|
|
];
|
|
assert_eq!(expected, result);
|
|
}
|
|
}
|