project_examer/
file_discovery.rs1use crate::config::Config;
2use ignore::WalkBuilder;
3use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5use std::fs;
6use regex;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct FileInfo {
10 pub path: PathBuf,
11 pub size: u64,
12 pub extension: Option<String>,
13 pub language: Option<String>,
14}
15
16pub struct FileDiscovery {
17 config: Config,
18}
19
20impl FileDiscovery {
21 pub fn new(config: Config) -> Self {
22 Self { config }
23 }
24
25 pub fn discover_files(&self) -> crate::Result<Vec<FileInfo>> {
26 let mut files = Vec::new();
27
28 let mut walker_builder = WalkBuilder::new(&self.config.target_directory);
29 walker_builder
30 .standard_filters(true) .hidden(false) .git_ignore(true) .git_global(true) .git_exclude(true); let walker = walker_builder.build();
39
40 for result in walker {
41 let entry = result?;
42 let path = entry.path();
43
44 if !path.is_file() {
45 continue;
46 }
47
48 if self.should_ignore_file(path) {
50 continue;
51 }
52
53 if let Some(file_info) = self.process_file(path)? {
54 files.push(file_info);
55 }
56 }
57
58 Ok(files)
59 }
60
61 fn should_ignore_file(&self, path: &Path) -> bool {
62 let path_str = path.to_string_lossy();
63
64 for pattern in &self.config.ignore_patterns {
65 if pattern.starts_with("*.") {
67 if let Some(filename) = path.file_name() {
68 let filename_str = filename.to_string_lossy();
69 let ext = &pattern[2..]; if filename_str.ends_with(&format!(".{}", ext)) {
71 return true;
72 }
73 }
74 } else if pattern.contains('*') {
75 let regex_pattern = pattern.replace('*', ".*");
77 if let Ok(re) = regex::Regex::new(®ex_pattern) {
78 if re.is_match(&path_str) {
79 return true;
80 }
81 if let Some(filename) = path.file_name() {
82 if re.is_match(&filename.to_string_lossy()) {
83 return true;
84 }
85 }
86 }
87 } else {
88 if path_str.contains(pattern) {
90 return true;
91 }
92 for component in path.components() {
94 if component.as_os_str().to_string_lossy() == *pattern {
95 return true;
96 }
97 }
98 }
99 }
100
101 false
102 }
103
104 fn process_file(&self, path: &Path) -> crate::Result<Option<FileInfo>> {
105 let metadata = fs::metadata(path)?;
106 let size = metadata.len();
107
108 if size > self.config.max_file_size as u64 {
109 return Ok(None);
110 }
111
112 let extension = path.extension()
113 .and_then(|ext| ext.to_str())
114 .map(|s| s.to_lowercase());
115
116 if let Some(ref ext) = extension {
117 if !self.config.file_extensions.contains(ext) {
118 return Ok(None);
119 }
120 }
121
122 let language = self.detect_language(path, &extension);
123
124 Ok(Some(FileInfo {
125 path: path.to_path_buf(),
126 size,
127 extension,
128 language,
129 }))
130 }
131
132 fn detect_language(&self, path: &Path, extension: &Option<String>) -> Option<String> {
133 if extension.is_none() {
135 if let Some(filename) = path.file_name() {
136 let filename_lower = filename.to_string_lossy().to_lowercase();
137 match filename_lower.as_str() {
138 "readme" | "license" | "changelog" | "contributing" | "authors" |
139 "install" | "usage" | "todo" | "news" | "history" | "acknowledgments" |
140 "makefile" | "dockerfile" => return Some("text".to_string()),
141 _ => {}
142 }
143 }
144 }
145
146 match extension.as_deref() {
147 Some("rs") => Some("rust".to_string()),
148 Some("js") => Some("javascript".to_string()),
149 Some("ts") => Some("typescript".to_string()),
150 Some("tsx") => Some("typescript".to_string()),
151 Some("jsx") => Some("javascript".to_string()),
152 Some("py") => Some("python".to_string()),
153 Some("java") => Some("java".to_string()),
154 Some("go") => Some("go".to_string()),
155 Some("cpp") | Some("cc") | Some("cxx") => Some("cpp".to_string()),
156 Some("c") => Some("c".to_string()),
157 Some("h") | Some("hpp") => Some("c".to_string()),
158 Some("php") => Some("php".to_string()),
159 Some("rb") => Some("ruby".to_string()),
160 Some("cs") => Some("csharp".to_string()),
161 Some("swift") => Some("swift".to_string()),
162 Some("kt") => Some("kotlin".to_string()),
163 Some("scala") => Some("scala".to_string()),
164 Some("clj") | Some("cljs") => Some("clojure".to_string()),
165 Some("hs") => Some("haskell".to_string()),
166 Some("ml") | Some("mli") => Some("ocaml".to_string()),
167 Some("elm") => Some("elm".to_string()),
168 Some("ex") | Some("exs") => Some("elixir".to_string()),
169 Some("erl") | Some("hrl") => Some("erlang".to_string()),
170 Some("dart") => Some("dart".to_string()),
171 Some("lua") => Some("lua".to_string()),
172 Some("r") => Some("r".to_string()),
173 Some("m") => Some("objective-c".to_string()),
174 Some("mm") => Some("objective-cpp".to_string()),
175 Some("pl") | Some("pm") => Some("perl".to_string()),
176 Some("sh") | Some("bash") => Some("bash".to_string()),
177 Some("ps1") => Some("powershell".to_string()),
178 Some("sql") => Some("sql".to_string()),
179 Some("html") | Some("htm") => Some("html".to_string()),
180 Some("css") => Some("css".to_string()),
181 Some("scss") | Some("sass") => Some("scss".to_string()),
182 Some("xml") => Some("xml".to_string()),
183 Some("json") => Some("json".to_string()),
184 Some("yaml") | Some("yml") => Some("yaml".to_string()),
185 Some("toml") => Some("toml".to_string()),
186 Some("md") => Some("markdown".to_string()),
187 Some("txt") => Some("text".to_string()),
188 Some("tex") => Some("latex".to_string()),
189 Some("dockerfile") => Some("dockerfile".to_string()),
190 Some("makefile") => Some("makefile".to_string()),
191 Some("cmake") => Some("cmake".to_string()),
192 _ => None,
193 }
194 }
195
196 pub fn filter_by_language<'a>(&self, files: &'a [FileInfo], language: &str) -> Vec<&'a FileInfo> {
197 files.iter()
198 .filter(|f| f.language.as_deref() == Some(language))
199 .collect()
200 }
201
202 pub fn get_stats(&self, files: &[FileInfo]) -> FileStats {
203 let mut stats = FileStats::default();
204
205 for file in files {
206 stats.total_files += 1;
207 stats.total_size += file.size;
208
209 if let Some(ref lang) = file.language {
210 *stats.languages.entry(lang.clone()).or_insert(0) += 1;
211 }
212 }
213
214 stats
215 }
216}
217
218#[derive(Debug, Default)]
219pub struct FileStats {
220 pub total_files: usize,
221 pub total_size: u64,
222 pub languages: std::collections::HashMap<String, usize>,
223}
224
225impl FileStats {
226 pub fn print_summary(&self) {
227 println!("File Discovery Summary:");
228 println!(" Total files: {}", self.total_files);
229 println!(" Total size: {:.2} MB", self.total_size as f64 / (1024.0 * 1024.0));
230 println!(" Languages:");
231
232 let mut langs: Vec<_> = self.languages.iter().collect();
233 langs.sort_by(|a, b| b.1.cmp(a.1));
234
235 for (lang, count) in langs {
236 println!(" {}: {} files", lang, count);
237 }
238 }
239}