1pub mod analyzer;
30pub mod config;
31pub mod error;
32pub mod filter;
33pub mod git;
34pub mod insight;
35pub mod language;
36pub mod output;
37pub mod walker;
38
39pub use analyzer::stats::{
40 AnalysisResult, Complexity, FileStats, LanguageSummary, LineStats, RepoStats, RepoSummary,
41 SizeDistribution, Summary,
42};
43pub use config::Config;
44pub use error::{Error, Result};
45pub use git::{FileChurn, GitClient};
46pub use language::{Language, LanguageRegistry};
47pub use output::{OutputFormat, OutputOptions};
48
49use std::path::Path;
50use std::sync::Arc;
51use std::time::Instant;
52
53use analyzer::FileAnalyzer;
54use filter::FilterChain;
55use walker::ParallelWalker;
56
57pub fn analyze<P: AsRef<Path>>(paths: &[P], config: &Config) -> Result<AnalysisResult> {
61 let start = Instant::now();
62
63 let registry = Arc::new(LanguageRegistry::with_builtin()?);
65 let analyzer = Arc::new(FileAnalyzer::new(Arc::clone(®istry), config));
66 let filter: Arc<dyn filter::Filter> = Arc::new(FilterChain::new(config)?);
67 let walker = ParallelWalker::new(config.walker.clone());
68
69 let mut all_stats = Vec::new();
71 let mut scanned_files = 0;
72 let mut skipped_files = 0;
73
74 for path in paths {
75 let path = path.as_ref();
76 if !path.exists() {
77 return Err(Error::DirectoryNotFound {
78 path: path.to_path_buf(),
79 });
80 }
81
82 walker.walk_and_analyze(
83 path,
84 Arc::clone(&analyzer),
85 Arc::clone(&filter),
86 |stats| {
87 all_stats.push(stats);
88 scanned_files += 1;
89 },
90 |_| {
91 skipped_files += 1;
92 },
93 )?;
94 }
95
96 let summary = Summary::from_file_stats(&all_stats);
98 let elapsed = start.elapsed();
99
100 Ok(AnalysisResult {
101 files: all_stats,
102 summary,
103 elapsed,
104 scanned_files,
105 skipped_files,
106 })
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::fs;
113 use std::path::PathBuf;
114 use tempfile::TempDir;
115
116 fn create_test_file(dir: &Path, name: &str, content: &str) {
117 let path = dir.join(name);
118 if let Some(parent) = path.parent() {
119 fs::create_dir_all(parent).unwrap();
120 }
121 fs::write(path, content).unwrap();
122 }
123
124 #[test]
125 fn test_analyze_empty_directory() {
126 let dir = TempDir::new().unwrap();
127 let config = Config::default();
128
129 let result = analyze(&[dir.path()], &config).unwrap();
130
131 assert_eq!(result.summary.total_files, 0);
132 assert_eq!(result.summary.lines.total, 0);
133 }
134
135 #[test]
136 fn test_analyze_single_rust_file() {
137 let dir = TempDir::new().unwrap();
138 let rust_code = "fn main() {\n println!(\"Hello, world!\");\n}\n";
139 create_test_file(dir.path(), "main.rs", rust_code);
140
141 let config = Config::default();
142 let result = analyze(&[dir.path()], &config).unwrap();
143
144 assert_eq!(result.summary.total_files, 1);
145 assert!(result.summary.by_language.contains_key("Rust"));
146 assert_eq!(result.summary.lines.total, 3);
147 assert_eq!(result.summary.lines.code, 3);
148 }
149
150 #[test]
151 fn test_analyze_multiple_languages() {
152 let dir = TempDir::new().unwrap();
153
154 let rust_code = "fn main() {}\n";
155 let python_code = "def main():\n pass\n";
156 let js_code = "function main() {}\n";
157
158 create_test_file(dir.path(), "main.rs", rust_code);
159 create_test_file(dir.path(), "main.py", python_code);
160 create_test_file(dir.path(), "main.js", js_code);
161
162 let config = Config::default();
163 let result = analyze(&[dir.path()], &config).unwrap();
164
165 assert_eq!(result.summary.total_files, 3);
166 assert!(result.summary.by_language.contains_key("Rust"));
167 assert!(result.summary.by_language.contains_key("Python"));
168 assert!(result.summary.by_language.contains_key("JavaScript"));
169 }
170
171 #[test]
172 fn test_analyze_with_comments() {
173 let dir = TempDir::new().unwrap();
174 let rust_code = r#"// This is a comment
175fn main() {
176 /* block comment */
177 println!("Hello");
178}
179"#;
180 create_test_file(dir.path(), "main.rs", rust_code);
181
182 let config = Config::default();
183 let result = analyze(&[dir.path()], &config).unwrap();
184
185 assert_eq!(result.summary.total_files, 1);
186 assert!(result.summary.lines.comment > 0);
187 assert!(result.summary.lines.code > 0);
188 }
189
190 #[test]
191 fn test_analyze_nested_directories() {
192 let dir = TempDir::new().unwrap();
193
194 create_test_file(dir.path(), "src/main.rs", "fn main() {}\n");
195 create_test_file(dir.path(), "src/lib.rs", "pub fn lib() {}\n");
196 create_test_file(dir.path(), "tests/test.rs", "#[test]\nfn test() {}\n");
197
198 let config = Config::default();
199 let result = analyze(&[dir.path()], &config).unwrap();
200
201 assert_eq!(result.summary.total_files, 3);
202 }
203
204 #[test]
205 fn test_analyze_nonexistent_path() {
206 let config = Config::default();
207 let result = analyze(&[PathBuf::from("/nonexistent/path")], &config);
208
209 assert!(result.is_err());
210 match result.unwrap_err() {
211 Error::DirectoryNotFound { path } => {
212 assert_eq!(path, PathBuf::from("/nonexistent/path"));
213 }
214 _ => panic!("Expected DirectoryNotFound error"),
215 }
216 }
217
218 #[test]
219 fn test_analyze_multiple_paths() {
220 let dir1 = TempDir::new().unwrap();
221 let dir2 = TempDir::new().unwrap();
222
223 create_test_file(dir1.path(), "a.rs", "fn a() {}\n");
224 create_test_file(dir2.path(), "b.rs", "fn b() {}\n");
225
226 let config = Config::default();
227 let result = analyze(&[dir1.path(), dir2.path()], &config).unwrap();
228
229 assert_eq!(result.summary.total_files, 2);
230 }
231
232 #[test]
233 fn test_analyze_respects_gitignore() {
234 let dir = TempDir::new().unwrap();
235
236 std::process::Command::new("git")
238 .args(["init"])
239 .current_dir(dir.path())
240 .output()
241 .ok();
242
243 create_test_file(dir.path(), ".gitignore", "ignored/\n");
245
246 create_test_file(dir.path(), "main.rs", "fn main() {}\n");
248 create_test_file(dir.path(), "ignored/skip.rs", "fn skip() {}\n");
249
250 let config = Config::default();
251 let result = analyze(&[dir.path()], &config).unwrap();
252
253 assert_eq!(result.summary.total_files, 1);
255 }
256
257 #[test]
258 fn test_analyze_result_contains_elapsed_time() {
259 let dir = TempDir::new().unwrap();
260 create_test_file(dir.path(), "main.rs", "fn main() {}\n");
261
262 let config = Config::default();
263 let result = analyze(&[dir.path()], &config).unwrap();
264
265 assert!(result.elapsed.as_nanos() > 0);
267 }
268
269 #[test]
270 fn test_analyze_scanned_files_count() {
271 let dir = TempDir::new().unwrap();
272
273 create_test_file(dir.path(), "a.rs", "fn a() {}\n");
274 create_test_file(dir.path(), "b.rs", "fn b() {}\n");
275 create_test_file(dir.path(), "c.txt", "not code\n"); let config = Config::default();
278 let result = analyze(&[dir.path()], &config).unwrap();
279
280 assert_eq!(result.scanned_files, 2);
281 assert!(result.skipped_files >= 1); }
283}