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