herolib_code/parser/
walker.rs1use std::path::{Path, PathBuf};
7use walkdir::WalkDir;
8
9use super::error::{ParseError, ParseResult};
10
11#[derive(Debug, Clone)]
13pub struct WalkerConfig {
14 pub follow_symlinks: bool,
16 pub max_depth: Option<usize>,
18 pub skip_dirs: Vec<String>,
20 pub include_patterns: Vec<String>,
22}
23
24impl Default for WalkerConfig {
25 fn default() -> Self {
26 Self {
27 follow_symlinks: false,
28 max_depth: None,
29 skip_dirs: vec![
30 "target".to_string(),
31 ".git".to_string(),
32 "node_modules".to_string(),
33 ".cargo".to_string(),
34 ],
35 include_patterns: vec!["*.rs".to_string()],
36 }
37 }
38}
39
40impl WalkerConfig {
41 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn follow_symlinks(mut self, follow: bool) -> Self {
48 self.follow_symlinks = follow;
49 self
50 }
51
52 pub fn max_depth(mut self, depth: Option<usize>) -> Self {
54 self.max_depth = depth;
55 self
56 }
57
58 pub fn skip_dir(mut self, dir: impl Into<String>) -> Self {
60 self.skip_dirs.push(dir.into());
61 self
62 }
63
64 pub fn skip_dirs(mut self, dirs: Vec<String>) -> Self {
66 self.skip_dirs = dirs;
67 self
68 }
69}
70
71pub struct DirectoryWalker {
73 config: WalkerConfig,
75}
76
77impl DirectoryWalker {
78 pub fn new(config: WalkerConfig) -> Self {
80 Self { config }
81 }
82
83 pub fn with_defaults() -> Self {
85 Self::new(WalkerConfig::default())
86 }
87
88 pub fn discover_rust_files<P: AsRef<Path>>(&self, root: P) -> ParseResult<Vec<PathBuf>> {
98 let root = root.as_ref();
99
100 if !root.exists() {
101 return Err(ParseError::DirectoryNotFound(root.to_path_buf()));
102 }
103
104 let mut walker = WalkDir::new(root).follow_links(self.config.follow_symlinks);
105
106 if let Some(depth) = self.config.max_depth {
107 walker = walker.max_depth(depth);
108 }
109
110 let mut rust_files = Vec::new();
111 let skip_dirs = &self.config.skip_dirs;
112
113 let walker_iter = walker.into_iter().filter_entry(move |entry| {
114 if entry.depth() == 0 {
116 return true;
117 }
118 if let Some(name) = entry.file_name().to_str() {
119 if name.starts_with('.') || name.starts_with('_') {
121 return false;
122 }
123 if entry.file_type().is_dir() && skip_dirs.contains(&name.to_string()) {
125 return false;
126 }
127 }
128 true
129 });
130
131 for entry in walker_iter {
132 let entry = entry.map_err(|e| ParseError::WalkError {
133 path: root.to_path_buf(),
134 source: e,
135 })?;
136
137 let path = entry.path();
138
139 if entry.file_type().is_file() {
141 if let Some(extension) = path.extension() {
142 if extension == "rs" {
143 rust_files.push(path.to_path_buf());
144 }
145 }
146 }
147 }
148
149 rust_files.sort();
151
152 Ok(rust_files)
153 }
154
155 pub fn config(&self) -> &WalkerConfig {
157 &self.config
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use std::fs;
165 use tempfile::tempdir;
166
167 #[test]
168 fn test_walker_config_default() {
169 let config = WalkerConfig::default();
170 assert!(!config.follow_symlinks);
171 assert!(config.max_depth.is_none());
172 assert!(config.skip_dirs.contains(&"target".to_string()));
173 }
174
175 #[test]
176 fn test_walker_config_builder() {
177 let config = WalkerConfig::new()
178 .follow_symlinks(true)
179 .max_depth(Some(5))
180 .skip_dir("custom_dir");
181
182 assert!(config.follow_symlinks);
183 assert_eq!(config.max_depth, Some(5));
184 assert!(config.skip_dirs.contains(&"custom_dir".to_string()));
185 }
186
187 #[test]
188 fn test_discover_rust_files() {
189 let dir = tempdir().unwrap();
190 let dir_path = dir.path();
191
192 fs::write(dir_path.join("main.rs"), "fn main() {}").unwrap();
194 fs::write(dir_path.join("lib.rs"), "pub fn lib() {}").unwrap();
195
196 let subdir = dir_path.join("src");
198 fs::create_dir(&subdir).unwrap();
199 fs::write(subdir.join("module.rs"), "pub mod module;").unwrap();
200
201 fs::write(dir_path.join("readme.md"), "# Readme").unwrap();
203
204 let walker = DirectoryWalker::with_defaults();
205 let files = walker.discover_rust_files(dir_path).unwrap();
206
207 assert_eq!(files.len(), 3);
208 assert!(files.iter().all(|f| f.extension().unwrap() == "rs"));
209 }
210
211 #[test]
212 fn test_skip_directories() {
213 let dir = tempdir().unwrap();
214 let dir_path = dir.path();
215
216 let target_dir = dir_path.join("target");
218 fs::create_dir(&target_dir).unwrap();
219 fs::write(target_dir.join("generated.rs"), "// generated").unwrap();
220
221 fs::write(dir_path.join("main.rs"), "fn main() {}").unwrap();
223
224 let walker = DirectoryWalker::with_defaults();
225 let files = walker.discover_rust_files(dir_path).unwrap();
226
227 assert_eq!(files.len(), 1);
229 assert!(files[0].ends_with("main.rs"));
230 }
231
232 #[test]
233 fn test_directory_not_found() {
234 let walker = DirectoryWalker::with_defaults();
235 let result = walker.discover_rust_files("/nonexistent/path");
236
237 assert!(matches!(result, Err(ParseError::DirectoryNotFound(_))));
238 }
239}