cargo_coupling/
aposd.rs

1//! A Philosophy of Software Design (APOSD) metrics detection
2//!
3//! Based on John Ousterhout's "A Philosophy of Software Design" (2nd Edition),
4//! this module detects design patterns and anti-patterns that contribute to
5//! software complexity.
6//!
7//! ## Key Concepts
8//!
9//! ### Deep vs Shallow Modules
10//! - **Deep modules**: Simple interfaces hiding complex implementations (good)
11//! - **Shallow modules**: Complex interfaces with simple implementations (bad)
12//!
13//! ### Information Hiding
14//! - Modules should encapsulate design decisions
15//! - Information leakage across boundaries indicates poor design
16//!
17//! ### Pass-through Methods
18//! - Methods that only delegate to another method without adding value
19//! - Indicates unclear responsibility division
20//!
21//! ## References
22//! - John Ousterhout, "A Philosophy of Software Design" (2nd Edition, 2021)
23//! - <https://web.stanford.edu/~ouster/cgi-bin/aposd.php>
24
25use 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/// Metrics for measuring module depth (interface vs implementation complexity)
35#[derive(Debug, Clone, Default)]
36pub struct ModuleDepthMetrics {
37    /// Module name/path
38    pub module_name: String,
39
40    // Interface complexity metrics
41    /// Number of public functions
42    pub pub_function_count: usize,
43    /// Number of public types (structs, enums, traits)
44    pub pub_type_count: usize,
45    /// Total parameters across all public functions
46    pub total_pub_params: usize,
47    /// Number of generic type parameters in public API
48    pub generic_param_count: usize,
49    /// Number of trait bounds in public API
50    pub trait_bound_count: usize,
51    /// Number of public constants
52    pub pub_const_count: usize,
53
54    // Implementation complexity metrics
55    /// Total lines of code (excluding comments/blanks)
56    pub implementation_loc: usize,
57    /// Number of private functions
58    pub private_function_count: usize,
59    /// Number of private types
60    pub private_type_count: usize,
61    /// Cyclomatic complexity estimate (branches, loops, etc.)
62    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    /// Calculate interface complexity score
74    ///
75    /// Higher score = more complex interface
76    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    /// Calculate implementation complexity score
93    ///
94    /// Higher score = more complex implementation (hidden behind interface)
95    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    /// Calculate module depth ratio
105    ///
106    /// Depth = Implementation Complexity / Interface Complexity
107    ///
108    /// - High ratio (> 5.0): Deep module (good - hides complexity)
109    /// - Low ratio (< 2.0): Shallow module (bad - interface as complex as implementation)
110    ///
111    /// Returns None if interface complexity is 0
112    pub fn depth_ratio(&self) -> Option<f64> {
113        let interface = self.interface_complexity();
114        if interface < 0.01 {
115            return None; // Avoid division by zero
116        }
117
118        let implementation = self.implementation_complexity();
119        Some(implementation / interface)
120    }
121
122    /// Classify module depth
123    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    /// Check if this module is considered shallow (a red flag)
135    pub fn is_shallow(&self) -> bool {
136        matches!(
137            self.depth_classification(),
138            ModuleDepthClass::Shallow | ModuleDepthClass::VeryShallow
139        )
140    }
141
142    /// Calculate average parameters per public function
143    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/// Classification of module depth
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum ModuleDepthClass {
154    /// Ratio >= 10.0: Excellent abstraction (like Unix I/O)
155    VeryDeep,
156    /// Ratio >= 5.0: Good abstraction
157    Deep,
158    /// Ratio >= 2.0: Acceptable
159    Moderate,
160    /// Ratio >= 1.0: Interface nearly as complex as implementation
161    Shallow,
162    /// Ratio < 1.0: Interface MORE complex than implementation
163    VeryShallow,
164    /// Cannot calculate (no public interface)
165    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/// Metrics for detecting pass-through methods
182#[derive(Debug, Clone)]
183pub struct PassThroughMethodInfo {
184    /// Method name
185    pub method_name: String,
186    /// Module where the method is defined
187    pub module_name: String,
188    /// The delegated method being called
189    pub delegated_to: String,
190    /// Number of parameters passed through unchanged
191    pub params_passed_through: usize,
192    /// Total number of parameters
193    pub total_params: usize,
194    /// Whether this is likely a pass-through (heuristic)
195    pub is_passthrough: bool,
196    /// Confidence score (0.0 - 1.0)
197    pub confidence: f64,
198}
199
200impl PassThroughMethodInfo {
201    /// Calculate pass-through ratio
202    pub fn passthrough_ratio(&self) -> f64 {
203        if self.total_params == 0 {
204            return 1.0; // No params = full pass-through
205        }
206        self.params_passed_through as f64 / self.total_params as f64
207    }
208}
209
210/// Cognitive load metrics for a module
211#[derive(Debug, Clone, Default)]
212pub struct CognitiveLoadMetrics {
213    /// Module name
214    pub module_name: String,
215
216    /// Number of public API items (functions, types, constants)
217    pub public_api_count: usize,
218    /// Number of dependencies (other modules this depends on)
219    pub dependency_count: usize,
220    /// Average function parameter count
221    pub avg_param_count: f64,
222    /// Number of different types used in public API
223    pub type_variety: usize,
224    /// Number of generic type parameters
225    pub generics_count: usize,
226    /// Number of trait bounds
227    pub trait_bounds_count: usize,
228    /// Maximum nesting depth in the module
229    pub max_nesting_depth: usize,
230    /// Number of control flow branches (if, match, loop)
231    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    /// Calculate cognitive load score
243    ///
244    /// Higher score = higher cognitive load (harder to understand)
245    pub fn cognitive_load_score(&self) -> f64 {
246        // Weights based on cognitive psychology research
247        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    /// Classify cognitive load level
267    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    /// Check if cognitive load is problematic
278    pub fn is_high_load(&self) -> bool {
279        matches!(
280            self.load_classification(),
281            CognitiveLoadLevel::High | CognitiveLoadLevel::VeryHigh
282        )
283    }
284}
285
286/// Classification of cognitive load
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum CognitiveLoadLevel {
289    /// Easy to understand
290    Low,
291    /// Manageable complexity
292    Moderate,
293    /// Requires significant effort to understand
294    High,
295    /// Overwhelming complexity
296    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/// Summary of APOSD metrics for a project
311#[derive(Debug, Default)]
312pub struct AposdAnalysis {
313    /// Module depth metrics for each module
314    pub module_depths: HashMap<String, ModuleDepthMetrics>,
315    /// Detected pass-through methods
316    pub passthrough_methods: Vec<PassThroughMethodInfo>,
317    /// Cognitive load metrics for each module
318    pub cognitive_loads: HashMap<String, CognitiveLoadMetrics>,
319}
320
321impl AposdAnalysis {
322    pub fn new() -> Self {
323        Self::default()
324    }
325
326    /// Get all shallow modules
327    pub fn shallow_modules(&self) -> Vec<&ModuleDepthMetrics> {
328        self.module_depths
329            .values()
330            .filter(|m| m.is_shallow())
331            .collect()
332    }
333
334    /// Get all high cognitive load modules
335    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    /// Get pass-through methods with high confidence
343    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    /// Calculate overall project depth score
351    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    /// Calculate overall cognitive load score
366    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    /// Count of issues by category
381    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/// Summary counts of APOSD issues
391#[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    /// Total number of APOSD issues
400    pub fn total(&self) -> usize {
401        self.shallow_modules + self.passthrough_methods + self.high_cognitive_load
402    }
403
404    /// Check if there are any issues
405    pub fn has_issues(&self) -> bool {
406        self.total() > 0
407    }
408}
409
410// ============================================================================
411// APOSD Analyzer - Analyzes project for APOSD patterns
412// ============================================================================
413
414/// Analyze a project for APOSD metrics
415///
416/// This analyzer computes module depth, cognitive load, and detects
417/// pass-through methods based on AST analysis.
418pub fn analyze_aposd(
419    _path: &Path,
420    project_metrics: &ProjectMetrics,
421    config: &AposdConfig,
422) -> AposdAnalysis {
423    let mut analysis = AposdAnalysis::new();
424
425    // Analyze each module for depth and cognitive load
426    for (module_name, module_metrics) in &project_metrics.modules {
427        // Calculate module depth
428        let mut depth = ModuleDepthMetrics::new(module_name.clone());
429
430        // Count public items from the module metrics
431        depth.pub_type_count = module_metrics.public_type_count();
432        depth.private_type_count = module_metrics.private_type_count();
433
434        // Analyze the source file for more detailed metrics
435        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            // Detect pass-through methods
445            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        // Calculate cognitive load
463        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
481/// Internal file metrics from AST analysis
482struct 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
501/// AST visitor for APOSD metrics
502struct 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    /// Check if a method name is a Rust idiomatic pattern that should not be flagged
543    fn is_rust_idiomatic_method(&self, name: &str) -> bool {
544        // Check if Rust idiom exclusion is disabled
545        if !self.config.exclude_rust_idioms {
546            // Only check custom exclusions
547            return self.is_custom_excluded_method(name);
548        }
549
550        // Rust-specific patterns that are intentionally simple delegations:
551
552        // 1. Conversion methods (AsRef, AsMut, Into, From patterns)
553        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        // 2. Accessor patterns (getters/setters)
562        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        // 3. Common trait method names
571        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        // 4. Builder pattern methods (typically return Self)
595        if name.starts_with("with_") || name.starts_with("and_") {
596            return true;
597        }
598
599        // 5. Iterator adaptor patterns
600        if name == "iter" || name == "iter_mut" || name == "into_iter" {
601            return true;
602        }
603
604        // 6. Common simple accessors
605        let simple_accessors = ["len", "is_empty", "capacity", "inner", "get", "new"];
606        if simple_accessors.contains(&name) {
607            return true;
608        }
609
610        // 7. Check custom exclusions from config
611        self.is_custom_excluded_method(name)
612    }
613
614    /// Check if a method is excluded via custom config
615    fn is_custom_excluded_method(&self, name: &str) -> bool {
616        // Check custom exclude_prefixes
617        for prefix in &self.config.exclude_prefixes {
618            if name.starts_with(prefix) {
619                return true;
620            }
621        }
622
623        // Check custom exclude_methods
624        if self.config.exclude_methods.contains(&name.to_string()) {
625            return true;
626        }
627
628        false
629    }
630
631    /// Check if the expression uses error propagation with `?` operator
632    fn uses_error_propagation(expr: &Expr) -> bool {
633        matches!(expr, Expr::Try(_))
634    }
635
636    /// Check if a function body is a simple delegation (pass-through)
637    fn check_passthrough(&mut self, name: &str, sig: &syn::Signature, block: &syn::Block) {
638        // A pass-through method typically has:
639        // 1. A single statement or expression
640        // 2. That expression is a method call or function call
641        // 3. Most parameters are passed through unchanged
642
643        // Skip Rust idiomatic patterns - these are intentional, not design issues
644        if self.is_rust_idiomatic_method(name) {
645            return;
646        }
647
648        let total_params = self.count_params(sig);
649
650        // Check if block has a single expression or single return
651        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        // Analyze the single statement
665        if let Some(Stmt::Expr(expr, _)) = block.stmts.first() {
666            // Skip methods that use `?` for error propagation - this is idiomatic Rust
667            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                // Consider it a pass-through if most params are passed through
679                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                // Try to extract a readable name from the function expression
710                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        // Check for pass-through pattern
748        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                // Check for pass-through pattern
767                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    // Count complexity indicators
779    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
790/// Analyze a file for APOSD metrics
791fn 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        // Simple interface, complex implementation = deep module
819        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        // VeryDeep because ratio is >= 10.0
828        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        // Complex interface, simple implementation = shallow module
843        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        // High load module
864        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        // Add a shallow module
894        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        // Add a deep module
903        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}