1use std::path::{Path, PathBuf};
7use walkdir::WalkDir;
8
9pub struct ScanConfig {
11 pub root: PathBuf,
13 pub exclude_patterns: Vec<String>,
15 pub extensions: Vec<String>,
17}
18
19impl ScanConfig {
20 #[must_use]
21 pub fn new(root: PathBuf) -> Self {
22 Self {
23 root,
24 exclude_patterns: Vec::new(),
25 extensions: vec!["rs".to_string()],
26 }
27 }
28
29 #[must_use]
30 pub fn with_excludes(mut self, patterns: Vec<String>) -> Self {
31 self.exclude_patterns = patterns;
32 self
33 }
34
35 #[must_use]
36 pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
37 self.extensions = extensions;
38 self
39 }
40}
41
42#[must_use]
45pub fn scan_files(config: &ScanConfig) -> Vec<PathBuf> {
46 let mut files = Vec::new();
47
48 for entry in WalkDir::new(&config.root)
49 .into_iter()
50 .filter_entry(|e| {
51 let path = e.path();
52 if path.is_dir()
54 && let Some(name) = path.file_name().and_then(|n| n.to_str())
55 {
56 if name == "target" {
57 return false;
58 }
59 if name.starts_with('.') && path != config.root.as_path() {
61 return false;
62 }
63 }
64 true
65 })
66 .flatten()
67 {
68 let path = entry.path();
69 if path.is_file()
70 && path
71 .extension()
72 .and_then(|ext| ext.to_str())
73 .is_some_and(|ext| {
74 config
75 .extensions
76 .iter()
77 .any(|e| e.eq_ignore_ascii_case(ext))
78 })
79 && !is_excluded(path, &config.exclude_patterns)
80 {
81 files.push(path.to_path_buf());
82 }
83 }
84
85 files
86}
87
88#[must_use]
90pub fn is_excluded(path: &Path, patterns: &[String]) -> bool {
91 let path_str = path.to_string_lossy();
92 patterns
93 .iter()
94 .any(|pattern| path_str.contains(pattern.as_str()))
95}