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
1070            if !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    // Second pass: build coupling relationships with workspace context
1219    for analyzed in &analyzed_files {
1220        // Clone metrics and add item_dependencies
1221        let mut metrics = analyzed.metrics.clone();
1222        metrics.item_dependencies = analyzed.item_dependencies.clone();
1223        project.add_module(metrics);
1224
1225        for dep in &analyzed.dependencies {
1226            // Skip invalid dependency paths (local variables, Self, etc.)
1227            if !is_valid_dependency_path(&dep.path) {
1228                continue;
1229            }
1230
1231            // Resolve the target crate using workspace info
1232            let resolved_crate =
1233                resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1234
1235            let target_module = extract_target_module(&dep.path);
1236
1237            // Skip if target module looks invalid
1238            if !is_valid_dependency_path(&target_module) {
1239                continue;
1240            }
1241
1242            // Calculate distance with workspace awareness
1243            let distance =
1244                calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1245
1246            // Determine strength from usage context (more accurate)
1247            let strength = dep.usage.to_strength();
1248
1249            // Default volatility
1250            let volatility = Volatility::Low;
1251
1252            // Create coupling metric with location info
1253            let mut coupling = CouplingMetrics::with_location(
1254                format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1255                if let Some(ref crate_name) = resolved_crate {
1256                    format!("{}::{}", crate_name, target_module)
1257                } else {
1258                    target_module.clone()
1259                },
1260                strength,
1261                distance,
1262                volatility,
1263                Visibility::Public, // Default visibility for workspace analysis
1264                analyzed.file_path.clone(),
1265                dep.line,
1266            );
1267
1268            // Add crate-level info
1269            coupling.source_crate = Some(analyzed.crate_name.clone());
1270            coupling.target_crate = resolved_crate;
1271
1272            project.add_coupling(coupling);
1273        }
1274    }
1275
1276    // Add crate-level dependency information
1277    for (crate_name, deps) in &workspace.dependency_graph {
1278        if workspace.is_workspace_member(crate_name) {
1279            for dep in deps {
1280                // Track crate-level dependencies
1281                project
1282                    .crate_dependencies
1283                    .entry(crate_name.clone())
1284                    .or_default()
1285                    .push(dep.clone());
1286            }
1287        }
1288    }
1289
1290    Ok(project)
1291}
1292
1293/// Calculate distance using workspace information
1294fn calculate_distance_with_workspace(
1295    dep_path: &str,
1296    current_crate: &str,
1297    workspace: &WorkspaceInfo,
1298) -> Distance {
1299    if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1300        // Same crate
1301        Distance::SameModule
1302    } else if dep_path.starts_with("super::") {
1303        // Could be same crate or parent module
1304        Distance::DifferentModule
1305    } else {
1306        // Resolve the target crate
1307        if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1308            if target_crate == current_crate {
1309                Distance::SameModule
1310            } else if workspace.is_workspace_member(&target_crate) {
1311                // Another workspace member
1312                Distance::DifferentModule
1313            } else {
1314                // External crate
1315                Distance::DifferentCrate
1316            }
1317        } else {
1318            Distance::DifferentCrate
1319        }
1320    }
1321}
1322
1323/// Analyzed file with crate information
1324#[derive(Debug, Clone)]
1325struct AnalyzedFileWithCrate {
1326    module_name: String,
1327    crate_name: String,
1328    #[allow(dead_code)]
1329    file_path: PathBuf,
1330    metrics: ModuleMetrics,
1331    dependencies: Vec<Dependency>,
1332    /// Item-level dependencies (function calls, field access, etc.)
1333    item_dependencies: Vec<ItemDependency>,
1334}
1335
1336/// Extract target module name from a path
1337fn extract_target_module(path: &str) -> String {
1338    // Remove common prefixes and get the module name
1339    let cleaned = path
1340        .trim_start_matches("crate::")
1341        .trim_start_matches("super::")
1342        .trim_start_matches("::");
1343
1344    // Get first significant segment
1345    cleaned.split("::").next().unwrap_or(path).to_string()
1346}
1347
1348/// Check if a path looks like a valid module/type reference (not a local variable)
1349fn is_valid_dependency_path(path: &str) -> bool {
1350    // Skip empty paths
1351    if path.is_empty() {
1352        return false;
1353    }
1354
1355    // Skip Self references
1356    if path == "Self" || path.starts_with("Self::") {
1357        return false;
1358    }
1359
1360    let segments: Vec<&str> = path.split("::").collect();
1361
1362    // Skip short single-segment lowercase names (likely local variables)
1363    if segments.len() == 1 {
1364        let name = segments[0];
1365        if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1366            return false;
1367        }
1368    }
1369
1370    // Skip patterns where last two segments are the same (likely module::type patterns from variables)
1371    if segments.len() >= 2 {
1372        let last = segments.last().unwrap();
1373        let second_last = segments.get(segments.len() - 2).unwrap();
1374        if last == second_last {
1375            return false;
1376        }
1377    }
1378
1379    // Skip common patterns that look like local variable accesses
1380    let last_segment = segments.last().unwrap_or(&path);
1381    let common_locals = [
1382        "request",
1383        "response",
1384        "result",
1385        "content",
1386        "config",
1387        "proto",
1388        "domain",
1389        "info",
1390        "data",
1391        "item",
1392        "value",
1393        "error",
1394        "message",
1395        "expected",
1396        "actual",
1397        "status",
1398        "state",
1399        "context",
1400        "params",
1401        "args",
1402        "options",
1403        "settings",
1404        "violation",
1405        "page_token",
1406    ];
1407    if common_locals.contains(last_segment) && segments.len() <= 2 {
1408        return false;
1409    }
1410
1411    true
1412}
1413
1414/// Calculate distance based on dependency path
1415fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1416    if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1417        // Internal dependency
1418        Distance::DifferentModule
1419    } else if dep_path.starts_with("self::") {
1420        Distance::SameModule
1421    } else {
1422        // External crate
1423        Distance::DifferentCrate
1424    }
1425}
1426
1427/// Analyze a single Rust file
1428/// Result of analyzing a single Rust file
1429pub struct AnalyzedFileResult {
1430    pub metrics: ModuleMetrics,
1431    pub dependencies: Vec<Dependency>,
1432    pub type_visibility: HashMap<String, Visibility>,
1433    pub item_dependencies: Vec<ItemDependency>,
1434}
1435
1436pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1437    let result = analyze_rust_file_full(path)?;
1438    Ok((result.metrics, result.dependencies))
1439}
1440
1441/// Analyze a Rust file and return full results including visibility
1442pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1443    let content = fs::read_to_string(path)?;
1444
1445    let module_name = path
1446        .file_stem()
1447        .and_then(|s| s.to_str())
1448        .unwrap_or("unknown")
1449        .to_string();
1450
1451    let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1452    analyzer.analyze_file(&content)?;
1453
1454    Ok(AnalyzedFileResult {
1455        metrics: analyzer.metrics,
1456        dependencies: analyzer.dependencies,
1457        type_visibility: analyzer.type_visibility,
1458        item_dependencies: analyzer.item_dependencies,
1459    })
1460}
1461
1462#[cfg(test)]
1463mod tests {
1464    use super::*;
1465
1466    #[test]
1467    fn test_analyzer_creation() {
1468        let analyzer = CouplingAnalyzer::new(
1469            "test_module".to_string(),
1470            std::path::PathBuf::from("test.rs"),
1471        );
1472        assert_eq!(analyzer.current_module, "test_module");
1473    }
1474
1475    #[test]
1476    fn test_analyze_simple_file() {
1477        let mut analyzer =
1478            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1479
1480        let code = r#"
1481            pub struct User {
1482                name: String,
1483                email: String,
1484            }
1485
1486            impl User {
1487                pub fn new(name: String, email: String) -> Self {
1488                    Self { name, email }
1489                }
1490            }
1491        "#;
1492
1493        let result = analyzer.analyze_file(code);
1494        assert!(result.is_ok());
1495        assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1496    }
1497
1498    #[test]
1499    fn test_item_dependencies() {
1500        let mut analyzer =
1501            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1502
1503        let code = r#"
1504            pub struct Config {
1505                pub value: i32,
1506            }
1507
1508            pub fn process(config: Config) -> i32 {
1509                let x = config.value;
1510                helper(x)
1511            }
1512
1513            fn helper(n: i32) -> i32 {
1514                n * 2
1515            }
1516        "#;
1517
1518        let result = analyzer.analyze_file(code);
1519        assert!(result.is_ok());
1520
1521        // Check that functions are recorded
1522        assert!(analyzer.defined_functions.contains_key("process"));
1523        assert!(analyzer.defined_functions.contains_key("helper"));
1524
1525        // Check item dependencies - process should have deps
1526        println!(
1527            "Item dependencies count: {}",
1528            analyzer.item_dependencies.len()
1529        );
1530        for dep in &analyzer.item_dependencies {
1531            println!(
1532                "  {} -> {} ({:?})",
1533                dep.source_item, dep.target, dep.dep_type
1534            );
1535        }
1536
1537        // process function should have dependencies
1538        let process_deps: Vec<_> = analyzer
1539            .item_dependencies
1540            .iter()
1541            .filter(|d| d.source_item == "process")
1542            .collect();
1543
1544        assert!(
1545            !process_deps.is_empty(),
1546            "process function should have item dependencies"
1547        );
1548    }
1549
1550    #[test]
1551    fn test_analyze_trait_impl() {
1552        let mut analyzer =
1553            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1554
1555        let code = r#"
1556            trait Printable {
1557                fn print(&self);
1558            }
1559
1560            struct Document;
1561
1562            impl Printable for Document {
1563                fn print(&self) {}
1564            }
1565        "#;
1566
1567        let result = analyzer.analyze_file(code);
1568        assert!(result.is_ok());
1569        assert!(analyzer.metrics.trait_impl_count >= 1);
1570    }
1571
1572    #[test]
1573    fn test_analyze_use_statements() {
1574        let mut analyzer =
1575            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1576
1577        let code = r#"
1578            use std::collections::HashMap;
1579            use serde::Serialize;
1580            use crate::utils;
1581            use crate::models::{User, Post};
1582        "#;
1583
1584        let result = analyzer.analyze_file(code);
1585        assert!(result.is_ok());
1586        assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1587        assert!(
1588            analyzer
1589                .metrics
1590                .external_deps
1591                .contains(&"serde".to_string())
1592        );
1593        assert!(!analyzer.dependencies.is_empty());
1594
1595        // Check internal dependencies
1596        let internal_deps: Vec<_> = analyzer
1597            .dependencies
1598            .iter()
1599            .filter(|d| d.kind == DependencyKind::InternalUse)
1600            .collect();
1601        assert!(!internal_deps.is_empty());
1602    }
1603
1604    #[test]
1605    fn test_extract_use_paths() {
1606        let analyzer =
1607            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1608
1609        // Test simple path
1610        let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1611        let paths = analyzer.extract_use_paths(&tree, "");
1612        assert_eq!(paths.len(), 1);
1613        assert_eq!(paths[0].0, "std::collections::HashMap");
1614
1615        // Test grouped path
1616        let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1617        let paths = analyzer.extract_use_paths(&tree, "");
1618        assert_eq!(paths.len(), 2);
1619    }
1620
1621    #[test]
1622    fn test_extract_target_module() {
1623        assert_eq!(extract_target_module("crate::models::user"), "models");
1624        assert_eq!(extract_target_module("super::utils"), "utils");
1625        assert_eq!(extract_target_module("std::collections"), "std");
1626    }
1627
1628    #[test]
1629    fn test_field_access_detection() {
1630        let mut analyzer =
1631            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1632
1633        let code = r#"
1634            use crate::models::User;
1635
1636            fn get_name(user: &User) -> String {
1637                user.name.clone()
1638            }
1639        "#;
1640
1641        let result = analyzer.analyze_file(code);
1642        assert!(result.is_ok());
1643
1644        // Should detect User as a dependency with field access
1645        let _field_deps: Vec<_> = analyzer
1646            .dependencies
1647            .iter()
1648            .filter(|d| d.usage == UsageContext::FieldAccess)
1649            .collect();
1650        // Note: This may not detect field access on function parameters
1651        // as the type info isn't fully available without type inference
1652    }
1653
1654    #[test]
1655    fn test_method_call_detection() {
1656        let mut analyzer =
1657            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1658
1659        let code = r#"
1660            fn process() {
1661                let data = String::new();
1662                data.push_str("hello");
1663            }
1664        "#;
1665
1666        let result = analyzer.analyze_file(code);
1667        assert!(result.is_ok());
1668        // Method calls on local variables are detected
1669    }
1670
1671    #[test]
1672    fn test_struct_construction_detection() {
1673        let mut analyzer =
1674            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1675
1676        let code = r#"
1677            use crate::config::Config;
1678
1679            fn create_config() {
1680                let c = Config { value: 42 };
1681            }
1682        "#;
1683
1684        let result = analyzer.analyze_file(code);
1685        assert!(result.is_ok());
1686
1687        // Should detect Config struct construction
1688        let struct_deps: Vec<_> = analyzer
1689            .dependencies
1690            .iter()
1691            .filter(|d| d.usage == UsageContext::StructConstruction)
1692            .collect();
1693        assert!(!struct_deps.is_empty());
1694    }
1695
1696    #[test]
1697    fn test_usage_context_to_strength() {
1698        assert_eq!(
1699            UsageContext::FieldAccess.to_strength(),
1700            IntegrationStrength::Intrusive
1701        );
1702        assert_eq!(
1703            UsageContext::MethodCall.to_strength(),
1704            IntegrationStrength::Functional
1705        );
1706        assert_eq!(
1707            UsageContext::TypeParameter.to_strength(),
1708            IntegrationStrength::Model
1709        );
1710        assert_eq!(
1711            UsageContext::TraitBound.to_strength(),
1712            IntegrationStrength::Contract
1713        );
1714    }
1715}