Prepare to split lints into multiple crates
* Move `declare_clippy_lint` to it's own crate * Move lint/group registration into the driver * Make `dev update_lints` handle multiple lint crates
This commit is contained in:
parent
19c1c70905
commit
acd8810e77
19 changed files with 485 additions and 436 deletions
|
|
@ -24,6 +24,7 @@ path = "src/driver.rs"
|
|||
clippy_config = { path = "clippy_config" }
|
||||
clippy_lints = { path = "clippy_lints" }
|
||||
clippy_utils = { path = "clippy_utils" }
|
||||
declare_clippy_lint = { path = "declare_clippy_lint" }
|
||||
rustc_tools_util = { path = "rustc_tools_util", version = "0.4.2" }
|
||||
clippy_lints_internal = { path = "clippy_lints_internal", optional = true }
|
||||
tempfile = { version = "3.20", optional = true }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ static CARGO_TOML_FILES: &[&str] = &[
|
|||
"clippy_config/Cargo.toml",
|
||||
"clippy_lints/Cargo.toml",
|
||||
"clippy_utils/Cargo.toml",
|
||||
"declare_clippy_lint/Cargo.toml",
|
||||
"Cargo.toml",
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ use crate::utils::{
|
|||
use itertools::Itertools;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write;
|
||||
use std::fs;
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
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\
|
||||
|
|
@ -36,123 +37,164 @@ pub fn generate_lint_files(
|
|||
deprecated: &[DeprecatedLint],
|
||||
renamed: &[RenamedLint],
|
||||
) {
|
||||
FileUpdater::default().update_files_checked(
|
||||
let mut updater = FileUpdater::default();
|
||||
updater.update_file_checked(
|
||||
"cargo dev update_lints",
|
||||
update_mode,
|
||||
&mut [
|
||||
(
|
||||
"README.md",
|
||||
&mut update_text_region_fn("[There are over ", " lints included in this crate!]", |dst| {
|
||||
write!(dst, "{}", round_to_fifty(lints.len())).unwrap();
|
||||
}),
|
||||
),
|
||||
(
|
||||
"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();
|
||||
}),
|
||||
),
|
||||
(
|
||||
"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();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
(
|
||||
"clippy_lints/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().map(|l| &l.module).sorted().dedup() {
|
||||
writeln!(dst, "mod {lint_mod};").unwrap();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
("clippy_lints/src/declared_lints.rs", &mut |_, src, dst| {
|
||||
dst.push_str(GENERATED_FILE_COMMENT);
|
||||
dst.push_str("pub static LINTS: &[&crate::LintInfo] = &[\n");
|
||||
for (module_name, lint_name) in lints.iter().map(|l| (&l.module, l.name.to_uppercase())).sorted() {
|
||||
writeln!(dst, " crate::{module_name}::{lint_name}_INFO,").unwrap();
|
||||
"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();
|
||||
}
|
||||
dst.push_str("];\n");
|
||||
UpdateStatus::from_changed(src != dst)
|
||||
}),
|
||||
("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\
|
||||
},
|
||||
),
|
||||
);
|
||||
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)
|
||||
}),
|
||||
("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)
|
||||
}),
|
||||
("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 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 {
|
||||
|
|
@ -186,13 +228,25 @@ pub struct RenamedLint {
|
|||
pub fn find_lint_decls() -> Vec<Lint> {
|
||||
let mut lints = Vec::with_capacity(1000);
|
||||
let mut contents = String::new();
|
||||
for (file, module) in read_src_with_module("clippy_lints/src".as_ref()) {
|
||||
parse_clippy_lint_decls(
|
||||
file.path(),
|
||||
File::open_read_to_cleared_string(file.path(), &mut contents),
|
||||
&module,
|
||||
&mut lints,
|
||||
);
|
||||
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
|
||||
|
|
@ -204,7 +258,7 @@ fn read_src_with_module(src_root: &Path) -> impl use<'_> + Iterator<Item = (DirE
|
|||
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("clippy_lints/src/".len()..)
|
||||
&& let Some(path) = path.get(src_root.as_os_str().len() + 1..)
|
||||
{
|
||||
if path == b"lib" {
|
||||
Some((e, String::new()))
|
||||
|
|
|
|||
|
|
@ -383,21 +383,6 @@ impl FileUpdater {
|
|||
self.update_file_checked_inner(tool, mode, path.as_ref(), update);
|
||||
}
|
||||
|
||||
#[expect(clippy::type_complexity)]
|
||||
pub fn update_files_checked(
|
||||
&mut self,
|
||||
tool: &str,
|
||||
mode: UpdateMode,
|
||||
files: &mut [(
|
||||
impl AsRef<Path>,
|
||||
&mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus,
|
||||
)],
|
||||
) {
|
||||
for (path, update) in files {
|
||||
self.update_file_checked_inner(tool, mode, path.as_ref(), update);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_file(
|
||||
&mut self,
|
||||
path: impl AsRef<Path>,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ arrayvec = { version = "0.7", default-features = false }
|
|||
cargo_metadata = "0.18"
|
||||
clippy_config = { path = "../clippy_config" }
|
||||
clippy_utils = { path = "../clippy_utils" }
|
||||
declare_clippy_lint = { path = "../declare_clippy_lint" }
|
||||
itertools = "0.12"
|
||||
quine-mc_cluskey = "0.2"
|
||||
regex-syntax = "0.8"
|
||||
|
|
|
|||
|
|
@ -1,168 +0,0 @@
|
|||
#[macro_export]
|
||||
#[allow(clippy::crate_in_macro_def)]
|
||||
macro_rules! declare_clippy_lint {
|
||||
(@
|
||||
$(#[doc = $lit:literal])*
|
||||
pub $lint_name:ident,
|
||||
$level:ident,
|
||||
$lintcategory:expr,
|
||||
$desc:literal,
|
||||
$version_expr:expr,
|
||||
$version_lit:literal
|
||||
$(, $eval_always: literal)?
|
||||
) => {
|
||||
rustc_session::declare_tool_lint! {
|
||||
$(#[doc = $lit])*
|
||||
#[clippy::version = $version_lit]
|
||||
pub clippy::$lint_name,
|
||||
$level,
|
||||
$desc,
|
||||
report_in_external_macro:true
|
||||
$(, @eval_always = $eval_always)?
|
||||
}
|
||||
|
||||
pub(crate) static ${concat($lint_name, _INFO)}: &'static crate::LintInfo = &crate::LintInfo {
|
||||
lint: &$lint_name,
|
||||
category: $lintcategory,
|
||||
explanation: concat!($($lit,"\n",)*),
|
||||
location: concat!(file!(), "#L", line!()),
|
||||
version: $version_expr
|
||||
};
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
restriction,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Allow, crate::LintCategory::Restriction, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
style,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Warn, crate::LintCategory::Style, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
correctness,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Deny, crate::LintCategory::Correctness, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
perf,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Warn, crate::LintCategory::Perf, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
complexity,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Warn, crate::LintCategory::Complexity, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
suspicious,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Warn, crate::LintCategory::Suspicious, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
nursery,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Allow, crate::LintCategory::Nursery, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
pedantic,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Allow, crate::LintCategory::Pedantic, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[doc = $lit:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
pub $lint_name:ident,
|
||||
cargo,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always: literal)?
|
||||
) => {
|
||||
declare_clippy_lint! {@
|
||||
$(#[doc = $lit])*
|
||||
pub $lint_name, Allow, crate::LintCategory::Cargo, $desc,
|
||||
Some($version), $version
|
||||
$(, $eval_always)?
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
// Use that command to update this file and do not edit by hand.
|
||||
// Manual edits will be overwritten.
|
||||
|
||||
pub static LINTS: &[&crate::LintInfo] = &[
|
||||
pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
|
||||
crate::absolute_paths::ABSOLUTE_PATHS_INFO,
|
||||
crate::almost_complete_range::ALMOST_COMPLETE_RANGE_INFO,
|
||||
crate::approx_const::APPROX_CONSTANT_INFO,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ use clippy_utils::{
|
|||
eq_expr_value, get_parent_expr, higher, is_in_const_context, is_inherent_method_call, is_no_std_crate,
|
||||
numeric_literal, peel_blocks, sugg, sym,
|
||||
};
|
||||
use rustc_ast::ast;
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_hir::{BinOpKind, Expr, ExprKind, PathSegment, UnOp};
|
||||
use rustc_lint::{LateContext, LateLintPass};
|
||||
use rustc_middle::ty;
|
||||
use rustc_session::declare_lint_pass;
|
||||
use rustc_span::source_map::Spanned;
|
||||
|
||||
use rustc_ast::ast;
|
||||
use std::f32::consts as f32_consts;
|
||||
use std::f64::consts as f64_consts;
|
||||
use sugg::Sugg;
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ extern crate smallvec;
|
|||
extern crate thin_vec;
|
||||
|
||||
#[macro_use]
|
||||
mod declare_clippy_lint;
|
||||
extern crate clippy_utils;
|
||||
|
||||
#[macro_use]
|
||||
extern crate clippy_utils;
|
||||
extern crate declare_clippy_lint;
|
||||
|
||||
mod utils;
|
||||
|
||||
|
|
@ -411,108 +411,9 @@ mod zombie_processes;
|
|||
use clippy_config::{Conf, get_configuration_metadata, sanitize_explanation};
|
||||
use clippy_utils::macros::FormatArgsStorage;
|
||||
use rustc_data_structures::fx::FxHashSet;
|
||||
use rustc_lint::{Lint, LintId};
|
||||
use rustc_lint::Lint;
|
||||
use utils::attr_collector::{AttrCollector, AttrStorage};
|
||||
|
||||
#[derive(Default)]
|
||||
struct RegistrationGroups {
|
||||
all: Vec<LintId>,
|
||||
cargo: Vec<LintId>,
|
||||
complexity: Vec<LintId>,
|
||||
correctness: Vec<LintId>,
|
||||
nursery: Vec<LintId>,
|
||||
pedantic: Vec<LintId>,
|
||||
perf: Vec<LintId>,
|
||||
restriction: Vec<LintId>,
|
||||
style: Vec<LintId>,
|
||||
suspicious: Vec<LintId>,
|
||||
}
|
||||
|
||||
impl RegistrationGroups {
|
||||
#[rustfmt::skip]
|
||||
fn register(self, store: &mut rustc_lint::LintStore) {
|
||||
store.register_group(true, "clippy::all", Some("clippy_all"), self.all);
|
||||
store.register_group(true, "clippy::cargo", Some("clippy_cargo"), self.cargo);
|
||||
store.register_group(true, "clippy::complexity", Some("clippy_complexity"), self.complexity);
|
||||
store.register_group(true, "clippy::correctness", Some("clippy_correctness"), self.correctness);
|
||||
store.register_group(true, "clippy::nursery", Some("clippy_nursery"), self.nursery);
|
||||
store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), self.pedantic);
|
||||
store.register_group(true, "clippy::perf", Some("clippy_perf"), self.perf);
|
||||
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), self.restriction);
|
||||
store.register_group(true, "clippy::style", Some("clippy_style"), self.style);
|
||||
store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), self.suspicious);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum LintCategory {
|
||||
Cargo,
|
||||
Complexity,
|
||||
Correctness,
|
||||
Nursery,
|
||||
Pedantic,
|
||||
Perf,
|
||||
Restriction,
|
||||
Style,
|
||||
Suspicious,
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use LintCategory::*;
|
||||
|
||||
impl LintCategory {
|
||||
fn is_all(self) -> bool {
|
||||
matches!(self, Correctness | Suspicious | Style | Complexity | Perf)
|
||||
}
|
||||
|
||||
fn group(self, groups: &mut RegistrationGroups) -> &mut Vec<LintId> {
|
||||
match self {
|
||||
Cargo => &mut groups.cargo,
|
||||
Complexity => &mut groups.complexity,
|
||||
Correctness => &mut groups.correctness,
|
||||
Nursery => &mut groups.nursery,
|
||||
Pedantic => &mut groups.pedantic,
|
||||
Perf => &mut groups.perf,
|
||||
Restriction => &mut groups.restriction,
|
||||
Style => &mut groups.style,
|
||||
Suspicious => &mut groups.suspicious,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LintInfo {
|
||||
/// Double reference to maintain pointer equality
|
||||
pub lint: &'static &'static Lint,
|
||||
category: LintCategory,
|
||||
pub explanation: &'static str,
|
||||
/// e.g. `clippy_lints/src/absolute_paths.rs#43`
|
||||
pub location: &'static str,
|
||||
pub version: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl LintInfo {
|
||||
/// Returns the lint name in lowercase without the `clippy::` prefix
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn name_lower(&self) -> String {
|
||||
self.lint.name.strip_prefix("clippy::").unwrap().to_ascii_lowercase()
|
||||
}
|
||||
|
||||
/// Returns the name of the lint's category in lowercase (`style`, `pedantic`)
|
||||
pub fn category_str(&self) -> &'static str {
|
||||
match self.category {
|
||||
Cargo => "cargo",
|
||||
Complexity => "complexity",
|
||||
Correctness => "correctness",
|
||||
Nursery => "nursery",
|
||||
Pedantic => "pedantic",
|
||||
Perf => "perf",
|
||||
Restriction => "restriction",
|
||||
Style => "style",
|
||||
Suspicious => "suspicious",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explain(name: &str) -> i32 {
|
||||
let target = format!("clippy::{}", name.to_ascii_uppercase());
|
||||
|
||||
|
|
@ -535,30 +436,11 @@ pub fn explain(name: &str) -> i32 {
|
|||
}
|
||||
}
|
||||
|
||||
fn register_categories(store: &mut rustc_lint::LintStore) {
|
||||
let mut groups = RegistrationGroups::default();
|
||||
|
||||
for LintInfo { lint, category, .. } in declared_lints::LINTS {
|
||||
if category.is_all() {
|
||||
groups.all.push(LintId::of(lint));
|
||||
}
|
||||
|
||||
category.group(&mut groups).push(LintId::of(lint));
|
||||
}
|
||||
|
||||
let lints: Vec<&'static Lint> = declared_lints::LINTS.iter().map(|info| *info.lint).collect();
|
||||
|
||||
store.register_lints(&lints);
|
||||
groups.register(store);
|
||||
}
|
||||
|
||||
/// Register all lints and lint groups with the rustc lint store
|
||||
///
|
||||
/// Used in `./src/driver.rs`.
|
||||
#[expect(clippy::too_many_lines)]
|
||||
pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
|
||||
register_categories(store);
|
||||
|
||||
pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
|
||||
for (old_name, new_name) in deprecated_lints::RENAMED {
|
||||
store.register_renamed(old_name, new_name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ use rustc_errors::Applicability;
|
|||
use rustc_hir::def::{CtorOf, DefKind, Res};
|
||||
use rustc_hir::{Arm, Expr, ExprKind, HirId, MatchSource, Pat, PatExpr, PatExprKind, PatKind, QPath, Stmt, StmtKind};
|
||||
use rustc_lint::{LateContext, LintContext};
|
||||
|
||||
use rustc_span::Span;
|
||||
use rustc_span::symbol::{Symbol, sym};
|
||||
use std::slice;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ use clippy_utils::source::{snippet, snippet_with_applicability};
|
|||
use rustc_ast::ast;
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_hir::{Expr, ExprKind};
|
||||
|
||||
use rustc_lint::{LateContext, LateLintPass};
|
||||
use rustc_session::declare_lint_pass;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use clippy_utils::diagnostics::span_lint_and_then;
|
||||
|
||||
use clippy_utils::macros::span_is_local;
|
||||
use rustc_hir::{Expr, ExprKind, MatchSource};
|
||||
use rustc_lint::{LateContext, LateLintPass};
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ use clippy_utils::higher::{VecInitKind, get_vec_init_kind};
|
|||
use clippy_utils::source::snippet;
|
||||
use clippy_utils::{get_enclosing_block, sym};
|
||||
|
||||
use hir::{Expr, ExprKind, HirId, LetStmt, PatKind, PathSegment, QPath, StmtKind};
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_hir as hir;
|
||||
use rustc_hir::def::Res;
|
||||
use rustc_hir::intravisit::{Visitor, walk_expr};
|
||||
use rustc_hir::{self as hir, Expr, ExprKind, HirId, LetStmt, PatKind, PathSegment, QPath, StmtKind};
|
||||
use rustc_lint::{LateContext, LateLintPass};
|
||||
use rustc_session::declare_lint_pass;
|
||||
|
||||
|
|
|
|||
10
declare_clippy_lint/Cargo.toml
Normal file
10
declare_clippy_lint/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "declare_clippy_lint"
|
||||
version = "0.1.89"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/rust-lang/rust-clippy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[package.metadata.rust-analyzer]
|
||||
# This crate uses #[feature(rustc_private)]
|
||||
rustc_private = true
|
||||
280
declare_clippy_lint/src/lib.rs
Normal file
280
declare_clippy_lint/src/lib.rs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
#![feature(macro_metavar_expr_concat, rustc_private)]
|
||||
|
||||
extern crate rustc_lint;
|
||||
|
||||
use rustc_lint::{Lint, LintId, LintStore};
|
||||
|
||||
// Needed by `declare_clippy_lint!`.
|
||||
pub extern crate rustc_session;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LintListBuilder {
|
||||
lints: Vec<&'static Lint>,
|
||||
all: Vec<LintId>,
|
||||
cargo: Vec<LintId>,
|
||||
complexity: Vec<LintId>,
|
||||
correctness: Vec<LintId>,
|
||||
nursery: Vec<LintId>,
|
||||
pedantic: Vec<LintId>,
|
||||
perf: Vec<LintId>,
|
||||
restriction: Vec<LintId>,
|
||||
style: Vec<LintId>,
|
||||
suspicious: Vec<LintId>,
|
||||
}
|
||||
impl LintListBuilder {
|
||||
pub fn insert(&mut self, lints: &[&LintInfo]) {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use LintCategory::*;
|
||||
|
||||
self.lints.extend(lints.iter().map(|&x| x.lint));
|
||||
for &&LintInfo { lint, category, .. } in lints {
|
||||
let (all, cat) = match category {
|
||||
Complexity => (Some(&mut self.all), &mut self.complexity),
|
||||
Correctness => (Some(&mut self.all), &mut self.correctness),
|
||||
Perf => (Some(&mut self.all), &mut self.perf),
|
||||
Style => (Some(&mut self.all), &mut self.style),
|
||||
Suspicious => (Some(&mut self.all), &mut self.suspicious),
|
||||
Cargo => (None, &mut self.cargo),
|
||||
Nursery => (None, &mut self.nursery),
|
||||
Pedantic => (None, &mut self.pedantic),
|
||||
Restriction => (None, &mut self.restriction),
|
||||
};
|
||||
if let Some(all) = all {
|
||||
all.push(LintId::of(lint));
|
||||
}
|
||||
cat.push(LintId::of(lint));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(self, store: &mut LintStore) {
|
||||
store.register_lints(&self.lints);
|
||||
store.register_group(true, "clippy::all", Some("clippy_all"), self.all);
|
||||
store.register_group(true, "clippy::cargo", Some("clippy_cargo"), self.cargo);
|
||||
store.register_group(true, "clippy::complexity", Some("clippy_complexity"), self.complexity);
|
||||
store.register_group(
|
||||
true,
|
||||
"clippy::correctness",
|
||||
Some("clippy_correctness"),
|
||||
self.correctness,
|
||||
);
|
||||
store.register_group(true, "clippy::nursery", Some("clippy_nursery"), self.nursery);
|
||||
store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), self.pedantic);
|
||||
store.register_group(true, "clippy::perf", Some("clippy_perf"), self.perf);
|
||||
store.register_group(
|
||||
true,
|
||||
"clippy::restriction",
|
||||
Some("clippy_restriction"),
|
||||
self.restriction,
|
||||
);
|
||||
store.register_group(true, "clippy::style", Some("clippy_style"), self.style);
|
||||
store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), self.suspicious);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum LintCategory {
|
||||
Cargo,
|
||||
Complexity,
|
||||
Correctness,
|
||||
Nursery,
|
||||
Pedantic,
|
||||
Perf,
|
||||
Restriction,
|
||||
Style,
|
||||
Suspicious,
|
||||
}
|
||||
impl LintCategory {
|
||||
#[must_use]
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Cargo => "cargo",
|
||||
Self::Complexity => "complexity",
|
||||
Self::Correctness => "correctness",
|
||||
Self::Nursery => "nursery",
|
||||
Self::Pedantic => "pedantic",
|
||||
Self::Perf => "perf",
|
||||
Self::Restriction => "restriction",
|
||||
Self::Style => "style",
|
||||
Self::Suspicious => "suspicious",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LintInfo {
|
||||
pub lint: &'static Lint,
|
||||
pub category: LintCategory,
|
||||
pub explanation: &'static str,
|
||||
/// e.g. `clippy_lints/src/absolute_paths.rs#43`
|
||||
pub location: &'static str,
|
||||
pub version: &'static str,
|
||||
}
|
||||
|
||||
impl LintInfo {
|
||||
/// Returns the lint name in lowercase without the `clippy::` prefix
|
||||
#[must_use]
|
||||
#[expect(clippy::missing_panics_doc)]
|
||||
pub fn name_lower(&self) -> String {
|
||||
self.lint.name.strip_prefix("clippy::").unwrap().to_ascii_lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! declare_clippy_lint_inner {
|
||||
(
|
||||
$(#[doc = $docs:literal])*
|
||||
#[clippy::version = $version:literal]
|
||||
$vis:vis $lint_name:ident,
|
||||
$level:ident,
|
||||
$category:ident,
|
||||
$desc:literal
|
||||
$(, @eval_always = $eval_always:literal)?
|
||||
) => {
|
||||
$crate::rustc_session::declare_tool_lint! {
|
||||
$(#[doc = $docs])*
|
||||
#[clippy::version = $version]
|
||||
$vis clippy::$lint_name,
|
||||
$level,
|
||||
$desc,
|
||||
report_in_external_macro:true
|
||||
$(, @eval_always = $eval_always)?
|
||||
}
|
||||
|
||||
pub(crate) static ${concat($lint_name, _INFO)}: &'static $crate::LintInfo = &$crate::LintInfo {
|
||||
lint: $lint_name,
|
||||
category: $crate::LintCategory::$category,
|
||||
explanation: concat!($($docs,"\n",)*),
|
||||
location: concat!(file!(), "#L", line!()),
|
||||
version: $version,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! declare_clippy_lint {
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
correctness,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Deny,
|
||||
Correctness,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
complexity,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Warn,
|
||||
Complexity,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
perf,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Warn,
|
||||
Perf,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
style,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Warn,
|
||||
Style,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
suspicious,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Warn,
|
||||
Suspicious,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
cargo,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Allow,
|
||||
Cargo,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
nursery,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Allow,
|
||||
Nursery,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
pedantic,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Allow,
|
||||
Pedantic,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
(
|
||||
$(#[$($meta:tt)*])*
|
||||
$vis:vis $lint_name:ident,
|
||||
restriction,
|
||||
$($rest:tt)*
|
||||
) => {
|
||||
$crate::declare_clippy_lint_inner! {
|
||||
$(#[$($meta)*])*
|
||||
$vis $lint_name,
|
||||
Allow,
|
||||
Restriction,
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ extern crate rustc_session;
|
|||
extern crate rustc_span;
|
||||
|
||||
use clippy_utils::sym;
|
||||
use declare_clippy_lint::LintListBuilder;
|
||||
use rustc_interface::interface;
|
||||
use rustc_session::EarlyDiagCtxt;
|
||||
use rustc_session::config::ErrorOutputType;
|
||||
|
|
@ -151,8 +152,13 @@ impl rustc_driver::Callbacks for ClippyCallbacks {
|
|||
(previous)(sess, lint_store);
|
||||
}
|
||||
|
||||
let mut list_builder = LintListBuilder::default();
|
||||
list_builder.insert(clippy_lints::declared_lints::LINTS);
|
||||
list_builder.register(lint_store);
|
||||
|
||||
let conf = clippy_config::Conf::read(sess, &conf_path);
|
||||
clippy_lints::register_lints(lint_store, conf);
|
||||
clippy_lints::register_lint_passes(lint_store, conf);
|
||||
|
||||
#[cfg(feature = "internal")]
|
||||
clippy_lints_internal::register_lints(lint_store);
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ use askama::filters::Safe;
|
|||
use cargo_metadata::Message;
|
||||
use cargo_metadata::diagnostic::{Applicability, Diagnostic};
|
||||
use clippy_config::ClippyConfiguration;
|
||||
use clippy_lints::LintInfo;
|
||||
use clippy_lints::declared_lints::LINTS;
|
||||
use clippy_lints::deprecated_lints::{DEPRECATED, DEPRECATED_VERSION, RENAMED};
|
||||
use declare_clippy_lint::LintInfo;
|
||||
use pulldown_cmark::{Options, Parser, html};
|
||||
use serde::Deserialize;
|
||||
use test_utils::IS_RUSTC_TEST_SUITE;
|
||||
|
|
@ -568,10 +568,10 @@ impl LintMetadata {
|
|||
Self {
|
||||
id: name,
|
||||
id_location: Some(lint.location),
|
||||
group: lint.category_str(),
|
||||
group: lint.category.name(),
|
||||
level: lint.lint.default_level.as_str(),
|
||||
docs,
|
||||
version: lint.version.unwrap(),
|
||||
version: lint.version,
|
||||
applicability,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ fn dogfood() {
|
|||
"clippy_lints",
|
||||
"clippy_utils",
|
||||
"clippy_config",
|
||||
"declare_clippy_lint",
|
||||
"lintcheck",
|
||||
"rustc_tools_util",
|
||||
] {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ fn consistent_clippy_crate_versions() {
|
|||
"clippy_config/Cargo.toml",
|
||||
"clippy_lints/Cargo.toml",
|
||||
"clippy_utils/Cargo.toml",
|
||||
"declare_clippy_lint/Cargo.toml",
|
||||
];
|
||||
|
||||
for path in paths {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue