Skip to main content

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