use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use ignore::{DirEntry, WalkBuilder};
use crate::languages;
pub fn is_scan_target(path: &Path) -> bool {
languages::detect(path).is_some()
}
pub fn is_markdown(path: &Path) -> bool {
path.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("md"))
}
const SHARED_IGNORED_DIRS: &[&str] = &[".git", "build", "dist", ".cache"];
pub fn is_ignored_dir(name: &str) -> bool {
let folded = name.to_ascii_lowercase();
if folded.ends_with(".egg-info") {
return true;
}
if SHARED_IGNORED_DIRS.iter().any(|d| *d == folded) {
return true;
}
crate::languages::vendored_dirs()
.iter()
.any(|d| d.eq_ignore_ascii_case(name))
}
pub fn walk_targets(root: &Path, predicate: fn(&Path) -> bool) -> Vec<PathBuf> {
let walker = WalkBuilder::new(root)
.hidden(false)
.git_ignore(true)
.git_global(false)
.git_exclude(true)
.require_git(false)
.sort_by_file_name(|a, b| a.cmp(b))
.filter_entry(|entry: &DirEntry| {
if entry.depth() == 0 {
return true;
}
if entry.file_type().is_some_and(|ft| ft.is_dir())
&& is_ignored_dir(&entry.file_name().to_string_lossy())
{
return false;
}
true
})
.build();
let mut found = Vec::new();
for result in walker {
let Ok(entry) = result else { continue };
if entry.file_type().is_some_and(|ft| ft.is_file()) {
let path = entry.path();
if predicate(path) {
found.push(path.to_path_buf());
}
}
}
found
}
pub fn expand_paths(paths: &[PathBuf], predicate: fn(&Path) -> bool) -> Vec<PathBuf> {
expand(paths, predicate).targets
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rejected {
Missing,
Unanalyzable,
}
pub struct Expansion {
pub targets: Vec<PathBuf>,
pub rejected: BTreeMap<PathBuf, Rejected>,
}
pub fn expand_named(paths: &[PathBuf], root: &Path, predicate: fn(&Path) -> bool) -> Expansion {
if paths.is_empty() {
return Expansion {
targets: expand_paths(&[root.to_path_buf()], predicate),
rejected: BTreeMap::new(),
};
}
expand(paths, predicate)
}
fn expand(paths: &[PathBuf], predicate: fn(&Path) -> bool) -> Expansion {
let mut targets = BTreeSet::new();
let mut rejected = BTreeMap::new();
for named in paths {
let verdict = match std::fs::metadata(named) {
Err(_) => Some(Rejected::Missing),
Ok(meta) if meta.is_dir() => {
targets.extend(walk_targets(named, predicate));
None
}
Ok(meta) if meta.is_file() && predicate(named) => {
targets.insert(named.clone());
None
}
Ok(_) => Some(Rejected::Unanalyzable),
};
if let Some(verdict) = verdict {
rejected.insert(named.clone(), verdict);
}
}
Expansion {
targets: targets.into_iter().collect(),
rejected,
}
}
pub fn owning_command(path: &Path) -> Option<&'static str> {
if is_scan_target(path) {
Some("check")
} else if is_markdown(path) {
Some("lint-docs")
} else {
None
}
}
pub fn redirect_hint(path: &Path) -> Option<String> {
owning_command(path).map(|command| format!("run `drep {command}` instead"))
}
#[cfg(test)]
mod tests;