1use crate::filter::{apply_walk_builder, FilterOptions};
2use std::path::PathBuf;
3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::sync::Arc;
5use thiserror::Error;
6use walkdir::WalkDir;
7
8#[derive(Debug, Clone, Copy, Default)]
9pub enum WalkMode {
10 #[default]
11 Standard,
12 Full,
13}
14
15#[derive(Debug, Error)]
16pub enum WalkError {
17 #[error(transparent)]
18 Ignore(#[from] ignore::Error),
19 #[error(transparent)]
20 Walkdir(#[from] walkdir::Error),
21}
22
23pub struct WalkOutcome {
24 pub files_seen: usize,
25}
26
27pub fn walk_roots_fn(
29 roots: &[PathBuf],
30 opts: &FilterOptions,
31 mode: WalkMode,
32 on_file: impl Fn(PathBuf) + Sync + Send + Clone + 'static,
33) -> Result<WalkOutcome, WalkError> {
34 let files_seen = Arc::new(AtomicUsize::new(0));
35
36 match mode {
37 WalkMode::Standard => {
38 for root in roots {
39 let wb = apply_walk_builder(root, opts)?;
40 let walker = wb.build_parallel();
41 let seen = Arc::clone(&files_seen);
42
43 walker.run({
44 let on_file = on_file.clone();
45 move || {
46 let on_file = on_file.clone();
47 let seen = Arc::clone(&seen);
48 Box::new(move |res| {
49 if let Ok(entry) = res {
50 if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
51 seen.fetch_add(1, Ordering::Relaxed);
52 on_file(entry.path().to_path_buf());
53 }
54 }
55 ignore::WalkState::Continue
56 })
57 }
58 });
59 }
60 }
61 WalkMode::Full => {
62 for root in roots {
63 for entry in WalkDir::new(root)
64 .follow_links(false)
65 .into_iter()
66 .filter_map(Result::ok)
67 {
68 if entry.file_type().is_file() {
69 files_seen.fetch_add(1, Ordering::Relaxed);
70 on_file(entry.path().to_path_buf());
71 }
72 }
73 }
74 }
75 }
76
77 Ok(WalkOutcome {
78 files_seen: files_seen.load(Ordering::SeqCst),
79 })
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use std::collections::HashSet;
86 use std::fs;
87 use std::path::PathBuf;
88 use std::sync::{Arc, Mutex};
89 use tempfile::tempdir;
90
91 #[test]
92 fn standard_skips_gitignored_and_full_includes() {
93 let dir = tempdir().expect("temp dir");
94 let root = dir.path();
95 fs::write(root.join(".ignore"), "node_modules/\n").expect("write ignore");
96 fs::create_dir_all(root.join("node_modules")).expect("create node_modules");
97 fs::write(root.join("node_modules").join("a.js"), "x").expect("write ignored file");
98 fs::write(root.join("keep.txt"), "k").expect("write keep file");
99
100 let roots = vec![root.to_path_buf()];
101 let opts = FilterOptions::default();
102 let standard_paths: Arc<Mutex<HashSet<PathBuf>>> = Arc::new(Mutex::new(HashSet::new()));
103 let standard_paths_ref = Arc::clone(&standard_paths);
104 walk_roots_fn(&roots, &opts, WalkMode::Standard, move |p| {
105 standard_paths_ref.lock().expect("lock").insert(p);
106 })
107 .expect("standard walk");
108
109 let full_paths: Arc<Mutex<HashSet<PathBuf>>> = Arc::new(Mutex::new(HashSet::new()));
110 let full_paths_ref = Arc::clone(&full_paths);
111 walk_roots_fn(&roots, &opts, WalkMode::Full, move |p| {
112 full_paths_ref.lock().expect("lock").insert(p);
113 })
114 .expect("full walk");
115
116 let standard_paths = standard_paths.lock().expect("lock");
117 let full_paths = full_paths.lock().expect("lock");
118 assert!(standard_paths.contains(&root.join("keep.txt")));
119 assert!(!standard_paths.contains(&root.join("node_modules").join("a.js")));
120 assert!(full_paths.contains(&root.join("keep.txt")));
121 assert!(full_paths.contains(&root.join("node_modules").join("a.js")));
122 }
123}