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