use std::fs;
use std::path::{Path, PathBuf};
const QUALIFIED_PATH_RULES: &[(&str, &str)] = &[
("anyhow::anyhow!", "This adds the crate name to a macro whose short name is already clear. Import it and use `anyhow!`."),
("std::cmp::Ordering", "`Ordering` provides the useful context, while the standard-library prefix does not. Import it unless another `Ordering` in this scope makes qualification genuinely helpful."),
("std::path::Path", "`Path` is already descriptive at the use site. Import it instead of repeating the standard-library path."),
("std::path::PathBuf", "`PathBuf` is already descriptive at the use site. Import it instead of repeating the standard-library path."),
("tokio::sync::mpsc::UnboundedReceiver", "The `mpsc` namespace distinguishes this from other channel types. Keep `mpsc`, but do not repeat the whole `tokio::sync` path."),
("tokio::sync::mpsc::Receiver", "The `mpsc` namespace distinguishes this from other `Receiver` types. Keep `mpsc`, but do not repeat the whole `tokio::sync` path."),
("tokio::sync::mpsc::Sender", "The `mpsc` namespace distinguishes this from other `Sender` types. Keep `mpsc`, but do not repeat the whole `tokio::sync` path."),
("tokio::sync::oneshot", "The `oneshot` namespace is enough to distinguish this channel family. Import `oneshot`; the `tokio::sync` prefix adds no useful context."),
("tokio::sync::broadcast::channel", "The `broadcast` namespace is enough context at this call site. Import `broadcast` instead of repeating `tokio::sync`."),
("tokio::sync::watch::channel", "The `watch` namespace is enough context at this call site. Import `watch` instead of repeating `tokio::sync`."),
("base64::engine::general_purpose::STANDARD", "The base64 namespace is not useful at every use of this well-known constant. Import `STANDARD`."),
("std::collections::HashMap", "`HashMap` is clear without its standard-library prefix. Import it; long qualification makes ordinary collection code noisier."),
("std::sync::Arc", "`Arc` is clear without its standard-library prefix. Import it; qualify it only when distinguishing competing `Arc` types is useful."),
("std::sync::Mutex", "This is a judgment call: qualification can distinguish standard and Tokio mutexes, but here the path is noise. Import `Mutex` when the surrounding context is unambiguous."),
("std::sync::RwLock", "This is a judgment call: qualification can distinguish standard and Tokio locks, but here the path is noise. Import `RwLock` when the surrounding context is unambiguous."),
("std::sync::atomic::AtomicU64", "`AtomicU64` is already specific and does not need the full standard-library path. Import it."),
("std::sync::atomic::Ordering", "`Ordering` is the useful part of this path. Import it unless another `Ordering` in the scope makes qualification helpful."),
];
pub struct LintResult {
pub errors: usize,
pub warnings: usize,
}
pub fn lint(root: &Path) -> LintResult {
let generic_path = regex::Regex::new(r"(?:^|[^A-Za-z0-9_:])(?:[A-Za-z_][A-Za-z0-9_]*::){3,}[A-Za-z_][A-Za-z0-9_]*").expect("valid qualified path regex");
let mut errors = 0;
let mut warnings = 0;
for path in rust_files(root) {
let Ok(text) = fs::read_to_string(&path) else {
continue;
};
let relative = path.strip_prefix(root).unwrap_or(&path).display();
for line_number in import_spacing_violations(&text) {
println!(
"{relative}:{line_number}: error: blank line between imports"
);
println!(" Why: Keep all imports in one contiguous group.");
println!(" Fix: remove the blank line between the imports.");
errors += 1;
}
for (line_number, line) in text.lines().enumerate() {
let stripped = line.trim_start();
if stripped.starts_with("//")
|| stripped.starts_with("use ")
|| stripped.starts_with("pub use ")
{
continue;
}
let mut exact_match = false;
for (pattern, explanation) in QUALIFIED_PATH_RULES {
if line.contains(pattern) {
println!(
"{relative}:{}: error: {pattern}",
line_number + 1
);
println!(" Why: {explanation}");
println!(" Fix: import the relevant type, macro, or short namespace");
errors += 1;
exact_match = true;
}
}
if !exact_match {
for matched in generic_path.find_iter(line) {
println!(
"{relative}:{}: warning: {}",
line_number + 1,
matched.as_str().trim()
);
println!(" Why: This qualified path may be longer than necessary. Consider importing part of it.");
warnings += 1;
}
}
}
}
LintResult { errors, warnings }
}
fn rust_files(root: &Path) -> impl Iterator<Item = PathBuf> {
let mut files = Vec::new();
collect_rust_files(root, root, &mut files);
files.into_iter()
}
fn collect_rust_files(
root: &Path,
directory: &Path,
files: &mut Vec<PathBuf>,
) {
let Ok(entries) = fs::read_dir(directory) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let relative = path.strip_prefix(root).unwrap_or(&path);
if relative.components().any(|component| {
matches!(
component.as_os_str().to_str(),
Some(".git" | "target" | "tmp" | "web" | "scripts")
)
}) {
continue;
}
if path.is_dir() {
collect_rust_files(root, &path, files);
} else if path
.extension()
.is_some_and(|extension| extension == "rs")
{
files.push(path);
}
}
}
fn is_import_start(line: &str) -> bool {
let line = line.trim_start();
line.starts_with("use ")
|| line.starts_with("pub use ")
|| line.starts_with("pub(crate) use ")
|| line.starts_with("pub(super) use ")
}
fn import_spacing_violations(text: &str) -> Vec<usize> {
let lines: Vec<_> = text.lines().collect();
let mut violations = Vec::new();
let mut import_group = false;
let mut import_statement_open = false;
for (index, line) in lines.iter().enumerate() {
if line.trim().is_empty() {
if import_group
&& !import_statement_open
&& lines[index + 1..]
.iter()
.find(|next| !next.trim().is_empty())
.is_some_and(|next| is_import_start(next))
{
violations.push(index + 1);
}
continue;
}
if !import_statement_open && !is_import_start(line) {
import_group = false;
continue;
}
import_group = true;
import_statement_open = !line.trim_end().ends_with(';');
}
violations
}
#[cfg(test)]
mod tests {
use super::import_spacing_violations;
#[test]
fn detects_blank_lines_between_import_groups() {
let source = "use std::sync::Arc;\n\nuse anyhow::Result;\n";
assert_eq!(import_spacing_violations(source), vec![2]);
}
#[test]
fn ignores_blank_lines_after_imports() {
let source = "use std::sync::Arc;\n\nfn main() {}\n";
assert!(import_spacing_violations(source).is_empty());
}
#[test]
fn handles_multiline_imports() {
let source = "use crate::{\n Foo,\n};\n\nuse std::sync::Arc;\n";
assert_eq!(import_spacing_violations(source), vec![4]);
}
}