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