project_examer/
file_discovery.rs

1use 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)  // This enables .gitignore support
31            .hidden(false)           // Show hidden files except those in .gitignore
32            .git_ignore(true)        // Explicitly enable .gitignore parsing
33            .git_global(true)        // Respect global git ignore
34            .git_exclude(true);      // Respect .git/info/exclude
35            
36        // The ignore patterns will be handled in the file processing logic
37        
38        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            // Check if file matches any ignore patterns
49            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            // Handle simple glob patterns (*.ext)
66            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..]; // Remove "*."
70                    if filename_str.ends_with(&format!(".{}", ext)) {
71                        return true;
72                    }
73                }
74            } else if pattern.contains('*') {
75                // Handle other wildcard patterns by converting to simple regex
76                let regex_pattern = pattern.replace('*', ".*");
77                if let Ok(re) = regex::Regex::new(&regex_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                // Handle exact matches and directory names
89                if path_str.contains(pattern) {
90                    return true;
91                }
92                // Check if any component of the path matches
93                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        // Handle files without extensions by filename
134        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}