1pub mod complexity;
2pub mod metrics;
3pub mod type_flow;
4
5pub use complexity::{
7 calculate_cognitive, calculate_cyclomatic, calculate_max_nesting, count_statements,
8};
9
10use anyhow::Result;
11use depyler_core::hir::{HirFunction, HirModule};
12use serde::{Deserialize, Serialize};
13
14#[cfg(test)]
15use depyler_annotations::TranspilationAnnotations;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AnalysisResult {
19 pub module_metrics: ModuleMetrics,
20 pub function_metrics: Vec<FunctionMetrics>,
21 pub type_coverage: TypeCoverage,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ModuleMetrics {
26 pub total_functions: usize,
27 pub total_lines: usize,
28 pub avg_cyclomatic_complexity: f64,
29 pub max_cyclomatic_complexity: u32,
30 pub avg_cognitive_complexity: f64,
31 pub max_cognitive_complexity: u32,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FunctionMetrics {
36 pub name: String,
37 pub cyclomatic_complexity: u32,
38 pub cognitive_complexity: u32,
39 pub lines_of_code: usize,
40 pub parameters: usize,
41 pub max_nesting_depth: usize,
42 pub has_type_annotations: bool,
43 pub return_type_annotated: bool,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TypeCoverage {
48 pub total_parameters: usize,
49 pub annotated_parameters: usize,
50 pub total_functions: usize,
51 pub functions_with_return_type: usize,
52 pub coverage_percentage: f64,
53}
54
55pub struct Analyzer {
56 #[allow(dead_code)]
57 enable_type_inference: bool,
58}
59
60impl Analyzer {
61 pub fn new() -> Self {
62 Self {
63 enable_type_inference: true,
64 }
65 }
66
67 pub fn analyze(&self, module: &HirModule) -> Result<AnalysisResult> {
68 let function_metrics: Vec<FunctionMetrics> = module
69 .functions
70 .iter()
71 .map(|f| self.analyze_function(f))
72 .collect::<Result<Vec<_>>>()?;
73
74 let module_metrics = self.calculate_module_metrics(&function_metrics);
75 let type_coverage = self.calculate_type_coverage(module);
76
77 Ok(AnalysisResult {
78 module_metrics,
79 function_metrics,
80 type_coverage,
81 })
82 }
83
84 fn analyze_function(&self, func: &HirFunction) -> Result<FunctionMetrics> {
85 let cyclomatic = complexity::calculate_cyclomatic(&func.body);
86 let cognitive = complexity::calculate_cognitive(&func.body);
87 let max_nesting = complexity::calculate_max_nesting(&func.body);
88 let loc = complexity::count_statements(&func.body);
89
90 let has_type_annotations = func
91 .params
92 .iter()
93 .all(|param| !matches!(param.ty, depyler_core::hir::Type::Unknown));
94 let return_type_annotated = !matches!(func.ret_type, depyler_core::hir::Type::Unknown);
95
96 Ok(FunctionMetrics {
97 name: func.name.clone(),
98 cyclomatic_complexity: cyclomatic,
99 cognitive_complexity: cognitive,
100 lines_of_code: loc,
101 parameters: func.params.len(),
102 max_nesting_depth: max_nesting,
103 has_type_annotations,
104 return_type_annotated,
105 })
106 }
107
108 fn calculate_module_metrics(&self, functions: &[FunctionMetrics]) -> ModuleMetrics {
109 let total_functions = functions.len();
110 let total_lines: usize = functions.iter().map(|f| f.lines_of_code).sum();
111
112 let avg_cyclomatic = if total_functions > 0 {
113 functions
114 .iter()
115 .map(|f| f.cyclomatic_complexity as f64)
116 .sum::<f64>()
117 / total_functions as f64
118 } else {
119 0.0
120 };
121
122 let max_cyclomatic = functions
123 .iter()
124 .map(|f| f.cyclomatic_complexity)
125 .max()
126 .unwrap_or(0);
127
128 let avg_cognitive = if total_functions > 0 {
129 functions
130 .iter()
131 .map(|f| f.cognitive_complexity as f64)
132 .sum::<f64>()
133 / total_functions as f64
134 } else {
135 0.0
136 };
137
138 let max_cognitive = functions
139 .iter()
140 .map(|f| f.cognitive_complexity)
141 .max()
142 .unwrap_or(0);
143
144 ModuleMetrics {
145 total_functions,
146 total_lines,
147 avg_cyclomatic_complexity: avg_cyclomatic,
148 max_cyclomatic_complexity: max_cyclomatic,
149 avg_cognitive_complexity: avg_cognitive,
150 max_cognitive_complexity: max_cognitive,
151 }
152 }
153
154 fn calculate_type_coverage(&self, module: &HirModule) -> TypeCoverage {
155 let mut total_parameters = 0;
156 let mut annotated_parameters = 0;
157 let mut functions_with_return_type = 0;
158
159 for func in &module.functions {
160 total_parameters += func.params.len();
161 annotated_parameters += func
162 .params
163 .iter()
164 .filter(|param| !matches!(param.ty, depyler_core::hir::Type::Unknown))
165 .count();
166
167 if !matches!(func.ret_type, depyler_core::hir::Type::Unknown) {
168 functions_with_return_type += 1;
169 }
170 }
171
172 let total_annotations = annotated_parameters + functions_with_return_type;
173 let total_possible = total_parameters + module.functions.len();
174 let coverage_percentage = if total_possible > 0 {
175 (total_annotations as f64 / total_possible as f64) * 100.0
176 } else {
177 100.0
178 };
179
180 TypeCoverage {
181 total_parameters,
182 annotated_parameters,
183 total_functions: module.functions.len(),
184 functions_with_return_type,
185 coverage_percentage,
186 }
187 }
188}
189
190impl Default for Analyzer {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use depyler_core::hir::*;
200
201 fn create_test_function() -> HirFunction {
202 use smallvec::smallvec;
203 HirFunction {
204 name: "test_func".to_string(),
205 params: smallvec![
206 HirParam {
207 name: Symbol::from("x"),
208 ty: Type::Int,
209 default: None,
210 is_vararg: false,
211 },
212 HirParam {
213 name: Symbol::from("y"),
214 ty: Type::String,
215 default: None,
216 is_vararg: false,
217 }
218 ],
219 ret_type: Type::Int,
220 body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(42))))],
221 properties: FunctionProperties::default(),
222 annotations: TranspilationAnnotations::default(),
223 docstring: None,
224 }
225 }
226
227 #[test]
228 fn test_analyzer_creation() {
229 let analyzer = Analyzer::new();
230 assert!(analyzer.enable_type_inference);
231
232 let default_analyzer = Analyzer::default();
233 assert!(default_analyzer.enable_type_inference);
234 }
235
236 #[test]
237 fn test_analyze_empty_module() {
238 let analyzer = Analyzer::new();
239 let module = HirModule {
240 functions: vec![],
241 imports: vec![],
242 type_aliases: vec![],
243 protocols: vec![],
244 classes: vec![],
245 constants: vec![],
246 top_level_stmts: vec![],
247 };
248
249 let result = analyzer.analyze(&module).unwrap();
250 assert_eq!(result.module_metrics.total_functions, 0);
251 assert_eq!(result.function_metrics.len(), 0);
252 assert_eq!(result.type_coverage.total_functions, 0);
253 assert_eq!(result.type_coverage.coverage_percentage, 100.0);
254 }
255
256 #[test]
257 fn test_analyze_single_function() {
258 let analyzer = Analyzer::new();
259 let func = create_test_function();
260 let module = HirModule {
261 functions: vec![func],
262 imports: vec![],
263 type_aliases: vec![],
264 protocols: vec![],
265 classes: vec![],
266 constants: vec![],
267 top_level_stmts: vec![],
268 };
269
270 let result = analyzer.analyze(&module).unwrap();
271 assert_eq!(result.module_metrics.total_functions, 1);
272 assert_eq!(result.function_metrics.len(), 1);
273
274 let func_metrics = &result.function_metrics[0];
275 assert_eq!(func_metrics.name, "test_func");
276 assert_eq!(func_metrics.parameters, 2);
277 assert!(func_metrics.has_type_annotations);
278 assert!(func_metrics.return_type_annotated);
279 }
280
281 #[test]
282 fn test_type_coverage_calculation() {
283 let analyzer = Analyzer::new();
284 use smallvec::smallvec;
285 let func_with_types = HirFunction {
286 name: "typed_func".to_string(),
287 params: smallvec![HirParam {
288 name: Symbol::from("x"),
289 ty: Type::Int,
290 default: None,
291 is_vararg: false,
292 }],
293 ret_type: Type::String,
294 body: vec![],
295 properties: FunctionProperties::default(),
296 annotations: TranspilationAnnotations::default(),
297 docstring: None,
298 };
299
300 let func_without_types = HirFunction {
301 name: "untyped_func".to_string(),
302 params: smallvec![HirParam {
303 name: Symbol::from("y"),
304 ty: Type::Unknown,
305 default: None,
306 is_vararg: false,
307 }],
308 ret_type: Type::Unknown,
309 body: vec![],
310 properties: FunctionProperties::default(),
311 annotations: TranspilationAnnotations::default(),
312 docstring: None,
313 };
314
315 let module = HirModule {
316 functions: vec![func_with_types, func_without_types],
317 imports: vec![],
318 type_aliases: vec![],
319 protocols: vec![],
320 classes: vec![],
321 constants: vec![],
322 top_level_stmts: vec![],
323 };
324
325 let coverage = analyzer.calculate_type_coverage(&module);
326 assert_eq!(coverage.total_parameters, 2);
327 assert_eq!(coverage.annotated_parameters, 1);
328 assert_eq!(coverage.total_functions, 2);
329 assert_eq!(coverage.functions_with_return_type, 1);
330 assert_eq!(coverage.coverage_percentage, 50.0); }
332
333 #[test]
338 fn test_analyze_function_with_unknown_types() {
339 use smallvec::smallvec;
340 let analyzer = Analyzer::new();
341 let func = HirFunction {
342 name: "untyped".to_string(),
343 params: smallvec![HirParam {
344 name: Symbol::from("a"),
345 ty: Type::Unknown,
346 default: None,
347 is_vararg: false,
348 }],
349 ret_type: Type::Unknown,
350 body: vec![HirStmt::Return(None)],
351 properties: FunctionProperties::default(),
352 annotations: TranspilationAnnotations::default(),
353 docstring: None,
354 };
355 let module = HirModule {
356 functions: vec![func],
357 imports: vec![],
358 type_aliases: vec![],
359 protocols: vec![],
360 classes: vec![],
361 constants: vec![],
362 top_level_stmts: vec![],
363 };
364 let result = analyzer.analyze(&module).unwrap();
365 let fm = &result.function_metrics[0];
366 assert!(!fm.has_type_annotations);
367 assert!(!fm.return_type_annotated);
368 }
369
370 #[test]
371 fn test_analyze_multiple_functions() {
372 use smallvec::smallvec;
373 let analyzer = Analyzer::new();
374 let func1 = create_test_function();
375 let func2 = HirFunction {
376 name: "func2".to_string(),
377 params: smallvec![],
378 ret_type: Type::Bool,
379 body: vec![
380 HirStmt::If {
381 condition: HirExpr::Literal(Literal::Bool(true)),
382 then_body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Bool(
383 true,
384 ))))],
385 else_body: Some(vec![HirStmt::Return(Some(HirExpr::Literal(
386 Literal::Bool(false),
387 )))]),
388 },
389 ],
390 properties: FunctionProperties::default(),
391 annotations: TranspilationAnnotations::default(),
392 docstring: None,
393 };
394 let module = HirModule {
395 functions: vec![func1, func2],
396 imports: vec![],
397 type_aliases: vec![],
398 protocols: vec![],
399 classes: vec![],
400 constants: vec![],
401 top_level_stmts: vec![],
402 };
403 let result = analyzer.analyze(&module).unwrap();
404 assert_eq!(result.module_metrics.total_functions, 2);
405 assert!(result.module_metrics.max_cyclomatic_complexity >= 2);
406 }
407
408 #[test]
409 fn test_analysis_result_serialization() {
410 let result = AnalysisResult {
411 module_metrics: ModuleMetrics {
412 total_functions: 1,
413 total_lines: 10,
414 avg_cyclomatic_complexity: 2.0,
415 max_cyclomatic_complexity: 2,
416 avg_cognitive_complexity: 1.0,
417 max_cognitive_complexity: 1,
418 },
419 function_metrics: vec![FunctionMetrics {
420 name: "test".to_string(),
421 cyclomatic_complexity: 2,
422 cognitive_complexity: 1,
423 lines_of_code: 10,
424 parameters: 1,
425 max_nesting_depth: 1,
426 has_type_annotations: true,
427 return_type_annotated: true,
428 }],
429 type_coverage: TypeCoverage {
430 total_parameters: 1,
431 annotated_parameters: 1,
432 total_functions: 1,
433 functions_with_return_type: 1,
434 coverage_percentage: 100.0,
435 },
436 };
437 let json = serde_json::to_string(&result).unwrap();
438 assert!(json.contains("test"));
439 let deserialized: AnalysisResult = serde_json::from_str(&json).unwrap();
440 assert_eq!(deserialized.module_metrics.total_functions, 1);
441 }
442
443 #[test]
444 fn test_module_metrics_debug_clone() {
445 let metrics = ModuleMetrics {
446 total_functions: 5,
447 total_lines: 50,
448 avg_cyclomatic_complexity: 3.0,
449 max_cyclomatic_complexity: 8,
450 avg_cognitive_complexity: 2.5,
451 max_cognitive_complexity: 6,
452 };
453 let debug = format!("{:?}", metrics);
454 assert!(debug.contains("ModuleMetrics"));
455 let cloned = metrics.clone();
456 assert_eq!(cloned.total_functions, 5);
457 }
458
459 #[test]
460 fn test_function_metrics_debug_clone() {
461 let fm = FunctionMetrics {
462 name: "my_func".to_string(),
463 cyclomatic_complexity: 3,
464 cognitive_complexity: 2,
465 lines_of_code: 15,
466 parameters: 2,
467 max_nesting_depth: 3,
468 has_type_annotations: false,
469 return_type_annotated: true,
470 };
471 let debug = format!("{:?}", fm);
472 assert!(debug.contains("my_func"));
473 let cloned = fm.clone();
474 assert_eq!(cloned.name, "my_func");
475 }
476
477 #[test]
478 fn test_type_coverage_debug_clone() {
479 let tc = TypeCoverage {
480 total_parameters: 10,
481 annotated_parameters: 7,
482 total_functions: 5,
483 functions_with_return_type: 3,
484 coverage_percentage: 66.67,
485 };
486 let debug = format!("{:?}", tc);
487 assert!(debug.contains("TypeCoverage"));
488 let cloned = tc.clone();
489 assert_eq!(cloned.total_parameters, 10);
490 }
491
492 #[test]
493 fn test_module_metrics_empty_functions() {
494 let analyzer = Analyzer::new();
495 let empty: Vec<FunctionMetrics> = vec![];
496 let metrics = analyzer.calculate_module_metrics(&empty);
497 assert_eq!(metrics.total_functions, 0);
498 assert_eq!(metrics.total_lines, 0);
499 assert_eq!(metrics.avg_cyclomatic_complexity, 0.0);
500 assert_eq!(metrics.max_cyclomatic_complexity, 0);
501 assert_eq!(metrics.avg_cognitive_complexity, 0.0);
502 assert_eq!(metrics.max_cognitive_complexity, 0);
503 }
504
505 #[test]
506 fn test_type_coverage_all_annotated() {
507 use smallvec::smallvec;
508 let analyzer = Analyzer::new();
509 let func = HirFunction {
510 name: "all_typed".to_string(),
511 params: smallvec![
512 HirParam {
513 name: Symbol::from("a"),
514 ty: Type::Int,
515 default: None,
516 is_vararg: false,
517 },
518 HirParam {
519 name: Symbol::from("b"),
520 ty: Type::String,
521 default: None,
522 is_vararg: false,
523 },
524 ],
525 ret_type: Type::Bool,
526 body: vec![],
527 properties: FunctionProperties::default(),
528 annotations: TranspilationAnnotations::default(),
529 docstring: None,
530 };
531 let module = HirModule {
532 functions: vec![func],
533 imports: vec![],
534 type_aliases: vec![],
535 protocols: vec![],
536 classes: vec![],
537 constants: vec![],
538 top_level_stmts: vec![],
539 };
540 let coverage = analyzer.calculate_type_coverage(&module);
541 assert_eq!(coverage.coverage_percentage, 100.0);
542 }
543
544 #[test]
549 fn test_s9b7_analyze_function_with_nested_if() {
550 use smallvec::smallvec;
551 let analyzer = Analyzer::new();
552 let func = HirFunction {
553 name: "nested".to_string(),
554 params: smallvec![],
555 ret_type: Type::Int,
556 body: vec![HirStmt::If {
557 condition: HirExpr::Literal(Literal::Bool(true)),
558 then_body: vec![HirStmt::If {
559 condition: HirExpr::Literal(Literal::Bool(false)),
560 then_body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(1))))],
561 else_body: None,
562 }],
563 else_body: Some(vec![HirStmt::Return(Some(HirExpr::Literal(
564 Literal::Int(2),
565 )))]),
566 }],
567 properties: FunctionProperties::default(),
568 annotations: TranspilationAnnotations::default(),
569 docstring: None,
570 };
571 let module = HirModule {
572 functions: vec![func],
573 imports: vec![],
574 type_aliases: vec![],
575 protocols: vec![],
576 classes: vec![],
577 constants: vec![],
578 top_level_stmts: vec![],
579 };
580 let result = analyzer.analyze(&module).unwrap();
581 let fm = &result.function_metrics[0];
582 assert!(fm.cyclomatic_complexity >= 3);
583 assert!(fm.max_nesting_depth >= 2);
584 }
585
586 #[test]
587 fn test_s9b7_type_coverage_no_functions() {
588 let analyzer = Analyzer::new();
589 let module = HirModule {
590 functions: vec![],
591 imports: vec![],
592 type_aliases: vec![],
593 protocols: vec![],
594 classes: vec![],
595 constants: vec![],
596 top_level_stmts: vec![],
597 };
598 let coverage = analyzer.calculate_type_coverage(&module);
599 assert_eq!(coverage.total_parameters, 0);
600 assert_eq!(coverage.annotated_parameters, 0);
601 assert_eq!(coverage.total_functions, 0);
602 assert_eq!(coverage.functions_with_return_type, 0);
603 assert_eq!(coverage.coverage_percentage, 100.0);
604 }
605
606 #[test]
607 fn test_s9b7_analyze_function_empty_body() {
608 use smallvec::smallvec;
609 let analyzer = Analyzer::new();
610 let func = HirFunction {
611 name: "empty_body".to_string(),
612 params: smallvec![],
613 ret_type: Type::None,
614 body: vec![],
615 properties: FunctionProperties::default(),
616 annotations: TranspilationAnnotations::default(),
617 docstring: None,
618 };
619 let result = analyzer.analyze_function(&func).unwrap();
620 assert_eq!(result.name, "empty_body");
621 assert_eq!(result.lines_of_code, 0);
622 assert_eq!(result.parameters, 0);
623 assert_eq!(result.max_nesting_depth, 0);
624 assert!(result.has_type_annotations);
625 assert!(result.return_type_annotated);
626 }
627
628 #[test]
629 fn test_s9b7_type_coverage_mixed_annotations() {
630 use smallvec::smallvec;
631 let analyzer = Analyzer::new();
632 let func = HirFunction {
633 name: "mixed".to_string(),
634 params: smallvec![
635 HirParam {
636 name: Symbol::from("a"),
637 ty: Type::Int,
638 default: None,
639 is_vararg: false,
640 },
641 HirParam {
642 name: Symbol::from("b"),
643 ty: Type::Unknown,
644 default: None,
645 is_vararg: false,
646 },
647 HirParam {
648 name: Symbol::from("c"),
649 ty: Type::Float,
650 default: None,
651 is_vararg: false,
652 },
653 ],
654 ret_type: Type::Unknown,
655 body: vec![],
656 properties: FunctionProperties::default(),
657 annotations: TranspilationAnnotations::default(),
658 docstring: None,
659 };
660 let module = HirModule {
661 functions: vec![func],
662 imports: vec![],
663 type_aliases: vec![],
664 protocols: vec![],
665 classes: vec![],
666 constants: vec![],
667 top_level_stmts: vec![],
668 };
669 let coverage = analyzer.calculate_type_coverage(&module);
670 assert_eq!(coverage.total_parameters, 3);
671 assert_eq!(coverage.annotated_parameters, 2);
672 assert_eq!(coverage.functions_with_return_type, 0);
673 assert!((coverage.coverage_percentage - 50.0).abs() < 0.01);
675 }
676
677 #[test]
678 fn test_s9b7_module_metrics_single_function() {
679 let analyzer = Analyzer::new();
680 let metrics = vec![FunctionMetrics {
681 name: "solo".to_string(),
682 cyclomatic_complexity: 5,
683 cognitive_complexity: 3,
684 lines_of_code: 10,
685 parameters: 2,
686 max_nesting_depth: 1,
687 has_type_annotations: true,
688 return_type_annotated: true,
689 }];
690 let mm = analyzer.calculate_module_metrics(&metrics);
691 assert_eq!(mm.total_functions, 1);
692 assert_eq!(mm.total_lines, 10);
693 assert_eq!(mm.avg_cyclomatic_complexity, 5.0);
694 assert_eq!(mm.max_cyclomatic_complexity, 5);
695 assert_eq!(mm.avg_cognitive_complexity, 3.0);
696 assert_eq!(mm.max_cognitive_complexity, 3);
697 }
698
699 #[test]
700 fn test_s9b7_analysis_result_debug_clone() {
701 let result = AnalysisResult {
702 module_metrics: ModuleMetrics {
703 total_functions: 2,
704 total_lines: 20,
705 avg_cyclomatic_complexity: 3.0,
706 max_cyclomatic_complexity: 4,
707 avg_cognitive_complexity: 2.0,
708 max_cognitive_complexity: 3,
709 },
710 function_metrics: vec![],
711 type_coverage: TypeCoverage {
712 total_parameters: 0,
713 annotated_parameters: 0,
714 total_functions: 0,
715 functions_with_return_type: 0,
716 coverage_percentage: 100.0,
717 },
718 };
719 let debug = format!("{:?}", result);
720 assert!(debug.contains("AnalysisResult"));
721 let cloned = result.clone();
722 assert_eq!(cloned.module_metrics.total_functions, 2);
723 }
724
725 #[test]
726 fn test_s9b7_analyze_function_partial_type_annotations() {
727 use smallvec::smallvec;
728 let analyzer = Analyzer::new();
729 let func = HirFunction {
730 name: "partial".to_string(),
731 params: smallvec![
732 HirParam {
733 name: Symbol::from("x"),
734 ty: Type::Int,
735 default: None,
736 is_vararg: false,
737 },
738 HirParam {
739 name: Symbol::from("y"),
740 ty: Type::Unknown,
741 default: None,
742 is_vararg: false,
743 },
744 ],
745 ret_type: Type::String,
746 body: vec![],
747 properties: FunctionProperties::default(),
748 annotations: TranspilationAnnotations::default(),
749 docstring: None,
750 };
751 let result = analyzer.analyze_function(&func).unwrap();
752 assert!(!result.has_type_annotations);
754 assert!(result.return_type_annotated);
755 }
756
757 #[test]
758 fn test_module_metrics_calculation() {
759 let analyzer = Analyzer::new();
760 let metrics = vec![
761 FunctionMetrics {
762 name: "func1".to_string(),
763 cyclomatic_complexity: 2,
764 cognitive_complexity: 3,
765 lines_of_code: 5,
766 parameters: 1,
767 max_nesting_depth: 1,
768 has_type_annotations: true,
769 return_type_annotated: true,
770 },
771 FunctionMetrics {
772 name: "func2".to_string(),
773 cyclomatic_complexity: 4,
774 cognitive_complexity: 6,
775 lines_of_code: 10,
776 parameters: 2,
777 max_nesting_depth: 2,
778 has_type_annotations: false,
779 return_type_annotated: false,
780 },
781 ];
782
783 let module_metrics = analyzer.calculate_module_metrics(&metrics);
784 assert_eq!(module_metrics.total_functions, 2);
785 assert_eq!(module_metrics.total_lines, 15);
786 assert_eq!(module_metrics.avg_cyclomatic_complexity, 3.0);
787 assert_eq!(module_metrics.max_cyclomatic_complexity, 4);
788 assert_eq!(module_metrics.avg_cognitive_complexity, 4.5);
789 assert_eq!(module_metrics.max_cognitive_complexity, 6);
790 }
791}