cargo_coupling/
analyzer.rs

1//! AST analysis for coupling detection
2//!
3//! Uses `syn` to parse Rust source code and detect coupling patterns.
4//! Optionally uses `cargo metadata` for accurate workspace analysis.
5//! Supports parallel processing via Rayon for large projects.
6
7use std::collections::{HashMap, HashSet};
8use std::ffi::OsStr;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use rayon::prelude::*;
13use syn::visit::Visit;
14use syn::{
15    Expr, ExprCall, ExprField, ExprMethodCall, ExprStruct, File, FnArg, ItemFn, ItemImpl, ItemMod,
16    ItemStruct, ItemTrait, ItemUse, ReturnType, Signature, Type, UseTree,
17};
18use thiserror::Error;
19use walkdir::WalkDir;
20
21use crate::metrics::{
22    CouplingMetrics, Distance, IntegrationStrength, ModuleMetrics, ProjectMetrics, Visibility,
23    Volatility,
24};
25use crate::workspace::{WorkspaceError, WorkspaceInfo, resolve_crate_from_path};
26
27/// Convert syn's Visibility to our Visibility enum
28fn convert_visibility(vis: &syn::Visibility) -> Visibility {
29    match vis {
30        syn::Visibility::Public(_) => Visibility::Public,
31        syn::Visibility::Restricted(restricted) => {
32            // Check the path to determine the restriction type
33            let path_str = restricted
34                .path
35                .segments
36                .iter()
37                .map(|s| s.ident.to_string())
38                .collect::<Vec<_>>()
39                .join("::");
40
41            match path_str.as_str() {
42                "crate" => Visibility::PubCrate,
43                "super" => Visibility::PubSuper,
44                "self" => Visibility::Private, // pub(self) is effectively private
45                _ => Visibility::PubIn,        // pub(in path)
46            }
47        }
48        syn::Visibility::Inherited => Visibility::Private,
49    }
50}
51
52/// Errors that can occur during analysis
53#[derive(Error, Debug)]
54pub enum AnalyzerError {
55    #[error("Failed to read file: {0}")]
56    IoError(#[from] std::io::Error),
57
58    #[error("Failed to parse Rust file: {0}")]
59    ParseError(String),
60
61    #[error("Invalid path: {0}")]
62    InvalidPath(String),
63
64    #[error("Workspace error: {0}")]
65    WorkspaceError(#[from] WorkspaceError),
66}
67
68/// Represents a detected dependency
69#[derive(Debug, Clone)]
70pub struct Dependency {
71    /// Full path of the dependency (e.g., "crate::models::user")
72    pub path: String,
73    /// Type of dependency
74    pub kind: DependencyKind,
75    /// Line number where the dependency is declared
76    pub line: usize,
77    /// Usage context for more accurate strength determination
78    pub usage: UsageContext,
79}
80
81/// Kind of dependency
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum DependencyKind {
84    /// use crate::xxx or use super::xxx
85    InternalUse,
86    /// use external_crate::xxx
87    ExternalUse,
88    /// impl Trait for Type
89    TraitImpl,
90    /// impl Type
91    InherentImpl,
92    /// Type reference in struct fields, function params, etc.
93    TypeRef,
94}
95
96/// Context of how a dependency is used - determines Integration Strength
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub enum UsageContext {
99    /// Just imported, usage unknown
100    Import,
101    /// Used as a trait bound or trait impl
102    TraitBound,
103    /// Field access: `foo.bar`
104    FieldAccess,
105    /// Method call: `foo.method()`
106    MethodCall,
107    /// Function call: `Foo::new()` or `foo()`
108    FunctionCall,
109    /// Struct construction: `Foo { field: value }`
110    StructConstruction,
111    /// Type parameter: `Vec<Foo>`
112    TypeParameter,
113    /// Function parameter type
114    FunctionParameter,
115    /// Return type
116    ReturnType,
117    /// Inherent impl block
118    InherentImplBlock,
119}
120
121impl UsageContext {
122    /// Convert usage context to integration strength
123    pub fn to_strength(&self) -> IntegrationStrength {
124        match self {
125            // Intrusive: Direct access to internals
126            UsageContext::FieldAccess => IntegrationStrength::Intrusive,
127            UsageContext::StructConstruction => IntegrationStrength::Intrusive,
128            UsageContext::InherentImplBlock => IntegrationStrength::Intrusive,
129
130            // Functional: Depends on function signatures
131            UsageContext::MethodCall => IntegrationStrength::Functional,
132            UsageContext::FunctionCall => IntegrationStrength::Functional,
133            UsageContext::FunctionParameter => IntegrationStrength::Functional,
134            UsageContext::ReturnType => IntegrationStrength::Functional,
135
136            // Model: Uses data types
137            UsageContext::TypeParameter => IntegrationStrength::Model,
138            UsageContext::Import => IntegrationStrength::Model,
139
140            // Contract: Uses traits/interfaces
141            UsageContext::TraitBound => IntegrationStrength::Contract,
142        }
143    }
144}
145
146impl DependencyKind {
147    pub fn to_strength(&self) -> IntegrationStrength {
148        match self {
149            DependencyKind::TraitImpl => IntegrationStrength::Contract,
150            DependencyKind::InternalUse => IntegrationStrength::Model,
151            DependencyKind::ExternalUse => IntegrationStrength::Model,
152            DependencyKind::TypeRef => IntegrationStrength::Model,
153            DependencyKind::InherentImpl => IntegrationStrength::Intrusive,
154        }
155    }
156}
157
158/// AST visitor for coupling analysis
159#[derive(Debug)]
160pub struct CouplingAnalyzer {
161    /// Current module being analyzed
162    pub current_module: String,
163    /// File path
164    pub file_path: std::path::PathBuf,
165    /// Collected metrics
166    pub metrics: ModuleMetrics,
167    /// Detected dependencies
168    pub dependencies: Vec<Dependency>,
169    /// Defined types in this module
170    pub defined_types: HashSet<String>,
171    /// Defined traits in this module
172    pub defined_traits: HashSet<String>,
173    /// Defined functions in this module (name -> visibility)
174    pub defined_functions: HashMap<String, Visibility>,
175    /// Imported types (name -> full path)
176    imported_types: HashMap<String, String>,
177    /// Track unique dependencies to avoid duplicates
178    seen_dependencies: HashSet<(String, UsageContext)>,
179    /// Counts of each usage type for statistics
180    pub usage_counts: UsageCounts,
181    /// Type visibility map: type name -> visibility
182    pub type_visibility: HashMap<String, Visibility>,
183    /// Current item being analyzed (function name, struct name, etc.)
184    current_item: Option<(String, ItemKind)>,
185    /// Item-level dependencies (detailed tracking)
186    pub item_dependencies: Vec<ItemDependency>,
187}
188
189/// Statistics about usage patterns
190#[derive(Debug, Default, Clone)]
191pub struct UsageCounts {
192    pub field_accesses: usize,
193    pub method_calls: usize,
194    pub function_calls: usize,
195    pub struct_constructions: usize,
196    pub trait_bounds: usize,
197    pub type_parameters: usize,
198}
199
200/// Detailed dependency at the item level (function, struct, etc.)
201#[derive(Debug, Clone)]
202pub struct ItemDependency {
203    /// Source item (e.g., "fn analyze_project")
204    pub source_item: String,
205    /// Source item kind
206    pub source_kind: ItemKind,
207    /// Target (e.g., "ProjectMetrics" or "analyze_file")
208    pub target: String,
209    /// Target module (if known)
210    pub target_module: Option<String>,
211    /// Type of dependency
212    pub dep_type: ItemDepType,
213    /// Line number in source
214    pub line: usize,
215    /// The actual expression/code (e.g., "config.thresholds" or "self.couplings")
216    pub expression: Option<String>,
217}
218
219/// Kind of source item
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum ItemKind {
222    Function,
223    Method,
224    Struct,
225    Enum,
226    Trait,
227    Impl,
228    Module,
229}
230
231/// Type of item-level dependency
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum ItemDepType {
234    /// Calls a function: foo()
235    FunctionCall,
236    /// Calls a method: x.foo()
237    MethodCall,
238    /// Uses a type: Vec<Foo>
239    TypeUsage,
240    /// Accesses a field: x.field
241    FieldAccess,
242    /// Constructs a struct: Foo { ... }
243    StructConstruction,
244    /// Implements a trait: impl Trait for Type
245    TraitImpl,
246    /// Uses a trait bound: T: Trait
247    TraitBound,
248    /// Imports: use foo::Bar
249    Import,
250}
251
252impl CouplingAnalyzer {
253    /// Create a new analyzer for a module
254    pub fn new(module_name: String, path: std::path::PathBuf) -> Self {
255        Self {
256            current_module: module_name.clone(),
257            file_path: path.clone(),
258            metrics: ModuleMetrics::new(path, module_name),
259            dependencies: Vec::new(),
260            defined_types: HashSet::new(),
261            defined_traits: HashSet::new(),
262            defined_functions: HashMap::new(),
263            imported_types: HashMap::new(),
264            seen_dependencies: HashSet::new(),
265            usage_counts: UsageCounts::default(),
266            type_visibility: HashMap::new(),
267            current_item: None,
268            item_dependencies: Vec::new(),
269        }
270    }
271
272    /// Analyze a Rust source file
273    pub fn analyze_file(&mut self, content: &str) -> Result<(), AnalyzerError> {
274        let syntax: File =
275            syn::parse_file(content).map_err(|e| AnalyzerError::ParseError(e.to_string()))?;
276
277        self.visit_file(&syntax);
278
279        Ok(())
280    }
281
282    /// Add a dependency with deduplication
283    fn add_dependency(&mut self, path: String, kind: DependencyKind, usage: UsageContext) {
284        let key = (path.clone(), usage);
285        if self.seen_dependencies.contains(&key) {
286            return;
287        }
288        self.seen_dependencies.insert(key);
289
290        self.dependencies.push(Dependency {
291            path,
292            kind,
293            line: 0,
294            usage,
295        });
296    }
297
298    /// Record an item-level dependency with detailed tracking
299    fn add_item_dependency(
300        &mut self,
301        target: String,
302        dep_type: ItemDepType,
303        line: usize,
304        expression: Option<String>,
305    ) {
306        if let Some((ref source_item, source_kind)) = self.current_item {
307            // Determine target module
308            let target_module = self.imported_types.get(&target).cloned().or_else(|| {
309                if self.defined_types.contains(&target)
310                    || self.defined_functions.contains_key(&target)
311                {
312                    Some(self.current_module.clone())
313                } else {
314                    None
315                }
316            });
317
318            self.item_dependencies.push(ItemDependency {
319                source_item: source_item.clone(),
320                source_kind,
321                target,
322                target_module,
323                dep_type,
324                line,
325                expression,
326            });
327        }
328    }
329
330    /// Extract full path from UseTree recursively
331    fn extract_use_paths(&self, tree: &UseTree, prefix: &str) -> Vec<(String, DependencyKind)> {
332        let mut paths = Vec::new();
333
334        match tree {
335            UseTree::Path(path) => {
336                let new_prefix = if prefix.is_empty() {
337                    path.ident.to_string()
338                } else {
339                    format!("{}::{}", prefix, path.ident)
340                };
341                paths.extend(self.extract_use_paths(&path.tree, &new_prefix));
342            }
343            UseTree::Name(name) => {
344                let full_path = if prefix.is_empty() {
345                    name.ident.to_string()
346                } else {
347                    format!("{}::{}", prefix, name.ident)
348                };
349                let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
350                    DependencyKind::InternalUse
351                } else {
352                    DependencyKind::ExternalUse
353                };
354                paths.push((full_path, kind));
355            }
356            UseTree::Rename(rename) => {
357                let full_path = if prefix.is_empty() {
358                    rename.ident.to_string()
359                } else {
360                    format!("{}::{}", prefix, rename.ident)
361                };
362                let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
363                    DependencyKind::InternalUse
364                } else {
365                    DependencyKind::ExternalUse
366                };
367                paths.push((full_path, kind));
368            }
369            UseTree::Glob(_) => {
370                let full_path = format!("{}::*", prefix);
371                let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
372                    DependencyKind::InternalUse
373                } else {
374                    DependencyKind::ExternalUse
375                };
376                paths.push((full_path, kind));
377            }
378            UseTree::Group(group) => {
379                for item in &group.items {
380                    paths.extend(self.extract_use_paths(item, prefix));
381                }
382            }
383        }
384
385        paths
386    }
387
388    /// Extract type name from a Type
389    fn extract_type_name(&self, ty: &Type) -> Option<String> {
390        match ty {
391            Type::Path(type_path) => {
392                let segments: Vec<_> = type_path
393                    .path
394                    .segments
395                    .iter()
396                    .map(|s| s.ident.to_string())
397                    .collect();
398                Some(segments.join("::"))
399            }
400            Type::Reference(ref_type) => self.extract_type_name(&ref_type.elem),
401            Type::Slice(slice_type) => self.extract_type_name(&slice_type.elem),
402            Type::Array(array_type) => self.extract_type_name(&array_type.elem),
403            Type::Ptr(ptr_type) => self.extract_type_name(&ptr_type.elem),
404            Type::Paren(paren_type) => self.extract_type_name(&paren_type.elem),
405            Type::Group(group_type) => self.extract_type_name(&group_type.elem),
406            _ => None,
407        }
408    }
409
410    /// Analyze function signature for dependencies
411    fn analyze_signature(&mut self, sig: &Signature) {
412        // Analyze parameters
413        for arg in &sig.inputs {
414            if let FnArg::Typed(pat_type) = arg
415                && let Some(type_name) = self.extract_type_name(&pat_type.ty)
416                && !self.is_primitive_type(&type_name)
417            {
418                self.add_dependency(
419                    type_name,
420                    DependencyKind::TypeRef,
421                    UsageContext::FunctionParameter,
422                );
423            }
424        }
425
426        // Analyze return type
427        if let ReturnType::Type(_, ty) = &sig.output
428            && let Some(type_name) = self.extract_type_name(ty)
429            && !self.is_primitive_type(&type_name)
430        {
431            self.add_dependency(type_name, DependencyKind::TypeRef, UsageContext::ReturnType);
432        }
433    }
434
435    /// Check if a type should be ignored (primitives, self, or short variable names)
436    fn is_primitive_type(&self, type_name: &str) -> bool {
437        // Primitive types
438        if matches!(
439            type_name,
440            "bool"
441                | "char"
442                | "str"
443                | "u8"
444                | "u16"
445                | "u32"
446                | "u64"
447                | "u128"
448                | "usize"
449                | "i8"
450                | "i16"
451                | "i32"
452                | "i64"
453                | "i128"
454                | "isize"
455                | "f32"
456                | "f64"
457                | "String"
458                | "Self"
459                | "()"
460                | "Option"
461                | "Result"
462                | "Vec"
463                | "Box"
464                | "Rc"
465                | "Arc"
466                | "RefCell"
467                | "Cell"
468                | "Mutex"
469                | "RwLock"
470        ) {
471            return true;
472        }
473
474        // Short variable names (likely local variables, not types)
475        // Type names in Rust are typically PascalCase and longer
476        if type_name.len() <= 3 && type_name.chars().all(|c| c.is_lowercase()) {
477            return true;
478        }
479
480        // Self-references or obviously local
481        if type_name.starts_with("self") || type_name == "self" {
482            return true;
483        }
484
485        false
486    }
487}
488
489impl<'ast> Visit<'ast> for CouplingAnalyzer {
490    fn visit_item_use(&mut self, node: &'ast ItemUse) {
491        let paths = self.extract_use_paths(&node.tree, "");
492
493        for (path, kind) in paths {
494            // Skip self references
495            if path == "self" || path.starts_with("self::") {
496                continue;
497            }
498
499            // Track imported types for later resolution
500            if let Some(type_name) = path.split("::").last() {
501                self.imported_types
502                    .insert(type_name.to_string(), path.clone());
503            }
504
505            self.add_dependency(path.clone(), kind, UsageContext::Import);
506
507            // Update metrics
508            if kind == DependencyKind::InternalUse {
509                if !self.metrics.internal_deps.contains(&path) {
510                    self.metrics.internal_deps.push(path.clone());
511                }
512            } else if kind == DependencyKind::ExternalUse {
513                // Extract crate name
514                let crate_name = path.split("::").next().unwrap_or(&path).to_string();
515                if !self.metrics.external_deps.contains(&crate_name) {
516                    self.metrics.external_deps.push(crate_name);
517                }
518            }
519        }
520
521        syn::visit::visit_item_use(self, node);
522    }
523
524    fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
525        if let Some((_, trait_path, _)) = &node.trait_ {
526            // Trait implementation = Contract coupling
527            self.metrics.trait_impl_count += 1;
528
529            // Extract trait path
530            let trait_name: String = trait_path
531                .segments
532                .iter()
533                .map(|s| s.ident.to_string())
534                .collect::<Vec<_>>()
535                .join("::");
536
537            self.add_dependency(
538                trait_name,
539                DependencyKind::TraitImpl,
540                UsageContext::TraitBound,
541            );
542            self.usage_counts.trait_bounds += 1;
543        } else {
544            // Inherent implementation = Intrusive coupling
545            self.metrics.inherent_impl_count += 1;
546
547            // Get the type being implemented
548            if let Some(type_name) = self.extract_type_name(&node.self_ty)
549                && !self.defined_types.contains(&type_name)
550            {
551                self.add_dependency(
552                    type_name,
553                    DependencyKind::InherentImpl,
554                    UsageContext::InherentImplBlock,
555                );
556            }
557        }
558        syn::visit::visit_item_impl(self, node);
559    }
560
561    fn visit_item_fn(&mut self, node: &'ast ItemFn) {
562        // Record function definition
563        let fn_name = node.sig.ident.to_string();
564        let visibility = convert_visibility(&node.vis);
565        self.defined_functions.insert(fn_name.clone(), visibility);
566
567        // Analyze parameters for primitive obsession detection
568        let mut param_count = 0;
569        let mut primitive_param_count = 0;
570        let mut param_types = Vec::new();
571
572        for arg in &node.sig.inputs {
573            if let FnArg::Typed(pat_type) = arg {
574                param_count += 1;
575                if let Some(type_name) = self.extract_type_name(&pat_type.ty) {
576                    param_types.push(type_name.clone());
577                    if self.is_primitive_type(&type_name) {
578                        primitive_param_count += 1;
579                    }
580                }
581            }
582        }
583
584        // Register in module metrics with full details
585        self.metrics.add_function_definition_full(
586            fn_name.clone(),
587            visibility,
588            param_count,
589            primitive_param_count,
590            param_types,
591        );
592
593        // Set current item context for dependency tracking
594        let previous_item = self.current_item.take();
595        self.current_item = Some((fn_name, ItemKind::Function));
596
597        // Analyze function signature
598        self.analyze_signature(&node.sig);
599        syn::visit::visit_item_fn(self, node);
600
601        // Restore previous context
602        self.current_item = previous_item;
603    }
604
605    fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
606        let name = node.ident.to_string();
607        let visibility = convert_visibility(&node.vis);
608
609        self.defined_types.insert(name.clone());
610        self.type_visibility.insert(name.clone(), visibility);
611
612        // Detect newtype pattern: single-field tuple struct
613        let (is_newtype, inner_type) = match &node.fields {
614            syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
615                let inner = fields
616                    .unnamed
617                    .first()
618                    .and_then(|f| self.extract_type_name(&f.ty));
619                (true, inner)
620            }
621            _ => (false, None),
622        };
623
624        // Check for serde derives
625        let has_serde_derive = node.attrs.iter().any(|attr| {
626            if attr.path().is_ident("derive")
627                && let Ok(nested) = attr.parse_args_with(
628                    syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
629                )
630            {
631                return nested.iter().any(|path| {
632                    let path_str = path
633                        .segments
634                        .iter()
635                        .map(|s| s.ident.to_string())
636                        .collect::<Vec<_>>()
637                        .join("::");
638                    path_str == "Serialize"
639                        || path_str == "Deserialize"
640                        || path_str == "serde::Serialize"
641                        || path_str == "serde::Deserialize"
642                });
643            }
644            false
645        });
646
647        // Count fields and public fields
648        let (total_field_count, public_field_count) = match &node.fields {
649            syn::Fields::Named(fields) => {
650                let total = fields.named.len();
651                let public = fields
652                    .named
653                    .iter()
654                    .filter(|f| matches!(f.vis, syn::Visibility::Public(_)))
655                    .count();
656                (total, public)
657            }
658            syn::Fields::Unnamed(fields) => {
659                let total = fields.unnamed.len();
660                let public = fields
661                    .unnamed
662                    .iter()
663                    .filter(|f| matches!(f.vis, syn::Visibility::Public(_)))
664                    .count();
665                (total, public)
666            }
667            syn::Fields::Unit => (0, 0),
668        };
669
670        // Register in module metrics with full details
671        self.metrics.add_type_definition_full(
672            name,
673            visibility,
674            false, // is_trait
675            is_newtype,
676            inner_type,
677            has_serde_derive,
678            public_field_count,
679            total_field_count,
680        );
681
682        // Analyze struct fields for type dependencies
683        match &node.fields {
684            syn::Fields::Named(fields) => {
685                self.metrics.type_usage_count += fields.named.len();
686                for field in &fields.named {
687                    if let Some(type_name) = self.extract_type_name(&field.ty)
688                        && !self.is_primitive_type(&type_name)
689                    {
690                        self.add_dependency(
691                            type_name,
692                            DependencyKind::TypeRef,
693                            UsageContext::TypeParameter,
694                        );
695                        self.usage_counts.type_parameters += 1;
696                    }
697                }
698            }
699            syn::Fields::Unnamed(fields) => {
700                for field in &fields.unnamed {
701                    if let Some(type_name) = self.extract_type_name(&field.ty)
702                        && !self.is_primitive_type(&type_name)
703                    {
704                        self.add_dependency(
705                            type_name,
706                            DependencyKind::TypeRef,
707                            UsageContext::TypeParameter,
708                        );
709                    }
710                }
711            }
712            syn::Fields::Unit => {}
713        }
714        syn::visit::visit_item_struct(self, node);
715    }
716
717    fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
718        let name = node.ident.to_string();
719        let visibility = convert_visibility(&node.vis);
720
721        self.defined_types.insert(name.clone());
722        self.type_visibility.insert(name.clone(), visibility);
723
724        // Register in module metrics with visibility
725        self.metrics.add_type_definition(name, visibility, false);
726
727        // Analyze enum variants for type dependencies
728        for variant in &node.variants {
729            match &variant.fields {
730                syn::Fields::Named(fields) => {
731                    for field in &fields.named {
732                        if let Some(type_name) = self.extract_type_name(&field.ty)
733                            && !self.is_primitive_type(&type_name)
734                        {
735                            self.add_dependency(
736                                type_name,
737                                DependencyKind::TypeRef,
738                                UsageContext::TypeParameter,
739                            );
740                        }
741                    }
742                }
743                syn::Fields::Unnamed(fields) => {
744                    for field in &fields.unnamed {
745                        if let Some(type_name) = self.extract_type_name(&field.ty)
746                            && !self.is_primitive_type(&type_name)
747                        {
748                            self.add_dependency(
749                                type_name,
750                                DependencyKind::TypeRef,
751                                UsageContext::TypeParameter,
752                            );
753                        }
754                    }
755                }
756                syn::Fields::Unit => {}
757            }
758        }
759        syn::visit::visit_item_enum(self, node);
760    }
761
762    fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
763        let name = node.ident.to_string();
764        let visibility = convert_visibility(&node.vis);
765
766        self.defined_traits.insert(name.clone());
767        self.type_visibility.insert(name.clone(), visibility);
768
769        // Register in module metrics with visibility (is_trait = true)
770        self.metrics.add_type_definition(name, visibility, true);
771
772        self.metrics.trait_impl_count += 1;
773        syn::visit::visit_item_trait(self, node);
774    }
775
776    fn visit_item_mod(&mut self, node: &'ast ItemMod) {
777        if node.content.is_some() {
778            self.metrics.internal_deps.push(node.ident.to_string());
779        }
780        syn::visit::visit_item_mod(self, node);
781    }
782
783    // Detect field access: `foo.bar`
784    fn visit_expr_field(&mut self, node: &'ast ExprField) {
785        let field_name = match &node.member {
786            syn::Member::Named(ident) => ident.to_string(),
787            syn::Member::Unnamed(idx) => format!("{}", idx.index),
788        };
789
790        // This is a field access - Intrusive coupling
791        if let Expr::Path(path_expr) = &*node.base {
792            let base_name = path_expr
793                .path
794                .segments
795                .iter()
796                .map(|s| s.ident.to_string())
797                .collect::<Vec<_>>()
798                .join("::");
799
800            // Resolve to full path if imported
801            let full_path = self
802                .imported_types
803                .get(&base_name)
804                .cloned()
805                .unwrap_or(base_name.clone());
806
807            if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
808                self.add_dependency(
809                    full_path.clone(),
810                    DependencyKind::TypeRef,
811                    UsageContext::FieldAccess,
812                );
813                self.usage_counts.field_accesses += 1;
814            }
815
816            // Record item-level dependency with field name
817            let expr = format!("{}.{}", base_name, field_name);
818            self.add_item_dependency(
819                format!("{}.{}", full_path, field_name),
820                ItemDepType::FieldAccess,
821                0,
822                Some(expr),
823            );
824        }
825        syn::visit::visit_expr_field(self, node);
826    }
827
828    // Detect method calls: `foo.method()`
829    fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
830        let method_name = node.method.to_string();
831
832        // This is a method call - Functional coupling
833        if let Expr::Path(path_expr) = &*node.receiver {
834            let receiver_name = path_expr
835                .path
836                .segments
837                .iter()
838                .map(|s| s.ident.to_string())
839                .collect::<Vec<_>>()
840                .join("::");
841
842            let full_path = self
843                .imported_types
844                .get(&receiver_name)
845                .cloned()
846                .unwrap_or(receiver_name.clone());
847
848            if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
849                self.add_dependency(
850                    full_path.clone(),
851                    DependencyKind::TypeRef,
852                    UsageContext::MethodCall,
853                );
854                self.usage_counts.method_calls += 1;
855            }
856
857            // Record item-level dependency
858            let expr = format!("{}.{}()", receiver_name, method_name);
859            self.add_item_dependency(
860                format!("{}::{}", full_path, method_name),
861                ItemDepType::MethodCall,
862                0, // TODO: get line number from span
863                Some(expr),
864            );
865        }
866        syn::visit::visit_expr_method_call(self, node);
867    }
868
869    // Detect function calls: `Foo::new()` or `foo()`
870    fn visit_expr_call(&mut self, node: &'ast ExprCall) {
871        if let Expr::Path(path_expr) = &*node.func {
872            let path_str = path_expr
873                .path
874                .segments
875                .iter()
876                .map(|s| s.ident.to_string())
877                .collect::<Vec<_>>()
878                .join("::");
879
880            // Check if this is a constructor or associated function call
881            if path_str.contains("::") || path_str.chars().next().is_some_and(|c| c.is_uppercase())
882            {
883                let full_path = self
884                    .imported_types
885                    .get(&path_str)
886                    .cloned()
887                    .unwrap_or(path_str.clone());
888
889                if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
890                    self.add_dependency(
891                        full_path.clone(),
892                        DependencyKind::TypeRef,
893                        UsageContext::FunctionCall,
894                    );
895                    self.usage_counts.function_calls += 1;
896                }
897
898                // Record item-level dependency
899                self.add_item_dependency(
900                    full_path,
901                    ItemDepType::FunctionCall,
902                    0,
903                    Some(format!("{}()", path_str)),
904                );
905            } else {
906                // Simple function call like foo()
907                self.add_item_dependency(
908                    path_str.clone(),
909                    ItemDepType::FunctionCall,
910                    0,
911                    Some(format!("{}()", path_str)),
912                );
913            }
914        }
915        syn::visit::visit_expr_call(self, node);
916    }
917
918    // Detect struct construction: `Foo { field: value }`
919    fn visit_expr_struct(&mut self, node: &'ast ExprStruct) {
920        let struct_name = node
921            .path
922            .segments
923            .iter()
924            .map(|s| s.ident.to_string())
925            .collect::<Vec<_>>()
926            .join("::");
927
928        // Skip Self and self constructions
929        if struct_name == "Self" || struct_name.starts_with("Self::") {
930            syn::visit::visit_expr_struct(self, node);
931            return;
932        }
933
934        let full_path = self
935            .imported_types
936            .get(&struct_name)
937            .cloned()
938            .unwrap_or(struct_name.clone());
939
940        if !self.defined_types.contains(&full_path) && !self.is_primitive_type(&struct_name) {
941            self.add_dependency(
942                full_path,
943                DependencyKind::TypeRef,
944                UsageContext::StructConstruction,
945            );
946            self.usage_counts.struct_constructions += 1;
947        }
948        syn::visit::visit_expr_struct(self, node);
949    }
950}
951
952/// Analyzed file data
953#[derive(Debug, Clone)]
954struct AnalyzedFile {
955    module_name: String,
956    #[allow(dead_code)]
957    file_path: PathBuf,
958    metrics: ModuleMetrics,
959    dependencies: Vec<Dependency>,
960    /// Type visibility information from this file
961    type_visibility: HashMap<String, Visibility>,
962    /// Item-level dependencies (function calls, field access, etc.)
963    item_dependencies: Vec<ItemDependency>,
964}
965
966/// Analyze an entire project (parallel version)
967pub fn analyze_project(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
968    analyze_project_parallel(path)
969}
970
971/// Analyze a project using parallel processing with Rayon
972///
973/// Automatically scales to available CPU cores. The parallel processing
974/// uses work-stealing for optimal load balancing across cores.
975pub fn analyze_project_parallel(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
976    if !path.exists() {
977        return Err(AnalyzerError::InvalidPath(path.display().to_string()));
978    }
979
980    // Collect all .rs file paths first (sequential, but fast)
981    let file_paths: Vec<PathBuf> = WalkDir::new(path)
982        .follow_links(true)
983        .into_iter()
984        .filter_map(|e| e.ok())
985        .filter(|entry| {
986            let file_path = entry.path();
987            // Skip target directory and hidden directories
988            !file_path.components().any(|c| {
989                let s = c.as_os_str().to_string_lossy();
990                s == "target" || s.starts_with('.')
991            }) && file_path.extension() == Some(OsStr::new("rs"))
992        })
993        .map(|e| e.path().to_path_buf())
994        .collect();
995
996    // Calculate optimal chunk size based on file count and available parallelism
997    // Smaller chunks = better load balancing, but more overhead
998    // Larger chunks = less overhead, but potential load imbalance
999    let num_threads = rayon::current_num_threads();
1000    let file_count = file_paths.len();
1001
1002    // Use smaller chunks for better load balancing with work-stealing
1003    // Minimum chunk size of 1, maximum of file_count / (num_threads * 4)
1004    let chunk_size = if file_count < num_threads * 2 {
1005        1 // Small projects: process one file at a time
1006    } else {
1007        // Larger projects: balance between parallelism and overhead
1008        // Use ~4 chunks per thread for good work-stealing behavior
1009        (file_count / (num_threads * 4)).max(1)
1010    };
1011
1012    // Parallel file analysis with optimized chunking
1013    let analyzed_results: Vec<_> = file_paths
1014        .par_chunks(chunk_size)
1015        .flat_map(|chunk| {
1016            chunk
1017                .iter()
1018                .filter_map(|file_path| match analyze_rust_file_full(file_path) {
1019                    Ok(result) => Some(AnalyzedFile {
1020                        module_name: result.metrics.name.clone(),
1021                        file_path: file_path.clone(),
1022                        metrics: result.metrics,
1023                        dependencies: result.dependencies,
1024                        type_visibility: result.type_visibility,
1025                        item_dependencies: result.item_dependencies,
1026                    }),
1027                    Err(e) => {
1028                        eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1029                        None
1030                    }
1031                })
1032                .collect::<Vec<_>>()
1033        })
1034        .collect();
1035
1036    // Build module names set
1037    let module_names: HashSet<String> = analyzed_results
1038        .iter()
1039        .map(|a| a.module_name.clone())
1040        .collect();
1041
1042    // Build project metrics (sequential, but fast)
1043    let mut project = ProjectMetrics::new();
1044    project.total_files = analyzed_results.len();
1045
1046    // First pass: register all types with their visibility
1047    for analyzed in &analyzed_results {
1048        for (type_name, visibility) in &analyzed.type_visibility {
1049            project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
1050        }
1051    }
1052
1053    // Second pass: add modules and couplings
1054    for analyzed in &analyzed_results {
1055        // Clone metrics and add item_dependencies
1056        let mut metrics = analyzed.metrics.clone();
1057        metrics.item_dependencies = analyzed.item_dependencies.clone();
1058        project.add_module(metrics);
1059
1060        for dep in &analyzed.dependencies {
1061            // Skip invalid dependency paths (local variables, Self, etc.)
1062            if !is_valid_dependency_path(&dep.path) {
1063                continue;
1064            }
1065
1066            // Determine if this is an internal coupling
1067            let target_module = extract_target_module(&dep.path);
1068
1069            // Skip if target module looks invalid (but allow known module names)
1070            if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1071                continue;
1072            }
1073
1074            // Calculate distance
1075            let distance = calculate_distance(&dep.path, &module_names);
1076
1077            // Determine strength from usage context
1078            let strength = dep.usage.to_strength();
1079
1080            // Default volatility
1081            let volatility = Volatility::Low;
1082
1083            // Look up target visibility from the type registry
1084            let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
1085            let visibility = project
1086                .get_type_visibility(target_type)
1087                .unwrap_or(Visibility::Public); // Default to public if unknown
1088
1089            // Create coupling metric with location
1090            let coupling = CouplingMetrics::with_location(
1091                analyzed.module_name.clone(),
1092                target_module.clone(),
1093                strength,
1094                distance,
1095                volatility,
1096                visibility,
1097                analyzed.file_path.clone(),
1098                dep.line,
1099            );
1100
1101            project.add_coupling(coupling);
1102        }
1103    }
1104
1105    // Update any remaining coupling visibility information
1106    project.update_coupling_visibility();
1107
1108    Ok(project)
1109}
1110
1111/// Analyze a workspace using cargo metadata for better accuracy
1112pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1113    // Try to get workspace info
1114    let workspace = match WorkspaceInfo::from_path(path) {
1115        Ok(ws) => Some(ws),
1116        Err(e) => {
1117            eprintln!("Note: Could not load workspace metadata: {}", e);
1118            eprintln!("Falling back to basic analysis...");
1119            None
1120        }
1121    };
1122
1123    if let Some(ws) = workspace {
1124        analyze_with_workspace(path, &ws)
1125    } else {
1126        // Fall back to basic analysis
1127        analyze_project(path)
1128    }
1129}
1130
1131/// Analyze project with workspace information (parallel version)
1132fn analyze_with_workspace(
1133    _path: &Path,
1134    workspace: &WorkspaceInfo,
1135) -> Result<ProjectMetrics, AnalyzerError> {
1136    let mut project = ProjectMetrics::new();
1137
1138    // Store workspace info for the report
1139    project.workspace_name = Some(
1140        workspace
1141            .root
1142            .file_name()
1143            .and_then(|n| n.to_str())
1144            .unwrap_or("workspace")
1145            .to_string(),
1146    );
1147    project.workspace_members = workspace.members.clone();
1148
1149    // Collect all file paths with their crate names (sequential, fast)
1150    let mut file_crate_pairs: Vec<(PathBuf, String)> = Vec::new();
1151
1152    for member_name in &workspace.members {
1153        if let Some(crate_info) = workspace.get_crate(member_name) {
1154            if !crate_info.src_path.exists() {
1155                continue;
1156            }
1157
1158            for entry in WalkDir::new(&crate_info.src_path)
1159                .follow_links(true)
1160                .into_iter()
1161                .filter_map(|e| e.ok())
1162            {
1163                let file_path = entry.path();
1164
1165                // Skip target directory and hidden directories
1166                if file_path.components().any(|c| {
1167                    let s = c.as_os_str().to_string_lossy();
1168                    s == "target" || s.starts_with('.')
1169                }) {
1170                    continue;
1171                }
1172
1173                // Only process .rs files
1174                if file_path.extension() == Some(OsStr::new("rs")) {
1175                    file_crate_pairs.push((file_path.to_path_buf(), member_name.clone()));
1176                }
1177            }
1178        }
1179    }
1180
1181    // Calculate optimal chunk size for parallel processing
1182    let num_threads = rayon::current_num_threads();
1183    let file_count = file_crate_pairs.len();
1184    let chunk_size = if file_count < num_threads * 2 {
1185        1
1186    } else {
1187        (file_count / (num_threads * 4)).max(1)
1188    };
1189
1190    // Parallel file analysis with optimized chunking
1191    let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1192        .par_chunks(chunk_size)
1193        .flat_map(|chunk| {
1194            chunk
1195                .iter()
1196                .filter_map(
1197                    |(file_path, crate_name)| match analyze_rust_file_full(file_path) {
1198                        Ok(result) => Some(AnalyzedFileWithCrate {
1199                            module_name: result.metrics.name.clone(),
1200                            crate_name: crate_name.clone(),
1201                            file_path: file_path.clone(),
1202                            metrics: result.metrics,
1203                            dependencies: result.dependencies,
1204                            item_dependencies: result.item_dependencies,
1205                        }),
1206                        Err(e) => {
1207                            eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1208                            None
1209                        }
1210                    },
1211                )
1212                .collect::<Vec<_>>()
1213        })
1214        .collect();
1215
1216    project.total_files = analyzed_files.len();
1217
1218    // Build set of known module names for validation
1219    let module_names: HashSet<String> = analyzed_files
1220        .iter()
1221        .map(|a| a.module_name.clone())
1222        .collect();
1223
1224    // Second pass: build coupling relationships with workspace context
1225    for analyzed in &analyzed_files {
1226        // Clone metrics and add item_dependencies
1227        let mut metrics = analyzed.metrics.clone();
1228        metrics.item_dependencies = analyzed.item_dependencies.clone();
1229        project.add_module(metrics);
1230
1231        for dep in &analyzed.dependencies {
1232            // Skip invalid dependency paths (local variables, Self, etc.)
1233            if !is_valid_dependency_path(&dep.path) {
1234                continue;
1235            }
1236
1237            // Resolve the target crate using workspace info
1238            let resolved_crate =
1239                resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1240
1241            let target_module = extract_target_module(&dep.path);
1242
1243            // Skip if target module looks invalid (but allow known module names)
1244            if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1245                continue;
1246            }
1247
1248            // Calculate distance with workspace awareness
1249            let distance =
1250                calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1251
1252            // Determine strength from usage context (more accurate)
1253            let strength = dep.usage.to_strength();
1254
1255            // Default volatility
1256            let volatility = Volatility::Low;
1257
1258            // Create coupling metric with location info
1259            let mut coupling = CouplingMetrics::with_location(
1260                format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1261                if let Some(ref crate_name) = resolved_crate {
1262                    format!("{}::{}", crate_name, target_module)
1263                } else {
1264                    target_module.clone()
1265                },
1266                strength,
1267                distance,
1268                volatility,
1269                Visibility::Public, // Default visibility for workspace analysis
1270                analyzed.file_path.clone(),
1271                dep.line,
1272            );
1273
1274            // Add crate-level info
1275            coupling.source_crate = Some(analyzed.crate_name.clone());
1276            coupling.target_crate = resolved_crate;
1277
1278            project.add_coupling(coupling);
1279        }
1280    }
1281
1282    // Add crate-level dependency information
1283    for (crate_name, deps) in &workspace.dependency_graph {
1284        if workspace.is_workspace_member(crate_name) {
1285            for dep in deps {
1286                // Track crate-level dependencies
1287                project
1288                    .crate_dependencies
1289                    .entry(crate_name.clone())
1290                    .or_default()
1291                    .push(dep.clone());
1292            }
1293        }
1294    }
1295
1296    Ok(project)
1297}
1298
1299/// Calculate distance using workspace information
1300fn calculate_distance_with_workspace(
1301    dep_path: &str,
1302    current_crate: &str,
1303    workspace: &WorkspaceInfo,
1304) -> Distance {
1305    if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1306        // Same crate
1307        Distance::SameModule
1308    } else if dep_path.starts_with("super::") {
1309        // Could be same crate or parent module
1310        Distance::DifferentModule
1311    } else {
1312        // Resolve the target crate
1313        if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1314            if target_crate == current_crate {
1315                Distance::SameModule
1316            } else if workspace.is_workspace_member(&target_crate) {
1317                // Another workspace member
1318                Distance::DifferentModule
1319            } else {
1320                // External crate
1321                Distance::DifferentCrate
1322            }
1323        } else {
1324            Distance::DifferentCrate
1325        }
1326    }
1327}
1328
1329/// Analyzed file with crate information
1330#[derive(Debug, Clone)]
1331struct AnalyzedFileWithCrate {
1332    module_name: String,
1333    crate_name: String,
1334    #[allow(dead_code)]
1335    file_path: PathBuf,
1336    metrics: ModuleMetrics,
1337    dependencies: Vec<Dependency>,
1338    /// Item-level dependencies (function calls, field access, etc.)
1339    item_dependencies: Vec<ItemDependency>,
1340}
1341
1342/// Extract target module name from a path
1343fn extract_target_module(path: &str) -> String {
1344    // Remove common prefixes and get the module name
1345    let cleaned = path
1346        .trim_start_matches("crate::")
1347        .trim_start_matches("super::")
1348        .trim_start_matches("::");
1349
1350    // Get first significant segment
1351    cleaned.split("::").next().unwrap_or(path).to_string()
1352}
1353
1354/// Check if a path looks like a valid module/type reference (not a local variable)
1355fn is_valid_dependency_path(path: &str) -> bool {
1356    // Skip empty paths
1357    if path.is_empty() {
1358        return false;
1359    }
1360
1361    // Skip Self references
1362    if path == "Self" || path.starts_with("Self::") {
1363        return false;
1364    }
1365
1366    let segments: Vec<&str> = path.split("::").collect();
1367
1368    // Skip short single-segment lowercase names (likely local variables)
1369    if segments.len() == 1 {
1370        let name = segments[0];
1371        if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1372            return false;
1373        }
1374    }
1375
1376    // Skip patterns where last two segments are the same (likely module::type patterns from variables)
1377    if segments.len() >= 2 {
1378        let last = segments.last().unwrap();
1379        let second_last = segments.get(segments.len() - 2).unwrap();
1380        if last == second_last {
1381            return false;
1382        }
1383    }
1384
1385    // Skip common patterns that look like local variable accesses
1386    let last_segment = segments.last().unwrap_or(&path);
1387    let common_locals = [
1388        "request",
1389        "response",
1390        "result",
1391        "content",
1392        "config",
1393        "proto",
1394        "domain",
1395        "info",
1396        "data",
1397        "item",
1398        "value",
1399        "error",
1400        "message",
1401        "expected",
1402        "actual",
1403        "status",
1404        "state",
1405        "context",
1406        "params",
1407        "args",
1408        "options",
1409        "settings",
1410        "violation",
1411        "page_token",
1412    ];
1413    if common_locals.contains(last_segment) && segments.len() <= 2 {
1414        return false;
1415    }
1416
1417    true
1418}
1419
1420/// Calculate distance based on dependency path
1421fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1422    if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1423        // Internal dependency
1424        Distance::DifferentModule
1425    } else if dep_path.starts_with("self::") {
1426        Distance::SameModule
1427    } else {
1428        // External crate
1429        Distance::DifferentCrate
1430    }
1431}
1432
1433/// Analyze a single Rust file
1434/// Result of analyzing a single Rust file
1435pub struct AnalyzedFileResult {
1436    pub metrics: ModuleMetrics,
1437    pub dependencies: Vec<Dependency>,
1438    pub type_visibility: HashMap<String, Visibility>,
1439    pub item_dependencies: Vec<ItemDependency>,
1440}
1441
1442pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1443    let result = analyze_rust_file_full(path)?;
1444    Ok((result.metrics, result.dependencies))
1445}
1446
1447/// Analyze a Rust file and return full results including visibility
1448pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1449    let content = fs::read_to_string(path)?;
1450
1451    let module_name = path
1452        .file_stem()
1453        .and_then(|s| s.to_str())
1454        .unwrap_or("unknown")
1455        .to_string();
1456
1457    let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1458    analyzer.analyze_file(&content)?;
1459
1460    Ok(AnalyzedFileResult {
1461        metrics: analyzer.metrics,
1462        dependencies: analyzer.dependencies,
1463        type_visibility: analyzer.type_visibility,
1464        item_dependencies: analyzer.item_dependencies,
1465    })
1466}
1467
1468#[cfg(test)]
1469mod tests {
1470    use super::*;
1471
1472    #[test]
1473    fn test_analyzer_creation() {
1474        let analyzer = CouplingAnalyzer::new(
1475            "test_module".to_string(),
1476            std::path::PathBuf::from("test.rs"),
1477        );
1478        assert_eq!(analyzer.current_module, "test_module");
1479    }
1480
1481    #[test]
1482    fn test_analyze_simple_file() {
1483        let mut analyzer =
1484            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1485
1486        let code = r#"
1487            pub struct User {
1488                name: String,
1489                email: String,
1490            }
1491
1492            impl User {
1493                pub fn new(name: String, email: String) -> Self {
1494                    Self { name, email }
1495                }
1496            }
1497        "#;
1498
1499        let result = analyzer.analyze_file(code);
1500        assert!(result.is_ok());
1501        assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1502    }
1503
1504    #[test]
1505    fn test_item_dependencies() {
1506        let mut analyzer =
1507            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1508
1509        let code = r#"
1510            pub struct Config {
1511                pub value: i32,
1512            }
1513
1514            pub fn process(config: Config) -> i32 {
1515                let x = config.value;
1516                helper(x)
1517            }
1518
1519            fn helper(n: i32) -> i32 {
1520                n * 2
1521            }
1522        "#;
1523
1524        let result = analyzer.analyze_file(code);
1525        assert!(result.is_ok());
1526
1527        // Check that functions are recorded
1528        assert!(analyzer.defined_functions.contains_key("process"));
1529        assert!(analyzer.defined_functions.contains_key("helper"));
1530
1531        // Check item dependencies - process should have deps
1532        println!(
1533            "Item dependencies count: {}",
1534            analyzer.item_dependencies.len()
1535        );
1536        for dep in &analyzer.item_dependencies {
1537            println!(
1538                "  {} -> {} ({:?})",
1539                dep.source_item, dep.target, dep.dep_type
1540            );
1541        }
1542
1543        // process function should have dependencies
1544        let process_deps: Vec<_> = analyzer
1545            .item_dependencies
1546            .iter()
1547            .filter(|d| d.source_item == "process")
1548            .collect();
1549
1550        assert!(
1551            !process_deps.is_empty(),
1552            "process function should have item dependencies"
1553        );
1554    }
1555
1556    #[test]
1557    fn test_analyze_trait_impl() {
1558        let mut analyzer =
1559            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1560
1561        let code = r#"
1562            trait Printable {
1563                fn print(&self);
1564            }
1565
1566            struct Document;
1567
1568            impl Printable for Document {
1569                fn print(&self) {}
1570            }
1571        "#;
1572
1573        let result = analyzer.analyze_file(code);
1574        assert!(result.is_ok());
1575        assert!(analyzer.metrics.trait_impl_count >= 1);
1576    }
1577
1578    #[test]
1579    fn test_analyze_use_statements() {
1580        let mut analyzer =
1581            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1582
1583        let code = r#"
1584            use std::collections::HashMap;
1585            use serde::Serialize;
1586            use crate::utils;
1587            use crate::models::{User, Post};
1588        "#;
1589
1590        let result = analyzer.analyze_file(code);
1591        assert!(result.is_ok());
1592        assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1593        assert!(
1594            analyzer
1595                .metrics
1596                .external_deps
1597                .contains(&"serde".to_string())
1598        );
1599        assert!(!analyzer.dependencies.is_empty());
1600
1601        // Check internal dependencies
1602        let internal_deps: Vec<_> = analyzer
1603            .dependencies
1604            .iter()
1605            .filter(|d| d.kind == DependencyKind::InternalUse)
1606            .collect();
1607        assert!(!internal_deps.is_empty());
1608    }
1609
1610    #[test]
1611    fn test_extract_use_paths() {
1612        let analyzer =
1613            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1614
1615        // Test simple path
1616        let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1617        let paths = analyzer.extract_use_paths(&tree, "");
1618        assert_eq!(paths.len(), 1);
1619        assert_eq!(paths[0].0, "std::collections::HashMap");
1620
1621        // Test grouped path
1622        let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1623        let paths = analyzer.extract_use_paths(&tree, "");
1624        assert_eq!(paths.len(), 2);
1625    }
1626
1627    #[test]
1628    fn test_extract_target_module() {
1629        assert_eq!(extract_target_module("crate::models::user"), "models");
1630        assert_eq!(extract_target_module("super::utils"), "utils");
1631        assert_eq!(extract_target_module("std::collections"), "std");
1632    }
1633
1634    #[test]
1635    fn test_field_access_detection() {
1636        let mut analyzer =
1637            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1638
1639        let code = r#"
1640            use crate::models::User;
1641
1642            fn get_name(user: &User) -> String {
1643                user.name.clone()
1644            }
1645        "#;
1646
1647        let result = analyzer.analyze_file(code);
1648        assert!(result.is_ok());
1649
1650        // Should detect User as a dependency with field access
1651        let _field_deps: Vec<_> = analyzer
1652            .dependencies
1653            .iter()
1654            .filter(|d| d.usage == UsageContext::FieldAccess)
1655            .collect();
1656        // Note: This may not detect field access on function parameters
1657        // as the type info isn't fully available without type inference
1658    }
1659
1660    #[test]
1661    fn test_method_call_detection() {
1662        let mut analyzer =
1663            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1664
1665        let code = r#"
1666            fn process() {
1667                let data = String::new();
1668                data.push_str("hello");
1669            }
1670        "#;
1671
1672        let result = analyzer.analyze_file(code);
1673        assert!(result.is_ok());
1674        // Method calls on local variables are detected
1675    }
1676
1677    #[test]
1678    fn test_struct_construction_detection() {
1679        let mut analyzer =
1680            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1681
1682        let code = r#"
1683            use crate::config::Config;
1684
1685            fn create_config() {
1686                let c = Config { value: 42 };
1687            }
1688        "#;
1689
1690        let result = analyzer.analyze_file(code);
1691        assert!(result.is_ok());
1692
1693        // Should detect Config struct construction
1694        let struct_deps: Vec<_> = analyzer
1695            .dependencies
1696            .iter()
1697            .filter(|d| d.usage == UsageContext::StructConstruction)
1698            .collect();
1699        assert!(!struct_deps.is_empty());
1700    }
1701
1702    #[test]
1703    fn test_usage_context_to_strength() {
1704        assert_eq!(
1705            UsageContext::FieldAccess.to_strength(),
1706            IntegrationStrength::Intrusive
1707        );
1708        assert_eq!(
1709            UsageContext::MethodCall.to_strength(),
1710            IntegrationStrength::Functional
1711        );
1712        assert_eq!(
1713            UsageContext::TypeParameter.to_strength(),
1714            IntegrationStrength::Model
1715        );
1716        assert_eq!(
1717            UsageContext::TraitBound.to_strength(),
1718            IntegrationStrength::Contract
1719        );
1720    }
1721}