1use anyhow::Result;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7use crate::core::supported_extensions;
8use crate::exit_codes::UsageError;
9
10pub struct WalkOptions {
12 pub skip_hidden: bool,
13 pub max_depth: Option<usize>,
14 pub verbose: bool,
15 pub skip_unsupported_extensions: bool,
18}
19
20impl Default for WalkOptions {
21 fn default() -> Self {
22 Self {
23 skip_hidden: true,
24 max_depth: None,
25 verbose: false,
26 skip_unsupported_extensions: true,
27 }
28 }
29}
30
31const SKIP_DIRS: &[&str] = &[
33 "node_modules",
34 "target",
35 "__pycache__",
36 "dist",
37 "build",
38 ".git",
39 ".hg",
40 ".svn",
41];
42
43fn should_skip_dir(name: &str, skip_hidden: bool) -> bool {
45 if skip_hidden && name.starts_with('.') {
46 return true;
47 }
48 SKIP_DIRS.contains(&name)
49}
50
51fn is_supported_file(path: &Path) -> bool {
53 path.extension()
54 .and_then(|ext| ext.to_str())
55 .map(|ext| supported_extensions().contains(&ext))
56 .unwrap_or(false)
57}
58
59pub fn collect_files(
68 paths: &[PathBuf],
69 recursive: bool,
70 opts: &WalkOptions,
71) -> Result<Vec<PathBuf>> {
72 let mut files = Vec::new();
73
74 for path in paths {
75 if path.is_file() || !path.exists() {
76 files.push(path.clone());
79 } else if path.is_dir() {
80 if !recursive {
81 return Err(UsageError(format!(
82 "'{}' is a directory; use -R/--recursive to process directories",
83 path.display()
84 ))
85 .into());
86 }
87 walk_directory(path, opts, &mut files)?;
88 }
89 }
90
91 files.sort();
92 files.dedup();
93 Ok(files)
94}
95
96fn walk_directory(dir: &Path, opts: &WalkOptions, files: &mut Vec<PathBuf>) -> Result<()> {
98 let mut walker = WalkDir::new(dir).follow_links(false);
99
100 if let Some(depth) = opts.max_depth {
101 walker = walker.max_depth(depth);
102 }
103
104 for entry in walker.into_iter().filter_entry(|e| {
105 if e.depth() == 0 {
107 return true;
108 }
109 if e.file_type().is_dir() {
111 let name = e.file_name().to_str().unwrap_or("");
112 return !should_skip_dir(name, opts.skip_hidden);
113 }
114 true
115 }) {
116 match entry {
117 Ok(entry) => {
118 if entry.file_type().is_file()
119 && (!opts.skip_unsupported_extensions || is_supported_file(entry.path()))
120 {
121 files.push(entry.into_path());
122 }
123 }
124 Err(e) => {
125 if opts.verbose {
127 eprintln!("Warning: {}", e);
128 }
129 }
130 }
131 }
132
133 Ok(())
134}