use anyhow::Result;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::core::supported_extensions;
use crate::exit_codes::UsageError;
pub struct WalkOptions {
pub skip_hidden: bool,
pub max_depth: Option<usize>,
pub verbose: bool,
pub skip_unsupported_extensions: bool,
}
impl Default for WalkOptions {
fn default() -> Self {
Self {
skip_hidden: true,
max_depth: None,
verbose: false,
skip_unsupported_extensions: true,
}
}
}
const SKIP_DIRS: &[&str] = &[
"node_modules",
"target",
"__pycache__",
"dist",
"build",
".git",
".hg",
".svn",
];
fn should_skip_dir(name: &str, skip_hidden: bool) -> bool {
if skip_hidden && name.starts_with('.') {
return true;
}
SKIP_DIRS.contains(&name)
}
fn is_supported_file(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| supported_extensions().contains(&ext))
.unwrap_or(false)
}
pub fn collect_files(
paths: &[PathBuf],
recursive: bool,
opts: &WalkOptions,
) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for path in paths {
if path.is_file() || !path.exists() {
files.push(path.clone());
} else if path.is_dir() {
if !recursive {
return Err(UsageError(format!(
"'{}' is a directory; use -R/--recursive to process directories",
path.display()
))
.into());
}
walk_directory(path, opts, &mut files)?;
}
}
files.sort();
files.dedup();
Ok(files)
}
fn walk_directory(dir: &Path, opts: &WalkOptions, files: &mut Vec<PathBuf>) -> Result<()> {
let mut walker = WalkDir::new(dir).follow_links(false);
if let Some(depth) = opts.max_depth {
walker = walker.max_depth(depth);
}
for entry in walker.into_iter().filter_entry(|e| {
if e.depth() == 0 {
return true;
}
if e.file_type().is_dir() {
let name = e.file_name().to_str().unwrap_or("");
return !should_skip_dir(name, opts.skip_hidden);
}
true
}) {
match entry {
Ok(entry) => {
if entry.file_type().is_file()
&& (!opts.skip_unsupported_extensions || is_supported_file(entry.path()))
{
files.push(entry.into_path());
}
}
Err(e) => {
if opts.verbose {
eprintln!("Warning: {}", e);
}
}
}
}
Ok(())
}