1use std::collections::HashMap;
26use std::fs;
27use std::path::Path;
28
29use syn::{visit::Visit, Expr, ItemFn, ItemImpl, Stmt};
30
31use crate::config::AposdConfig;
32use crate::metrics::ProjectMetrics;
33
34#[derive(Debug, Clone, Default)]
36pub struct ModuleDepthMetrics {
37 pub module_name: String,
39
40 pub pub_function_count: usize,
43 pub pub_type_count: usize,
45 pub total_pub_params: usize,
47 pub generic_param_count: usize,
49 pub trait_bound_count: usize,
51 pub pub_const_count: usize,
53
54 pub implementation_loc: usize,
57 pub private_function_count: usize,
59 pub private_type_count: usize,
61 pub complexity_estimate: usize,
63}
64
65impl ModuleDepthMetrics {
66 pub fn new(module_name: String) -> Self {
67 Self {
68 module_name,
69 ..Default::default()
70 }
71 }
72
73 pub fn interface_complexity(&self) -> f64 {
77 let fn_complexity = self.pub_function_count as f64 * 1.0;
78 let type_complexity = self.pub_type_count as f64 * 0.5;
79 let param_complexity = self.total_pub_params as f64 * 0.3;
80 let generic_complexity = self.generic_param_count as f64 * 0.5;
81 let trait_complexity = self.trait_bound_count as f64 * 0.3;
82 let const_complexity = self.pub_const_count as f64 * 0.1;
83
84 fn_complexity
85 + type_complexity
86 + param_complexity
87 + generic_complexity
88 + trait_complexity
89 + const_complexity
90 }
91
92 pub fn implementation_complexity(&self) -> f64 {
96 let loc_complexity = self.implementation_loc as f64 * 0.1;
97 let private_fn_complexity = self.private_function_count as f64 * 1.0;
98 let private_type_complexity = self.private_type_count as f64 * 0.5;
99 let cyclomatic_complexity = self.complexity_estimate as f64 * 0.5;
100
101 loc_complexity + private_fn_complexity + private_type_complexity + cyclomatic_complexity
102 }
103
104 pub fn depth_ratio(&self) -> Option<f64> {
113 let interface = self.interface_complexity();
114 if interface < 0.01 {
115 return None; }
117
118 let implementation = self.implementation_complexity();
119 Some(implementation / interface)
120 }
121
122 pub fn depth_classification(&self) -> ModuleDepthClass {
124 match self.depth_ratio() {
125 None => ModuleDepthClass::Unknown,
126 Some(ratio) if ratio >= 10.0 => ModuleDepthClass::VeryDeep,
127 Some(ratio) if ratio >= 5.0 => ModuleDepthClass::Deep,
128 Some(ratio) if ratio >= 2.0 => ModuleDepthClass::Moderate,
129 Some(ratio) if ratio >= 1.0 => ModuleDepthClass::Shallow,
130 Some(_) => ModuleDepthClass::VeryShallow,
131 }
132 }
133
134 pub fn is_shallow(&self) -> bool {
136 matches!(
137 self.depth_classification(),
138 ModuleDepthClass::Shallow | ModuleDepthClass::VeryShallow
139 )
140 }
141
142 pub fn avg_params_per_function(&self) -> f64 {
144 if self.pub_function_count == 0 {
145 return 0.0;
146 }
147 self.total_pub_params as f64 / self.pub_function_count as f64
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum ModuleDepthClass {
154 VeryDeep,
156 Deep,
158 Moderate,
160 Shallow,
162 VeryShallow,
164 Unknown,
166}
167
168impl std::fmt::Display for ModuleDepthClass {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 ModuleDepthClass::VeryDeep => write!(f, "Very Deep"),
172 ModuleDepthClass::Deep => write!(f, "Deep"),
173 ModuleDepthClass::Moderate => write!(f, "Moderate"),
174 ModuleDepthClass::Shallow => write!(f, "Shallow"),
175 ModuleDepthClass::VeryShallow => write!(f, "Very Shallow"),
176 ModuleDepthClass::Unknown => write!(f, "Unknown"),
177 }
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct PassThroughMethodInfo {
184 pub method_name: String,
186 pub module_name: String,
188 pub delegated_to: String,
190 pub params_passed_through: usize,
192 pub total_params: usize,
194 pub is_passthrough: bool,
196 pub confidence: f64,
198}
199
200impl PassThroughMethodInfo {
201 pub fn passthrough_ratio(&self) -> f64 {
203 if self.total_params == 0 {
204 return 1.0; }
206 self.params_passed_through as f64 / self.total_params as f64
207 }
208}
209
210#[derive(Debug, Clone, Default)]
212pub struct CognitiveLoadMetrics {
213 pub module_name: String,
215
216 pub public_api_count: usize,
218 pub dependency_count: usize,
220 pub avg_param_count: f64,
222 pub type_variety: usize,
224 pub generics_count: usize,
226 pub trait_bounds_count: usize,
228 pub max_nesting_depth: usize,
230 pub branch_count: usize,
232}
233
234impl CognitiveLoadMetrics {
235 pub fn new(module_name: String) -> Self {
236 Self {
237 module_name,
238 ..Default::default()
239 }
240 }
241
242 pub fn cognitive_load_score(&self) -> f64 {
246 let api_weight = self.public_api_count as f64 * 0.25;
248 let dep_weight = self.dependency_count as f64 * 0.20;
249 let param_weight = self.avg_param_count * 0.15;
250 let type_weight = self.type_variety as f64 * 0.10;
251 let generic_weight = self.generics_count as f64 * 0.10;
252 let trait_weight = self.trait_bounds_count as f64 * 0.10;
253 let nesting_weight = self.max_nesting_depth as f64 * 0.05;
254 let branch_weight = self.branch_count as f64 * 0.05;
255
256 api_weight
257 + dep_weight
258 + param_weight
259 + type_weight
260 + generic_weight
261 + trait_weight
262 + nesting_weight
263 + branch_weight
264 }
265
266 pub fn load_classification(&self) -> CognitiveLoadLevel {
268 let score = self.cognitive_load_score();
269 match score {
270 s if s < 5.0 => CognitiveLoadLevel::Low,
271 s if s < 15.0 => CognitiveLoadLevel::Moderate,
272 s if s < 30.0 => CognitiveLoadLevel::High,
273 _ => CognitiveLoadLevel::VeryHigh,
274 }
275 }
276
277 pub fn is_high_load(&self) -> bool {
279 matches!(
280 self.load_classification(),
281 CognitiveLoadLevel::High | CognitiveLoadLevel::VeryHigh
282 )
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum CognitiveLoadLevel {
289 Low,
291 Moderate,
293 High,
295 VeryHigh,
297}
298
299impl std::fmt::Display for CognitiveLoadLevel {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 match self {
302 CognitiveLoadLevel::Low => write!(f, "Low"),
303 CognitiveLoadLevel::Moderate => write!(f, "Moderate"),
304 CognitiveLoadLevel::High => write!(f, "High"),
305 CognitiveLoadLevel::VeryHigh => write!(f, "Very High"),
306 }
307 }
308}
309
310#[derive(Debug, Default)]
312pub struct AposdAnalysis {
313 pub module_depths: HashMap<String, ModuleDepthMetrics>,
315 pub passthrough_methods: Vec<PassThroughMethodInfo>,
317 pub cognitive_loads: HashMap<String, CognitiveLoadMetrics>,
319}
320
321impl AposdAnalysis {
322 pub fn new() -> Self {
323 Self::default()
324 }
325
326 pub fn shallow_modules(&self) -> Vec<&ModuleDepthMetrics> {
328 self.module_depths
329 .values()
330 .filter(|m| m.is_shallow())
331 .collect()
332 }
333
334 pub fn high_load_modules(&self) -> Vec<&CognitiveLoadMetrics> {
336 self.cognitive_loads
337 .values()
338 .filter(|m| m.is_high_load())
339 .collect()
340 }
341
342 pub fn confirmed_passthroughs(&self) -> Vec<&PassThroughMethodInfo> {
344 self.passthrough_methods
345 .iter()
346 .filter(|m| m.is_passthrough && m.confidence > 0.7)
347 .collect()
348 }
349
350 pub fn average_depth_ratio(&self) -> Option<f64> {
352 let ratios: Vec<f64> = self
353 .module_depths
354 .values()
355 .filter_map(|m| m.depth_ratio())
356 .collect();
357
358 if ratios.is_empty() {
359 return None;
360 }
361
362 Some(ratios.iter().sum::<f64>() / ratios.len() as f64)
363 }
364
365 pub fn average_cognitive_load(&self) -> f64 {
367 if self.cognitive_loads.is_empty() {
368 return 0.0;
369 }
370
371 let sum: f64 = self
372 .cognitive_loads
373 .values()
374 .map(|m| m.cognitive_load_score())
375 .sum();
376
377 sum / self.cognitive_loads.len() as f64
378 }
379
380 pub fn issue_counts(&self) -> AposdIssueCounts {
382 AposdIssueCounts {
383 shallow_modules: self.shallow_modules().len(),
384 passthrough_methods: self.confirmed_passthroughs().len(),
385 high_cognitive_load: self.high_load_modules().len(),
386 }
387 }
388}
389
390#[derive(Debug, Clone, Default)]
392pub struct AposdIssueCounts {
393 pub shallow_modules: usize,
394 pub passthrough_methods: usize,
395 pub high_cognitive_load: usize,
396}
397
398impl AposdIssueCounts {
399 pub fn total(&self) -> usize {
401 self.shallow_modules + self.passthrough_methods + self.high_cognitive_load
402 }
403
404 pub fn has_issues(&self) -> bool {
406 self.total() > 0
407 }
408}
409
410pub fn analyze_aposd(
419 _path: &Path,
420 project_metrics: &ProjectMetrics,
421 config: &AposdConfig,
422) -> AposdAnalysis {
423 let mut analysis = AposdAnalysis::new();
424
425 for (module_name, module_metrics) in &project_metrics.modules {
427 let mut depth = ModuleDepthMetrics::new(module_name.clone());
429
430 depth.pub_type_count = module_metrics.public_type_count();
432 depth.private_type_count = module_metrics.private_type_count();
433
434 if let Ok(content) = fs::read_to_string(&module_metrics.path) {
436 let file_metrics = analyze_file_for_aposd(&content, config);
437 depth.pub_function_count = file_metrics.pub_function_count;
438 depth.total_pub_params = file_metrics.total_pub_params;
439 depth.generic_param_count = file_metrics.generic_param_count;
440 depth.implementation_loc = file_metrics.implementation_loc;
441 depth.private_function_count = file_metrics.private_function_count;
442 depth.complexity_estimate = file_metrics.complexity_estimate;
443
444 for pt in file_metrics.passthrough_candidates {
446 analysis.passthrough_methods.push(PassThroughMethodInfo {
447 method_name: pt.method_name,
448 module_name: module_name.clone(),
449 delegated_to: pt.delegated_to,
450 params_passed_through: pt.params_passed_through,
451 total_params: pt.total_params,
452 is_passthrough: pt.is_passthrough,
453 confidence: pt.confidence,
454 });
455 }
456 }
457
458 analysis
459 .module_depths
460 .insert(module_name.clone(), depth.clone());
461
462 let mut cognitive = CognitiveLoadMetrics::new(module_name.clone());
464 cognitive.public_api_count =
465 depth.pub_function_count + depth.pub_type_count + depth.pub_const_count;
466 cognitive.dependency_count =
467 module_metrics.external_deps.len() + module_metrics.internal_deps.len();
468 cognitive.avg_param_count = depth.avg_params_per_function();
469 cognitive.generics_count = depth.generic_param_count;
470 cognitive.trait_bounds_count = depth.trait_bound_count;
471 cognitive.branch_count = depth.complexity_estimate;
472
473 analysis
474 .cognitive_loads
475 .insert(module_name.clone(), cognitive);
476 }
477
478 analysis
479}
480
481struct FileAposdMetrics {
483 pub_function_count: usize,
484 total_pub_params: usize,
485 generic_param_count: usize,
486 implementation_loc: usize,
487 private_function_count: usize,
488 complexity_estimate: usize,
489 passthrough_candidates: Vec<PassThroughCandidate>,
490}
491
492struct PassThroughCandidate {
493 method_name: String,
494 delegated_to: String,
495 params_passed_through: usize,
496 total_params: usize,
497 is_passthrough: bool,
498 confidence: f64,
499}
500
501struct AposdVisitor<'a> {
503 pub_function_count: usize,
504 private_function_count: usize,
505 total_pub_params: usize,
506 generic_param_count: usize,
507 complexity_estimate: usize,
508 line_count: usize,
509 passthrough_candidates: Vec<PassThroughCandidate>,
510 config: &'a AposdConfig,
511}
512
513impl<'a> AposdVisitor<'a> {
514 fn new(config: &'a AposdConfig) -> Self {
515 Self {
516 pub_function_count: 0,
517 private_function_count: 0,
518 total_pub_params: 0,
519 generic_param_count: 0,
520 complexity_estimate: 0,
521 line_count: 0,
522 passthrough_candidates: Vec::new(),
523 config,
524 }
525 }
526
527 fn is_public(&self, vis: &syn::Visibility) -> bool {
528 matches!(vis, syn::Visibility::Public(_))
529 }
530
531 fn count_params(&self, sig: &syn::Signature) -> usize {
532 sig.inputs
533 .iter()
534 .filter(|arg| !matches!(arg, syn::FnArg::Receiver(_)))
535 .count()
536 }
537
538 fn count_generics(&self, generics: &syn::Generics) -> usize {
539 generics.type_params().count() + generics.lifetimes().count()
540 }
541
542 fn is_rust_idiomatic_method(&self, name: &str) -> bool {
544 if !self.config.exclude_rust_idioms {
546 return self.is_custom_excluded_method(name);
548 }
549
550 if name.starts_with("as_")
554 || name.starts_with("into_")
555 || name.starts_with("from_")
556 || name.starts_with("to_")
557 {
558 return true;
559 }
560
561 if name.starts_with("get_")
563 || name.starts_with("set_")
564 || name.ends_with("_ref")
565 || name.ends_with("_mut")
566 {
567 return true;
568 }
569
570 let trait_methods = [
572 "deref",
573 "deref_mut",
574 "as_ref",
575 "as_mut",
576 "borrow",
577 "borrow_mut",
578 "clone",
579 "default",
580 "eq",
581 "ne",
582 "partial_cmp",
583 "cmp",
584 "hash",
585 "fmt",
586 "drop",
587 "index",
588 "index_mut",
589 ];
590 if trait_methods.contains(&name) {
591 return true;
592 }
593
594 if name.starts_with("with_") || name.starts_with("and_") {
596 return true;
597 }
598
599 if name == "iter" || name == "iter_mut" || name == "into_iter" {
601 return true;
602 }
603
604 let simple_accessors = ["len", "is_empty", "capacity", "inner", "get", "new"];
606 if simple_accessors.contains(&name) {
607 return true;
608 }
609
610 self.is_custom_excluded_method(name)
612 }
613
614 fn is_custom_excluded_method(&self, name: &str) -> bool {
616 for prefix in &self.config.exclude_prefixes {
618 if name.starts_with(prefix) {
619 return true;
620 }
621 }
622
623 if self.config.exclude_methods.contains(&name.to_string()) {
625 return true;
626 }
627
628 false
629 }
630
631 fn uses_error_propagation(expr: &Expr) -> bool {
633 matches!(expr, Expr::Try(_))
634 }
635
636 fn check_passthrough(&mut self, name: &str, sig: &syn::Signature, block: &syn::Block) {
638 if self.is_rust_idiomatic_method(name) {
645 return;
646 }
647
648 let total_params = self.count_params(sig);
649
650 if block.stmts.len() != 1 {
652 return;
653 }
654
655 let is_single_expr = match &block.stmts[0] {
656 Stmt::Expr(expr, _) => self.is_simple_delegation(expr),
657 _ => false,
658 };
659
660 if !is_single_expr {
661 return;
662 }
663
664 if let Some(Stmt::Expr(expr, _)) = block.stmts.first() {
666 if Self::uses_error_propagation(expr) {
668 return;
669 }
670
671 if let Some((delegated_to, passed_through)) = self.analyze_delegation(expr) {
672 let passthrough_ratio = if total_params > 0 {
673 passed_through as f64 / total_params as f64
674 } else {
675 1.0
676 };
677
678 let is_passthrough = passthrough_ratio >= 0.8 && total_params > 0;
680 let confidence = passthrough_ratio;
681
682 self.passthrough_candidates.push(PassThroughCandidate {
683 method_name: name.to_string(),
684 delegated_to,
685 params_passed_through: passed_through,
686 total_params,
687 is_passthrough,
688 confidence,
689 });
690 }
691 }
692 }
693
694 fn is_simple_delegation(&self, expr: &Expr) -> bool {
695 matches!(
696 expr,
697 Expr::MethodCall(_) | Expr::Call(_) | Expr::Try(_) | Expr::Await(_)
698 )
699 }
700
701 fn analyze_delegation(&self, expr: &Expr) -> Option<(String, usize)> {
702 match expr {
703 Expr::MethodCall(mc) => {
704 let method_name = mc.method.to_string();
705 let args_count = mc.args.len();
706 Some((format!("self.{}", method_name), args_count))
707 }
708 Expr::Call(call) => {
709 let callee = match call.func.as_ref() {
711 Expr::Path(path) => {
712 path.path
713 .segments
714 .iter()
715 .map(|s| s.ident.to_string())
716 .collect::<Vec<_>>()
717 .join("::")
718 }
719 Expr::Field(field) => {
720 match &field.member {
721 syn::Member::Named(ident) => format!("_.{}", ident),
722 syn::Member::Unnamed(index) => format!("_.{}", index.index),
723 }
724 }
725 _ => "unknown".to_string(),
726 };
727 let args_count = call.args.len();
728 Some((callee, args_count))
729 }
730 Expr::Try(try_expr) => self.analyze_delegation(&try_expr.expr),
731 Expr::Await(await_expr) => self.analyze_delegation(&await_expr.base),
732 _ => None,
733 }
734 }
735}
736
737impl<'ast, 'a> Visit<'ast> for AposdVisitor<'a> {
738 fn visit_item_fn(&mut self, node: &'ast ItemFn) {
739 if self.is_public(&node.vis) {
740 self.pub_function_count += 1;
741 self.total_pub_params += self.count_params(&node.sig);
742 self.generic_param_count += self.count_generics(&node.sig.generics);
743 } else {
744 self.private_function_count += 1;
745 }
746
747 self.check_passthrough(&node.sig.ident.to_string(), &node.sig, &node.block);
749
750 syn::visit::visit_item_fn(self, node);
751 }
752
753 fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
754 for item in &node.items {
755 if let syn::ImplItem::Fn(method) = item {
756 let is_pub = matches!(method.vis, syn::Visibility::Public(_));
757
758 if is_pub {
759 self.pub_function_count += 1;
760 self.total_pub_params += self.count_params(&method.sig);
761 self.generic_param_count += self.count_generics(&method.sig.generics);
762 } else {
763 self.private_function_count += 1;
764 }
765
766 self.check_passthrough(
768 &method.sig.ident.to_string(),
769 &method.sig,
770 &method.block,
771 );
772 }
773 }
774
775 syn::visit::visit_item_impl(self, node);
776 }
777
778 fn visit_expr(&mut self, node: &'ast Expr) {
780 match node {
781 Expr::If(_) | Expr::Match(_) | Expr::While(_) | Expr::ForLoop(_) | Expr::Loop(_) => {
782 self.complexity_estimate += 1;
783 }
784 _ => {}
785 }
786 syn::visit::visit_expr(self, node);
787 }
788}
789
790fn analyze_file_for_aposd(content: &str, config: &AposdConfig) -> FileAposdMetrics {
792 let mut visitor = AposdVisitor::new(config);
793 visitor.line_count = content.lines().count();
794
795 if let Ok(syntax) = syn::parse_file(content) {
796 visitor.visit_file(&syntax);
797 }
798
799 FileAposdMetrics {
800 pub_function_count: visitor.pub_function_count,
801 total_pub_params: visitor.total_pub_params,
802 generic_param_count: visitor.generic_param_count,
803 implementation_loc: visitor.line_count,
804 private_function_count: visitor.private_function_count,
805 complexity_estimate: visitor.complexity_estimate,
806 passthrough_candidates: visitor.passthrough_candidates,
807 }
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
815 fn test_module_depth_calculation() {
816 let mut metrics = ModuleDepthMetrics::new("test_module".to_string());
817
818 metrics.pub_function_count = 2;
820 metrics.total_pub_params = 4;
821 metrics.implementation_loc = 200;
822 metrics.private_function_count = 10;
823 metrics.complexity_estimate = 20;
824
825 let ratio = metrics.depth_ratio().unwrap();
826 assert!(ratio > 5.0, "Expected deep module, got ratio: {}", ratio);
827 assert!(
829 matches!(
830 metrics.depth_classification(),
831 ModuleDepthClass::Deep | ModuleDepthClass::VeryDeep
832 ),
833 "Expected Deep or VeryDeep, got {:?}",
834 metrics.depth_classification()
835 );
836 }
837
838 #[test]
839 fn test_shallow_module_detection() {
840 let mut metrics = ModuleDepthMetrics::new("shallow_module".to_string());
841
842 metrics.pub_function_count = 10;
844 metrics.total_pub_params = 30;
845 metrics.pub_type_count = 5;
846 metrics.implementation_loc = 50;
847 metrics.private_function_count = 2;
848
849 assert!(metrics.is_shallow(), "Expected shallow module");
850 }
851
852 #[test]
853 fn test_cognitive_load_scoring() {
854 let mut load = CognitiveLoadMetrics::new("test".to_string());
855 load.public_api_count = 5;
856 load.dependency_count = 3;
857 load.avg_param_count = 2.0;
858
859 let score = load.cognitive_load_score();
860 assert!(score > 0.0);
861 assert_eq!(load.load_classification(), CognitiveLoadLevel::Low);
862
863 let mut high_load = CognitiveLoadMetrics::new("complex".to_string());
865 high_load.public_api_count = 50;
866 high_load.dependency_count = 20;
867 high_load.avg_param_count = 5.0;
868 high_load.generics_count = 10;
869 high_load.trait_bounds_count = 15;
870
871 assert!(high_load.is_high_load());
872 }
873
874 #[test]
875 fn test_passthrough_ratio() {
876 let passthrough = PassThroughMethodInfo {
877 method_name: "delegate".to_string(),
878 module_name: "wrapper".to_string(),
879 delegated_to: "inner.method".to_string(),
880 params_passed_through: 3,
881 total_params: 3,
882 is_passthrough: true,
883 confidence: 0.9,
884 };
885
886 assert_eq!(passthrough.passthrough_ratio(), 1.0);
887 }
888
889 #[test]
890 fn test_aposd_analysis_summary() {
891 let mut analysis = AposdAnalysis::new();
892
893 let mut shallow = ModuleDepthMetrics::new("shallow".to_string());
895 shallow.pub_function_count = 10;
896 shallow.total_pub_params = 20;
897 shallow.implementation_loc = 30;
898 analysis
899 .module_depths
900 .insert("shallow".to_string(), shallow);
901
902 let mut deep = ModuleDepthMetrics::new("deep".to_string());
904 deep.pub_function_count = 2;
905 deep.implementation_loc = 500;
906 deep.private_function_count = 20;
907 analysis.module_depths.insert("deep".to_string(), deep);
908
909 let counts = analysis.issue_counts();
910 assert_eq!(counts.shallow_modules, 1);
911 }
912}