1use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::time::Duration;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct FileStats {
12 pub path: PathBuf,
14 pub language: String,
16 pub lines: LineStats,
18 pub size: u64,
20 pub complexity: Complexity,
22}
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
26pub struct LineStats {
27 pub total: usize,
29 pub code: usize,
31 pub comment: usize,
33 pub blank: usize,
35}
36
37impl LineStats {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn add(&mut self, other: &LineStats) {
45 self.total += other.total;
46 self.code += other.code;
47 self.comment += other.comment;
48 self.blank += other.blank;
49 }
50}
51
52impl std::ops::Add for LineStats {
53 type Output = Self;
54
55 fn add(self, other: Self) -> Self {
56 Self {
57 total: self.total + other.total,
58 code: self.code + other.code,
59 comment: self.comment + other.comment,
60 blank: self.blank + other.blank,
61 }
62 }
63}
64
65impl std::ops::AddAssign for LineStats {
66 fn add_assign(&mut self, other: Self) {
67 self.add(&other);
68 }
69}
70
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73pub struct Complexity {
74 pub functions: usize,
76 pub cyclomatic: usize,
78 pub max_depth: usize,
80 pub avg_func_lines: f64,
82}
83
84impl Complexity {
85 pub fn add(&mut self, other: &Complexity) {
87 self.functions += other.functions;
88 self.cyclomatic += other.cyclomatic;
89 self.max_depth = self.max_depth.max(other.max_depth);
90 }
91}
92
93#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95pub struct SizeDistribution {
96 pub tiny: usize,
98 pub small: usize,
100 pub medium: usize,
102 pub large: usize,
104 pub huge: usize,
106}
107
108impl SizeDistribution {
109 pub fn add(&mut self, size: u64) {
111 match size {
112 s if s < 1024 => self.tiny += 1,
113 s if s < 10 * 1024 => self.small += 1,
114 s if s < 100 * 1024 => self.medium += 1,
115 s if s < 1024 * 1024 => self.large += 1,
116 _ => self.huge += 1,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct LanguageSummary {
124 pub files: usize,
126 pub lines: LineStats,
128 pub size: u64,
130 pub complexity: Complexity,
132}
133
134#[derive(Debug, Clone, Serialize)]
136pub struct RepoStats {
137 pub name: String,
139 pub path: PathBuf,
141 pub primary_language: String,
143 pub files: Vec<FileStats>,
145 pub summary: RepoSummary,
147 pub by_language: IndexMap<String, LanguageSummary>,
149 pub git_info: Option<GitInfo>,
151}
152
153#[derive(Debug, Clone, Default, Serialize)]
155pub struct RepoSummary {
156 pub total_files: usize,
158 pub lines: LineStats,
160 pub total_size: u64,
162 pub complexity: Complexity,
164 pub size_distribution: SizeDistribution,
166}
167
168#[derive(Debug, Clone, Serialize)]
170pub struct GitInfo {
171 pub branch: Option<String>,
173 pub commit: Option<String>,
175 pub author: Option<String>,
177 pub date: Option<String>,
179}
180
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct Summary {
184 pub total_files: usize,
186 pub lines: LineStats,
188 pub total_size: u64,
190 pub by_language: IndexMap<String, LanguageSummary>,
192 pub size_distribution: SizeDistribution,
194 pub complexity: Complexity,
196}
197
198impl Summary {
199 pub fn from_file_stats(files: &[FileStats]) -> Self {
201 let mut summary = Summary::default();
202 let mut by_language: HashMap<String, LanguageSummary> = HashMap::new();
203
204 for file in files {
205 summary.total_files += 1;
206 summary.lines.add(&file.lines);
207 summary.total_size += file.size;
208 summary.size_distribution.add(file.size);
209 summary.complexity.add(&file.complexity);
210
211 let lang_summary = by_language.entry(file.language.clone()).or_default();
212 lang_summary.files += 1;
213 lang_summary.lines.add(&file.lines);
214 lang_summary.size += file.size;
215 lang_summary.complexity.add(&file.complexity);
216 }
217
218 let mut sorted: Vec<_> = by_language.into_iter().collect();
220 sorted.sort_by(|a, b| b.1.lines.code.cmp(&a.1.lines.code));
221 summary.by_language = sorted.into_iter().collect();
222
223 if summary.complexity.functions > 0 {
225 summary.complexity.avg_func_lines =
226 summary.lines.code as f64 / summary.complexity.functions as f64;
227 }
228
229 summary
230 }
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct AnalysisResult {
236 pub files: Vec<FileStats>,
238 pub summary: Summary,
240 #[serde(with = "duration_serde")]
242 pub elapsed: Duration,
243 pub scanned_files: usize,
245 pub skipped_files: usize,
247}
248
249mod duration_serde {
250 use serde::{Deserialize, Deserializer, Serialize, Serializer};
251 use std::time::Duration;
252
253 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
254 where
255 S: Serializer,
256 {
257 duration.as_secs_f64().serialize(serializer)
258 }
259
260 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
261 where
262 D: Deserializer<'de>,
263 {
264 let secs = f64::deserialize(deserializer)?;
265 Ok(Duration::from_secs_f64(secs))
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_line_stats_default() {
275 let stats = LineStats::default();
276 assert_eq!(stats.total, 0);
277 assert_eq!(stats.code, 0);
278 assert_eq!(stats.comment, 0);
279 assert_eq!(stats.blank, 0);
280 }
281
282 #[test]
283 fn test_line_stats_add() {
284 let mut stats1 = LineStats {
285 total: 100,
286 code: 80,
287 comment: 10,
288 blank: 10,
289 };
290 let stats2 = LineStats {
291 total: 50,
292 code: 40,
293 comment: 5,
294 blank: 5,
295 };
296
297 stats1.add(&stats2);
298
299 assert_eq!(stats1.total, 150);
300 assert_eq!(stats1.code, 120);
301 assert_eq!(stats1.comment, 15);
302 assert_eq!(stats1.blank, 15);
303 }
304
305 #[test]
306 fn test_line_stats_add_trait() {
307 let stats1 = LineStats {
308 total: 100,
309 code: 80,
310 comment: 10,
311 blank: 10,
312 };
313 let stats2 = LineStats {
314 total: 50,
315 code: 40,
316 comment: 5,
317 blank: 5,
318 };
319
320 let result = stats1 + stats2;
321
322 assert_eq!(result.total, 150);
323 assert_eq!(result.code, 120);
324 }
325
326 #[test]
327 fn test_line_stats_add_assign() {
328 let mut stats1 = LineStats {
329 total: 100,
330 code: 80,
331 comment: 10,
332 blank: 10,
333 };
334 let stats2 = LineStats {
335 total: 50,
336 code: 40,
337 comment: 5,
338 blank: 5,
339 };
340
341 stats1 += stats2;
342
343 assert_eq!(stats1.total, 150);
344 assert_eq!(stats1.code, 120);
345 }
346
347 #[test]
348 fn test_complexity_add() {
349 let mut c1 = Complexity {
350 functions: 10,
351 cyclomatic: 20,
352 max_depth: 5,
353 avg_func_lines: 0.0,
354 };
355 let c2 = Complexity {
356 functions: 5,
357 cyclomatic: 10,
358 max_depth: 8,
359 avg_func_lines: 0.0,
360 };
361
362 c1.add(&c2);
363
364 assert_eq!(c1.functions, 15);
365 assert_eq!(c1.cyclomatic, 30);
366 assert_eq!(c1.max_depth, 8); }
368
369 #[test]
370 fn test_size_distribution() {
371 let mut dist = SizeDistribution::default();
372
373 dist.add(500); dist.add(1024); dist.add(5000); dist.add(15000); dist.add(500_000); dist.add(2_000_000); assert_eq!(dist.tiny, 1);
381 assert_eq!(dist.small, 2);
382 assert_eq!(dist.medium, 1);
383 assert_eq!(dist.large, 1);
384 assert_eq!(dist.huge, 1);
385 }
386
387 #[test]
388 fn test_summary_from_file_stats() {
389 let files = vec![
390 FileStats {
391 path: PathBuf::from("src/main.rs"),
392 language: "Rust".to_string(),
393 lines: LineStats {
394 total: 100,
395 code: 80,
396 comment: 10,
397 blank: 10,
398 },
399 size: 2000,
400 complexity: Complexity {
401 functions: 5,
402 cyclomatic: 10,
403 max_depth: 3,
404 avg_func_lines: 16.0,
405 },
406 },
407 FileStats {
408 path: PathBuf::from("src/lib.rs"),
409 language: "Rust".to_string(),
410 lines: LineStats {
411 total: 50,
412 code: 40,
413 comment: 5,
414 blank: 5,
415 },
416 size: 1000,
417 complexity: Complexity {
418 functions: 3,
419 cyclomatic: 6,
420 max_depth: 2,
421 avg_func_lines: 13.3,
422 },
423 },
424 FileStats {
425 path: PathBuf::from("test.py"),
426 language: "Python".to_string(),
427 lines: LineStats {
428 total: 30,
429 code: 20,
430 comment: 5,
431 blank: 5,
432 },
433 size: 500,
434 complexity: Complexity {
435 functions: 2,
436 cyclomatic: 4,
437 max_depth: 2,
438 avg_func_lines: 10.0,
439 },
440 },
441 ];
442
443 let summary = Summary::from_file_stats(&files);
444
445 assert_eq!(summary.total_files, 3);
446 assert_eq!(summary.lines.total, 180);
447 assert_eq!(summary.lines.code, 140);
448 assert_eq!(summary.total_size, 3500);
449 assert_eq!(summary.by_language.len(), 2);
450 assert_eq!(summary.complexity.functions, 10);
451
452 let first_lang = summary.by_language.keys().next().unwrap();
454 assert_eq!(first_lang, "Rust");
455
456 let rust_stats = summary.by_language.get("Rust").unwrap();
457 assert_eq!(rust_stats.files, 2);
458 assert_eq!(rust_stats.lines.code, 120);
459 }
460
461 #[test]
462 fn test_summary_empty() {
463 let summary = Summary::from_file_stats(&[]);
464
465 assert_eq!(summary.total_files, 0);
466 assert_eq!(summary.lines.total, 0);
467 assert!(summary.by_language.is_empty());
468 }
469
470 #[test]
471 fn test_file_stats_default() {
472 let stats = FileStats::default();
473 assert!(stats.path.as_os_str().is_empty());
474 assert!(stats.language.is_empty());
475 assert_eq!(stats.size, 0);
476 }
477}