use ignore::WalkBuilder;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::config::Config;
use crate::convention::Convention;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Violation {
pub path: PathBuf,
pub stem: String,
pub expected: Convention,
}
impl fmt::Display for Violation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"error[convention]: `{}` — stem `{}` does not follow {} convention",
self.path.display(),
self.stem,
self.expected,
)
}
}
#[must_use]
pub fn run(config: &Config, project_root: &Path) -> Vec<Violation> {
let violations = Arc::new(Mutex::new(Vec::new()));
for (ext, convention) in &config.rules {
let targets = search_dirs(config, ext, project_root);
for target in targets {
let violations_lock = Arc::clone(&violations);
let ext_owned = ext.clone();
let conv_owned = convention.clone();
let root_owned = project_root.to_path_buf();
if !target.exists() {
eprintln!(
"warning: directory `{}` for extension `.{ext_owned}` does not exist",
target.display()
);
continue;
}
let walker = WalkBuilder::new(target)
.standard_filters(true)
.hidden(false)
.parents(false)
.build_parallel();
walker.run(|| {
let v_inner = Arc::clone(&violations_lock);
let e_inner = ext_owned.clone();
let c_inner = conv_owned.clone();
let r_inner = root_owned.clone();
Box::new(move |result| {
if let Ok(entry) = result {
let path = entry.path();
let file_name = entry.file_name().to_string_lossy();
if (file_name == "target"
|| (file_name.starts_with('.') && entry.depth() > 0))
&& entry.file_type().is_some_and(|ft| ft.is_dir())
{
return ignore::WalkState::Skip;
}
if entry.file_type().is_some_and(|f| f.is_file())
&& path.extension().and_then(|s| s.to_str()) == Some(&e_inner)
{
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
if !c_inner.is_valid(stem) {
let rel_path =
path.strip_prefix(&r_inner).unwrap_or(path).to_path_buf();
v_inner.lock().expect("mutex poisoned").push(Violation {
path: rel_path,
stem: stem.to_owned(),
expected: c_inner.clone(),
});
}
}
}
}
ignore::WalkState::Continue
})
});
}
}
let final_lock = Arc::try_unwrap(violations).expect("Lock still has multiple owners");
final_lock.into_inner().expect("Mutex is poisoned")
}
fn search_dirs(config: &Config, ext: &str, project_root: &Path) -> Vec<PathBuf> {
config.dirs.get(ext).map_or_else(
|| vec![project_root.to_path_buf()],
|dirs| {
dirs.iter()
.map(|d| {
if d.as_os_str().is_empty() || d == Path::new(".") {
project_root.to_path_buf()
} else {
project_root.join(d)
}
})
.collect()
},
)
}