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/// Get an iterator over all Rust source files in `dir`, excluding hidden directories and `target/`.
972///
973/// Uses relative paths for filtering to avoid false positives when the project
974/// is located in a path containing hidden directories (e.g., `/home/user/.local/projects/`).
975/// See: https://github.com/nwiizo/cargo-coupling/issues/7
976fn rs_files(dir: &Path) -> impl Iterator<Item = PathBuf> {
977    WalkDir::new(dir)
978        .follow_links(true)
979        .into_iter()
980        .filter_map(|e| e.ok())
981        .filter(move |entry| {
982            let file_path = entry.path();
983            // Use relative path from the search root to check for hidden/target directories.
984            // This prevents false positives when parent directories contain `.` or `target`.
985            // Example: `/home/user/.config/myproject/src/lib.rs` should not be skipped
986            // just because `.config` is in the parent path.
987            let file_path = file_path.strip_prefix(dir).unwrap_or(file_path);
988
989            // Skip target directory and hidden directories
990            !file_path.components().any(|c| {
991                let s = c.as_os_str().to_string_lossy();
992                s == "target" || s.starts_with('.')
993            }) && file_path.extension() == Some(OsStr::new("rs"))
994        })
995        .map(|e| e.path().to_path_buf())
996}
997
998/// Analyze a project using parallel processing with Rayon
999///
1000/// Automatically scales to available CPU cores. The parallel processing
1001/// uses work-stealing for optimal load balancing across cores.
1002pub fn analyze_project_parallel(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1003    if !path.exists() {
1004        return Err(AnalyzerError::InvalidPath(path.display().to_string()));
1005    }
1006
1007    // Collect all .rs file paths first (sequential, but fast)
1008    let file_paths: Vec<PathBuf> = rs_files(path).collect();
1009
1010    // Calculate optimal chunk size based on file count and available parallelism
1011    // Smaller chunks = better load balancing, but more overhead
1012    // Larger chunks = less overhead, but potential load imbalance
1013    let num_threads = rayon::current_num_threads();
1014    let file_count = file_paths.len();
1015
1016    // Use smaller chunks for better load balancing with work-stealing
1017    // Minimum chunk size of 1, maximum of file_count / (num_threads * 4)
1018    let chunk_size = if file_count < num_threads * 2 {
1019        1 // Small projects: process one file at a time
1020    } else {
1021        // Larger projects: balance between parallelism and overhead
1022        // Use ~4 chunks per thread for good work-stealing behavior
1023        (file_count / (num_threads * 4)).max(1)
1024    };
1025
1026    // Parallel file analysis with optimized chunking
1027    let analyzed_results: Vec<_> = file_paths
1028        .par_chunks(chunk_size)
1029        .flat_map(|chunk| {
1030            chunk
1031                .iter()
1032                .filter_map(|file_path| match analyze_rust_file_full(file_path) {
1033                    Ok(result) => Some(AnalyzedFile {
1034                        module_name: result.metrics.name.clone(),
1035                        file_path: file_path.clone(),
1036                        metrics: result.metrics,
1037                        dependencies: result.dependencies,
1038                        type_visibility: result.type_visibility,
1039                        item_dependencies: result.item_dependencies,
1040                    }),
1041                    Err(e) => {
1042                        eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1043                        None
1044                    }
1045                })
1046                .collect::<Vec<_>>()
1047        })
1048        .collect();
1049
1050    // Build module names set
1051    let module_names: HashSet<String> = analyzed_results
1052        .iter()
1053        .map(|a| a.module_name.clone())
1054        .collect();
1055
1056    // Build project metrics (sequential, but fast)
1057    let mut project = ProjectMetrics::new();
1058    project.total_files = analyzed_results.len();
1059
1060    // First pass: register all types with their visibility
1061    for analyzed in &analyzed_results {
1062        for (type_name, visibility) in &analyzed.type_visibility {
1063            project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
1064        }
1065    }
1066
1067    // Second pass: add modules and couplings
1068    for analyzed in &analyzed_results {
1069        // Clone metrics and add item_dependencies
1070        let mut metrics = analyzed.metrics.clone();
1071        metrics.item_dependencies = analyzed.item_dependencies.clone();
1072        project.add_module(metrics);
1073
1074        for dep in &analyzed.dependencies {
1075            // Skip invalid dependency paths (local variables, Self, etc.)
1076            if !is_valid_dependency_path(&dep.path) {
1077                continue;
1078            }
1079
1080            // Determine if this is an internal coupling
1081            let target_module = extract_target_module(&dep.path);
1082
1083            // Skip if target module looks invalid (but allow known module names)
1084            if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1085                continue;
1086            }
1087
1088            // Calculate distance
1089            let distance = calculate_distance(&dep.path, &module_names);
1090
1091            // Determine strength from usage context
1092            let strength = dep.usage.to_strength();
1093
1094            // Default volatility
1095            let volatility = Volatility::Low;
1096
1097            // Look up target visibility from the type registry
1098            let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
1099            let visibility = project
1100                .get_type_visibility(target_type)
1101                .unwrap_or(Visibility::Public); // Default to public if unknown
1102
1103            // Create coupling metric with location
1104            let coupling = CouplingMetrics::with_location(
1105                analyzed.module_name.clone(),
1106                target_module.clone(),
1107                strength,
1108                distance,
1109                volatility,
1110                visibility,
1111                analyzed.file_path.clone(),
1112                dep.line,
1113            );
1114
1115            project.add_coupling(coupling);
1116        }
1117    }
1118
1119    // Update any remaining coupling visibility information
1120    project.update_coupling_visibility();
1121
1122    Ok(project)
1123}
1124
1125/// Analyze a workspace using cargo metadata for better accuracy
1126pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1127    // Try to get workspace info
1128    let workspace = match WorkspaceInfo::from_path(path) {
1129        Ok(ws) => Some(ws),
1130        Err(e) => {
1131            eprintln!("Note: Could not load workspace metadata: {}", e);
1132            eprintln!("Falling back to basic analysis...");
1133            None
1134        }
1135    };
1136
1137    if let Some(ws) = workspace {
1138        analyze_with_workspace(path, &ws)
1139    } else {
1140        // Fall back to basic analysis
1141        analyze_project(path)
1142    }
1143}
1144
1145/// Analyze project with workspace information (parallel version)
1146fn analyze_with_workspace(
1147    _path: &Path,
1148    workspace: &WorkspaceInfo,
1149) -> Result<ProjectMetrics, AnalyzerError> {
1150    let mut project = ProjectMetrics::new();
1151
1152    // Store workspace info for the report
1153    project.workspace_name = Some(
1154        workspace
1155            .root
1156            .file_name()
1157            .and_then(|n| n.to_str())
1158            .unwrap_or("workspace")
1159            .to_string(),
1160    );
1161    project.workspace_members = workspace.members.clone();
1162
1163    // Collect all file paths with their crate names (sequential, fast)
1164    let mut file_crate_pairs: Vec<(PathBuf, String)> = Vec::new();
1165
1166    for member_name in &workspace.members {
1167        if let Some(crate_info) = workspace.get_crate(member_name) {
1168            if !crate_info.src_path.exists() {
1169                continue;
1170            }
1171
1172            for file_path in rs_files(&crate_info.src_path) {
1173                file_crate_pairs.push((file_path.to_path_buf(), member_name.clone()));
1174            }
1175        }
1176    }
1177
1178    // Calculate optimal chunk size for parallel processing
1179    let num_threads = rayon::current_num_threads();
1180    let file_count = file_crate_pairs.len();
1181    let chunk_size = if file_count < num_threads * 2 {
1182        1
1183    } else {
1184        (file_count / (num_threads * 4)).max(1)
1185    };
1186
1187    // Parallel file analysis with optimized chunking
1188    let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1189        .par_chunks(chunk_size)
1190        .flat_map(|chunk| {
1191            chunk
1192                .iter()
1193                .filter_map(
1194                    |(file_path, crate_name)| match analyze_rust_file_full(file_path) {
1195                        Ok(result) => Some(AnalyzedFileWithCrate {
1196                            module_name: result.metrics.name.clone(),
1197                            crate_name: crate_name.clone(),
1198                            file_path: file_path.clone(),
1199                            metrics: result.metrics,
1200                            dependencies: result.dependencies,
1201                            item_dependencies: result.item_dependencies,
1202                        }),
1203                        Err(e) => {
1204                            eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1205                            None
1206                        }
1207                    },
1208                )
1209                .collect::<Vec<_>>()
1210        })
1211        .collect();
1212
1213    project.total_files = analyzed_files.len();
1214
1215    // Build set of known module names for validation
1216    let module_names: HashSet<String> = analyzed_files
1217        .iter()
1218        .map(|a| a.module_name.clone())
1219        .collect();
1220
1221    // Second pass: build coupling relationships with workspace context
1222    for analyzed in &analyzed_files {
1223        // Clone metrics and add item_dependencies
1224        let mut metrics = analyzed.metrics.clone();
1225        metrics.item_dependencies = analyzed.item_dependencies.clone();
1226        project.add_module(metrics);
1227
1228        for dep in &analyzed.dependencies {
1229            // Skip invalid dependency paths (local variables, Self, etc.)
1230            if !is_valid_dependency_path(&dep.path) {
1231                continue;
1232            }
1233
1234            // Resolve the target crate using workspace info
1235            let resolved_crate =
1236                resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1237
1238            let target_module = extract_target_module(&dep.path);
1239
1240            // Skip if target module looks invalid (but allow known module names)
1241            if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1242                continue;
1243            }
1244
1245            // Calculate distance with workspace awareness
1246            let distance =
1247                calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1248
1249            // Determine strength from usage context (more accurate)
1250            let strength = dep.usage.to_strength();
1251
1252            // Default volatility
1253            let volatility = Volatility::Low;
1254
1255            // Create coupling metric with location info
1256            let mut coupling = CouplingMetrics::with_location(
1257                format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1258                if let Some(ref crate_name) = resolved_crate {
1259                    format!("{}::{}", crate_name, target_module)
1260                } else {
1261                    target_module.clone()
1262                },
1263                strength,
1264                distance,
1265                volatility,
1266                Visibility::Public, // Default visibility for workspace analysis
1267                analyzed.file_path.clone(),
1268                dep.line,
1269            );
1270
1271            // Add crate-level info
1272            coupling.source_crate = Some(analyzed.crate_name.clone());
1273            coupling.target_crate = resolved_crate;
1274
1275            project.add_coupling(coupling);
1276        }
1277    }
1278
1279    // Add crate-level dependency information
1280    for (crate_name, deps) in &workspace.dependency_graph {
1281        if workspace.is_workspace_member(crate_name) {
1282            for dep in deps {
1283                // Track crate-level dependencies
1284                project
1285                    .crate_dependencies
1286                    .entry(crate_name.clone())
1287                    .or_default()
1288                    .push(dep.clone());
1289            }
1290        }
1291    }
1292
1293    Ok(project)
1294}
1295
1296/// Calculate distance using workspace information
1297fn calculate_distance_with_workspace(
1298    dep_path: &str,
1299    current_crate: &str,
1300    workspace: &WorkspaceInfo,
1301) -> Distance {
1302    if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1303        // Same crate
1304        Distance::SameModule
1305    } else if dep_path.starts_with("super::") {
1306        // Could be same crate or parent module
1307        Distance::DifferentModule
1308    } else {
1309        // Resolve the target crate
1310        if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1311            if target_crate == current_crate {
1312                Distance::SameModule
1313            } else if workspace.is_workspace_member(&target_crate) {
1314                // Another workspace member
1315                Distance::DifferentModule
1316            } else {
1317                // External crate
1318                Distance::DifferentCrate
1319            }
1320        } else {
1321            Distance::DifferentCrate
1322        }
1323    }
1324}
1325
1326/// Analyzed file with crate information
1327#[derive(Debug, Clone)]
1328struct AnalyzedFileWithCrate {
1329    module_name: String,
1330    crate_name: String,
1331    #[allow(dead_code)]
1332    file_path: PathBuf,
1333    metrics: ModuleMetrics,
1334    dependencies: Vec<Dependency>,
1335    /// Item-level dependencies (function calls, field access, etc.)
1336    item_dependencies: Vec<ItemDependency>,
1337}
1338
1339/// Extract target module name from a path
1340fn extract_target_module(path: &str) -> String {
1341    // Remove common prefixes and get the module name
1342    let cleaned = path
1343        .trim_start_matches("crate::")
1344        .trim_start_matches("super::")
1345        .trim_start_matches("::");
1346
1347    // Get first significant segment
1348    cleaned.split("::").next().unwrap_or(path).to_string()
1349}
1350
1351/// Check if a path looks like a valid module/type reference (not a local variable)
1352fn is_valid_dependency_path(path: &str) -> bool {
1353    // Skip empty paths
1354    if path.is_empty() {
1355        return false;
1356    }
1357
1358    // Skip Self references
1359    if path == "Self" || path.starts_with("Self::") {
1360        return false;
1361    }
1362
1363    let segments: Vec<&str> = path.split("::").collect();
1364
1365    // Skip short single-segment lowercase names (likely local variables)
1366    if segments.len() == 1 {
1367        let name = segments[0];
1368        if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1369            return false;
1370        }
1371    }
1372
1373    // Skip patterns where last two segments are the same (likely module::type patterns from variables)
1374    if segments.len() >= 2 {
1375        let last = segments.last().unwrap();
1376        let second_last = segments.get(segments.len() - 2).unwrap();
1377        if last == second_last {
1378            return false;
1379        }
1380    }
1381
1382    // Skip common patterns that look like local variable accesses
1383    let last_segment = segments.last().unwrap_or(&path);
1384    let common_locals = [
1385        "request",
1386        "response",
1387        "result",
1388        "content",
1389        "config",
1390        "proto",
1391        "domain",
1392        "info",
1393        "data",
1394        "item",
1395        "value",
1396        "error",
1397        "message",
1398        "expected",
1399        "actual",
1400        "status",
1401        "state",
1402        "context",
1403        "params",
1404        "args",
1405        "options",
1406        "settings",
1407        "violation",
1408        "page_token",
1409    ];
1410    if common_locals.contains(last_segment) && segments.len() <= 2 {
1411        return false;
1412    }
1413
1414    true
1415}
1416
1417/// Calculate distance based on dependency path
1418fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1419    if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1420        // Internal dependency
1421        Distance::DifferentModule
1422    } else if dep_path.starts_with("self::") {
1423        Distance::SameModule
1424    } else {
1425        // External crate
1426        Distance::DifferentCrate
1427    }
1428}
1429
1430/// Analyze a single Rust file
1431/// Result of analyzing a single Rust file
1432pub struct AnalyzedFileResult {
1433    pub metrics: ModuleMetrics,
1434    pub dependencies: Vec<Dependency>,
1435    pub type_visibility: HashMap<String, Visibility>,
1436    pub item_dependencies: Vec<ItemDependency>,
1437}
1438
1439pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1440    let result = analyze_rust_file_full(path)?;
1441    Ok((result.metrics, result.dependencies))
1442}
1443
1444/// Analyze a Rust file and return full results including visibility
1445pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1446    let content = fs::read_to_string(path)?;
1447
1448    let module_name = path
1449        .file_stem()
1450        .and_then(|s| s.to_str())
1451        .unwrap_or("unknown")
1452        .to_string();
1453
1454    let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1455    analyzer.analyze_file(&content)?;
1456
1457    Ok(AnalyzedFileResult {
1458        metrics: analyzer.metrics,
1459        dependencies: analyzer.dependencies,
1460        type_visibility: analyzer.type_visibility,
1461        item_dependencies: analyzer.item_dependencies,
1462    })
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467    use super::*;
1468
1469    #[test]
1470    fn test_analyzer_creation() {
1471        let analyzer = CouplingAnalyzer::new(
1472            "test_module".to_string(),
1473            std::path::PathBuf::from("test.rs"),
1474        );
1475        assert_eq!(analyzer.current_module, "test_module");
1476    }
1477
1478    #[test]
1479    fn test_analyze_simple_file() {
1480        let mut analyzer =
1481            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1482
1483        let code = r#"
1484            pub struct User {
1485                name: String,
1486                email: String,
1487            }
1488
1489            impl User {
1490                pub fn new(name: String, email: String) -> Self {
1491                    Self { name, email }
1492                }
1493            }
1494        "#;
1495
1496        let result = analyzer.analyze_file(code);
1497        assert!(result.is_ok());
1498        assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1499    }
1500
1501    #[test]
1502    fn test_item_dependencies() {
1503        let mut analyzer =
1504            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1505
1506        let code = r#"
1507            pub struct Config {
1508                pub value: i32,
1509            }
1510
1511            pub fn process(config: Config) -> i32 {
1512                let x = config.value;
1513                helper(x)
1514            }
1515
1516            fn helper(n: i32) -> i32 {
1517                n * 2
1518            }
1519        "#;
1520
1521        let result = analyzer.analyze_file(code);
1522        assert!(result.is_ok());
1523
1524        // Check that functions are recorded
1525        assert!(analyzer.defined_functions.contains_key("process"));
1526        assert!(analyzer.defined_functions.contains_key("helper"));
1527
1528        // Check item dependencies - process should have deps
1529        println!(
1530            "Item dependencies count: {}",
1531            analyzer.item_dependencies.len()
1532        );
1533        for dep in &analyzer.item_dependencies {
1534            println!(
1535                "  {} -> {} ({:?})",
1536                dep.source_item, dep.target, dep.dep_type
1537            );
1538        }
1539
1540        // process function should have dependencies
1541        let process_deps: Vec<_> = analyzer
1542            .item_dependencies
1543            .iter()
1544            .filter(|d| d.source_item == "process")
1545            .collect();
1546
1547        assert!(
1548            !process_deps.is_empty(),
1549            "process function should have item dependencies"
1550        );
1551    }
1552
1553    #[test]
1554    fn test_analyze_trait_impl() {
1555        let mut analyzer =
1556            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1557
1558        let code = r#"
1559            trait Printable {
1560                fn print(&self);
1561            }
1562
1563            struct Document;
1564
1565            impl Printable for Document {
1566                fn print(&self) {}
1567            }
1568        "#;
1569
1570        let result = analyzer.analyze_file(code);
1571        assert!(result.is_ok());
1572        assert!(analyzer.metrics.trait_impl_count >= 1);
1573    }
1574
1575    #[test]
1576    fn test_analyze_use_statements() {
1577        let mut analyzer =
1578            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1579
1580        let code = r#"
1581            use std::collections::HashMap;
1582            use serde::Serialize;
1583            use crate::utils;
1584            use crate::models::{User, Post};
1585        "#;
1586
1587        let result = analyzer.analyze_file(code);
1588        assert!(result.is_ok());
1589        assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1590        assert!(
1591            analyzer
1592                .metrics
1593                .external_deps
1594                .contains(&"serde".to_string())
1595        );
1596        assert!(!analyzer.dependencies.is_empty());
1597
1598        // Check internal dependencies
1599        let internal_deps: Vec<_> = analyzer
1600            .dependencies
1601            .iter()
1602            .filter(|d| d.kind == DependencyKind::InternalUse)
1603            .collect();
1604        assert!(!internal_deps.is_empty());
1605    }
1606
1607    #[test]
1608    fn test_extract_use_paths() {
1609        let analyzer =
1610            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1611
1612        // Test simple path
1613        let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1614        let paths = analyzer.extract_use_paths(&tree, "");
1615        assert_eq!(paths.len(), 1);
1616        assert_eq!(paths[0].0, "std::collections::HashMap");
1617
1618        // Test grouped path
1619        let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1620        let paths = analyzer.extract_use_paths(&tree, "");
1621        assert_eq!(paths.len(), 2);
1622    }
1623
1624    #[test]
1625    fn test_extract_target_module() {
1626        assert_eq!(extract_target_module("crate::models::user"), "models");
1627        assert_eq!(extract_target_module("super::utils"), "utils");
1628        assert_eq!(extract_target_module("std::collections"), "std");
1629    }
1630
1631    #[test]
1632    fn test_field_access_detection() {
1633        let mut analyzer =
1634            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1635
1636        let code = r#"
1637            use crate::models::User;
1638
1639            fn get_name(user: &User) -> String {
1640                user.name.clone()
1641            }
1642        "#;
1643
1644        let result = analyzer.analyze_file(code);
1645        assert!(result.is_ok());
1646
1647        // Should detect User as a dependency with field access
1648        let _field_deps: Vec<_> = analyzer
1649            .dependencies
1650            .iter()
1651            .filter(|d| d.usage == UsageContext::FieldAccess)
1652            .collect();
1653        // Note: This may not detect field access on function parameters
1654        // as the type info isn't fully available without type inference
1655    }
1656
1657    #[test]
1658    fn test_method_call_detection() {
1659        let mut analyzer =
1660            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1661
1662        let code = r#"
1663            fn process() {
1664                let data = String::new();
1665                data.push_str("hello");
1666            }
1667        "#;
1668
1669        let result = analyzer.analyze_file(code);
1670        assert!(result.is_ok());
1671        // Method calls on local variables are detected
1672    }
1673
1674    #[test]
1675    fn test_struct_construction_detection() {
1676        let mut analyzer =
1677            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1678
1679        let code = r#"
1680            use crate::config::Config;
1681
1682            fn create_config() {
1683                let c = Config { value: 42 };
1684            }
1685        "#;
1686
1687        let result = analyzer.analyze_file(code);
1688        assert!(result.is_ok());
1689
1690        // Should detect Config struct construction
1691        let struct_deps: Vec<_> = analyzer
1692            .dependencies
1693            .iter()
1694            .filter(|d| d.usage == UsageContext::StructConstruction)
1695            .collect();
1696        assert!(!struct_deps.is_empty());
1697    }
1698
1699    #[test]
1700    fn test_usage_context_to_strength() {
1701        assert_eq!(
1702            UsageContext::FieldAccess.to_strength(),
1703            IntegrationStrength::Intrusive
1704        );
1705        assert_eq!(
1706            UsageContext::MethodCall.to_strength(),
1707            IntegrationStrength::Functional
1708        );
1709        assert_eq!(
1710            UsageContext::TypeParameter.to_strength(),
1711            IntegrationStrength::Model
1712        );
1713        assert_eq!(
1714            UsageContext::TraitBound.to_strength(),
1715            IntegrationStrength::Contract
1716        );
1717    }
1718
1719    /// Test that rs_files correctly handles paths with hidden parent directories.
1720    /// Regression test for https://github.com/nwiizo/cargo-coupling/issues/7
1721    #[test]
1722    fn test_rs_files_with_hidden_parent_directory() {
1723        use std::fs;
1724        use tempfile::TempDir;
1725
1726        // Create a temporary directory structure that simulates a project
1727        // inside a hidden parent directory (e.g., /home/user/.local/projects/myproject)
1728        let temp = TempDir::new().unwrap();
1729        let hidden_parent = temp.path().join(".hidden-parent");
1730        let project_dir = hidden_parent.join("myproject").join("src");
1731        fs::create_dir_all(&project_dir).unwrap();
1732
1733        // Create some Rust files
1734        fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1735        fs::write(project_dir.join("main.rs"), "fn main() {}").unwrap();
1736
1737        // rs_files should find both files even though there's a hidden parent
1738        let files: Vec<_> = rs_files(&project_dir).collect();
1739        assert_eq!(
1740            files.len(),
1741            2,
1742            "Should find 2 .rs files in hidden parent path"
1743        );
1744
1745        // Verify the files are the ones we created
1746        let file_names: Vec<_> = files
1747            .iter()
1748            .filter_map(|p| p.file_name())
1749            .filter_map(|n| n.to_str())
1750            .collect();
1751        assert!(file_names.contains(&"lib.rs"));
1752        assert!(file_names.contains(&"main.rs"));
1753    }
1754
1755    /// Test that rs_files correctly excludes hidden directories within the project.
1756    #[test]
1757    fn test_rs_files_excludes_hidden_dirs_in_project() {
1758        use std::fs;
1759        use tempfile::TempDir;
1760
1761        let temp = TempDir::new().unwrap();
1762        let project_dir = temp.path().join("myproject").join("src");
1763        let hidden_dir = project_dir.join(".hidden");
1764        fs::create_dir_all(&hidden_dir).unwrap();
1765
1766        // Create files in both regular and hidden directories
1767        fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1768        fs::write(hidden_dir.join("secret.rs"), "fn secret() {}").unwrap();
1769
1770        // rs_files should only find lib.rs, not the file in .hidden
1771        let files: Vec<_> = rs_files(&project_dir).collect();
1772        assert_eq!(
1773            files.len(),
1774            1,
1775            "Should find only 1 .rs file (excluding .hidden/)"
1776        );
1777
1778        let file_names: Vec<_> = files
1779            .iter()
1780            .filter_map(|p| p.file_name())
1781            .filter_map(|n| n.to_str())
1782            .collect();
1783        assert!(file_names.contains(&"lib.rs"));
1784        assert!(!file_names.contains(&"secret.rs"));
1785    }
1786
1787    /// Test that rs_files correctly excludes the target directory.
1788    #[test]
1789    fn test_rs_files_excludes_target_directory() {
1790        use std::fs;
1791        use tempfile::TempDir;
1792
1793        let temp = TempDir::new().unwrap();
1794        let project_dir = temp.path().join("myproject");
1795        let src_dir = project_dir.join("src");
1796        let target_dir = project_dir.join("target").join("debug");
1797        fs::create_dir_all(&src_dir).unwrap();
1798        fs::create_dir_all(&target_dir).unwrap();
1799
1800        // Create files in both src and target directories
1801        fs::write(src_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1802        fs::write(target_dir.join("generated.rs"), "// generated").unwrap();
1803
1804        // rs_files should only find lib.rs, not the file in target/
1805        let files: Vec<_> = rs_files(&project_dir).collect();
1806        assert_eq!(
1807            files.len(),
1808            1,
1809            "Should find only 1 .rs file (excluding target/)"
1810        );
1811
1812        let file_names: Vec<_> = files
1813            .iter()
1814            .filter_map(|p| p.file_name())
1815            .filter_map(|n| n.to_str())
1816            .collect();
1817        assert!(file_names.contains(&"lib.rs"));
1818        assert!(!file_names.contains(&"generated.rs"));
1819    }
1820}