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 module_name = if module_path.is_empty() {
1117                            // Crate root (lib.rs/main.rs) - use the original name
1118                            result.metrics.name.clone()
1119                        } else {
1120                            module_path
1121                        };
1122                        Some(AnalyzedFile {
1123                            module_name: module_name.clone(),
1124                            file_path: file_path.clone(),
1125                            metrics: {
1126                                let mut m = result.metrics;
1127                                m.name = module_name;
1128                                m
1129                            },
1130                            dependencies: result.dependencies,
1131                            type_visibility: result.type_visibility,
1132                            item_dependencies: result.item_dependencies,
1133                        })
1134                    }
1135                    Err(e) => {
1136                        eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1137                        None
1138                    }
1139                })
1140                .collect::<Vec<_>>()
1141        })
1142        .collect();
1143
1144    // Build module names set
1145    let module_names: HashSet<String> = analyzed_results
1146        .iter()
1147        .map(|a| a.module_name.clone())
1148        .collect();
1149
1150    // Build project metrics (sequential, but fast)
1151    let mut project = ProjectMetrics::new();
1152    project.total_files = analyzed_results.len();
1153
1154    // First pass: register all types with their visibility
1155    for analyzed in &analyzed_results {
1156        for (type_name, visibility) in &analyzed.type_visibility {
1157            project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
1158        }
1159    }
1160
1161    // Second pass: add modules and couplings
1162    for analyzed in &analyzed_results {
1163        // Clone metrics and add item_dependencies
1164        let mut metrics = analyzed.metrics.clone();
1165        metrics.item_dependencies = analyzed.item_dependencies.clone();
1166        project.add_module(metrics);
1167
1168        for dep in &analyzed.dependencies {
1169            // Skip invalid dependency paths (local variables, Self, etc.)
1170            if !is_valid_dependency_path(&dep.path) {
1171                continue;
1172            }
1173
1174            // Determine if this is an internal coupling
1175            let target_module = extract_target_module(&dep.path);
1176
1177            // Skip if target module looks invalid (but allow known module names)
1178            if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1179                continue;
1180            }
1181
1182            // Calculate distance
1183            let distance = calculate_distance(&dep.path, &module_names);
1184
1185            // Determine strength from usage context
1186            let strength = dep.usage.to_strength();
1187
1188            // Default volatility
1189            let volatility = Volatility::Low;
1190
1191            // Look up target visibility from the type registry
1192            let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
1193            let visibility = project
1194                .get_type_visibility(target_type)
1195                .unwrap_or(Visibility::Public); // Default to public if unknown
1196
1197            // Create coupling metric with location
1198            let coupling = CouplingMetrics::with_location(
1199                analyzed.module_name.clone(),
1200                target_module.clone(),
1201                strength,
1202                distance,
1203                volatility,
1204                visibility,
1205                analyzed.file_path.clone(),
1206                dep.line,
1207            );
1208
1209            project.add_coupling(coupling);
1210        }
1211    }
1212
1213    // Update any remaining coupling visibility information
1214    project.update_coupling_visibility();
1215
1216    Ok(project)
1217}
1218
1219/// Analyze a workspace using cargo metadata for better accuracy
1220pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1221    // Try to get workspace info
1222    let workspace = match WorkspaceInfo::from_path(path) {
1223        Ok(ws) => Some(ws),
1224        Err(e) => {
1225            eprintln!("Note: Could not load workspace metadata: {}", e);
1226            eprintln!("Falling back to basic analysis...");
1227            None
1228        }
1229    };
1230
1231    if let Some(ws) = workspace {
1232        analyze_with_workspace(path, &ws)
1233    } else {
1234        // Fall back to basic analysis
1235        analyze_project(path)
1236    }
1237}
1238
1239/// Analyze project with workspace information (parallel version)
1240fn analyze_with_workspace(
1241    _path: &Path,
1242    workspace: &WorkspaceInfo,
1243) -> Result<ProjectMetrics, AnalyzerError> {
1244    let mut project = ProjectMetrics::new();
1245
1246    // Store workspace info for the report
1247    project.workspace_name = Some(
1248        workspace
1249            .root
1250            .file_name()
1251            .and_then(|n| n.to_str())
1252            .unwrap_or("workspace")
1253            .to_string(),
1254    );
1255    project.workspace_members = workspace.members.clone();
1256
1257    // Collect all file paths with their crate names and src roots (sequential, fast)
1258    // Tuple: (file_path, crate_name, src_root)
1259    let mut file_crate_pairs: Vec<(PathBuf, String, PathBuf)> = Vec::new();
1260
1261    for member_name in &workspace.members {
1262        if let Some(crate_info) = workspace.get_crate(member_name) {
1263            if !crate_info.src_path.exists() {
1264                continue;
1265            }
1266
1267            let src_root = crate_info.src_path.clone();
1268            for file_path in rs_files(&crate_info.src_path) {
1269                file_crate_pairs.push((
1270                    file_path.to_path_buf(),
1271                    member_name.clone(),
1272                    src_root.clone(),
1273                ));
1274            }
1275        }
1276    }
1277
1278    // Calculate optimal chunk size for parallel processing
1279    let num_threads = rayon::current_num_threads();
1280    let file_count = file_crate_pairs.len();
1281    let chunk_size = if file_count < num_threads * 2 {
1282        1
1283    } else {
1284        (file_count / (num_threads * 4)).max(1)
1285    };
1286
1287    // Parallel file analysis with optimized chunking
1288    let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1289        .par_chunks(chunk_size)
1290        .flat_map(|chunk| {
1291            chunk
1292                .iter()
1293                .filter_map(|(file_path, crate_name, src_root)| {
1294                    match analyze_rust_file_full(file_path) {
1295                        Ok(result) => {
1296                            // Use full module path instead of just file stem (Issue #14)
1297                            let module_path = file_path_to_module_path(file_path, src_root);
1298                            let module_name = if module_path.is_empty() {
1299                                // Crate root (lib.rs/main.rs) - use the original name
1300                                result.metrics.name.clone()
1301                            } else {
1302                                module_path
1303                            };
1304                            Some(AnalyzedFileWithCrate {
1305                                module_name: module_name.clone(),
1306                                crate_name: crate_name.clone(),
1307                                file_path: file_path.clone(),
1308                                metrics: {
1309                                    let mut m = result.metrics;
1310                                    m.name = module_name;
1311                                    m
1312                                },
1313                                dependencies: result.dependencies,
1314                                item_dependencies: result.item_dependencies,
1315                            })
1316                        }
1317                        Err(e) => {
1318                            eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1319                            None
1320                        }
1321                    }
1322                })
1323                .collect::<Vec<_>>()
1324        })
1325        .collect();
1326
1327    project.total_files = analyzed_files.len();
1328
1329    // Build set of known module names for validation
1330    let module_names: HashSet<String> = analyzed_files
1331        .iter()
1332        .map(|a| a.module_name.clone())
1333        .collect();
1334
1335    // Second pass: build coupling relationships with workspace context
1336    for analyzed in &analyzed_files {
1337        // Clone metrics and add item_dependencies
1338        let mut metrics = analyzed.metrics.clone();
1339        metrics.item_dependencies = analyzed.item_dependencies.clone();
1340        project.add_module(metrics);
1341
1342        for dep in &analyzed.dependencies {
1343            // Skip invalid dependency paths (local variables, Self, etc.)
1344            if !is_valid_dependency_path(&dep.path) {
1345                continue;
1346            }
1347
1348            // Resolve the target crate using workspace info
1349            let resolved_crate =
1350                resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1351
1352            let target_module = extract_target_module(&dep.path);
1353
1354            // Skip if target module looks invalid (but allow known module names)
1355            if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1356                continue;
1357            }
1358
1359            // Calculate distance with workspace awareness
1360            let distance =
1361                calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1362
1363            // Determine strength from usage context (more accurate)
1364            let strength = dep.usage.to_strength();
1365
1366            // Default volatility
1367            let volatility = Volatility::Low;
1368
1369            // Create coupling metric with location info
1370            let mut coupling = CouplingMetrics::with_location(
1371                format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1372                if let Some(ref crate_name) = resolved_crate {
1373                    format!("{}::{}", crate_name, target_module)
1374                } else {
1375                    target_module.clone()
1376                },
1377                strength,
1378                distance,
1379                volatility,
1380                Visibility::Public, // Default visibility for workspace analysis
1381                analyzed.file_path.clone(),
1382                dep.line,
1383            );
1384
1385            // Add crate-level info
1386            coupling.source_crate = Some(analyzed.crate_name.clone());
1387            coupling.target_crate = resolved_crate;
1388
1389            project.add_coupling(coupling);
1390        }
1391    }
1392
1393    // Add crate-level dependency information
1394    for (crate_name, deps) in &workspace.dependency_graph {
1395        if workspace.is_workspace_member(crate_name) {
1396            for dep in deps {
1397                // Track crate-level dependencies
1398                project
1399                    .crate_dependencies
1400                    .entry(crate_name.clone())
1401                    .or_default()
1402                    .push(dep.clone());
1403            }
1404        }
1405    }
1406
1407    Ok(project)
1408}
1409
1410/// Calculate distance using workspace information
1411fn calculate_distance_with_workspace(
1412    dep_path: &str,
1413    current_crate: &str,
1414    workspace: &WorkspaceInfo,
1415) -> Distance {
1416    if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1417        // Same crate
1418        Distance::SameModule
1419    } else if dep_path.starts_with("super::") {
1420        // Could be same crate or parent module
1421        Distance::DifferentModule
1422    } else {
1423        // Resolve the target crate
1424        if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1425            if target_crate == current_crate {
1426                Distance::SameModule
1427            } else if workspace.is_workspace_member(&target_crate) {
1428                // Another workspace member
1429                Distance::DifferentModule
1430            } else {
1431                // External crate
1432                Distance::DifferentCrate
1433            }
1434        } else {
1435            Distance::DifferentCrate
1436        }
1437    }
1438}
1439
1440/// Analyzed file with crate information
1441#[derive(Debug, Clone)]
1442struct AnalyzedFileWithCrate {
1443    module_name: String,
1444    crate_name: String,
1445    #[allow(dead_code)]
1446    file_path: PathBuf,
1447    metrics: ModuleMetrics,
1448    dependencies: Vec<Dependency>,
1449    /// Item-level dependencies (function calls, field access, etc.)
1450    item_dependencies: Vec<ItemDependency>,
1451}
1452
1453/// Extract target module name from a path
1454fn extract_target_module(path: &str) -> String {
1455    // Remove common prefixes and get the module name
1456    let cleaned = path
1457        .trim_start_matches("crate::")
1458        .trim_start_matches("super::")
1459        .trim_start_matches("::");
1460
1461    // Get first significant segment
1462    cleaned.split("::").next().unwrap_or(path).to_string()
1463}
1464
1465/// Check if a path looks like a valid module/type reference (not a local variable)
1466fn is_valid_dependency_path(path: &str) -> bool {
1467    // Skip empty paths
1468    if path.is_empty() {
1469        return false;
1470    }
1471
1472    // Skip Self references
1473    if path == "Self" || path.starts_with("Self::") {
1474        return false;
1475    }
1476
1477    let segments: Vec<&str> = path.split("::").collect();
1478
1479    // Skip short single-segment lowercase names (likely local variables)
1480    if segments.len() == 1 {
1481        let name = segments[0];
1482        if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1483            return false;
1484        }
1485    }
1486
1487    // Skip patterns where last two segments are the same (likely module::type patterns from variables)
1488    if segments.len() >= 2 {
1489        let last = segments.last().unwrap();
1490        let second_last = segments.get(segments.len() - 2).unwrap();
1491        if last == second_last {
1492            return false;
1493        }
1494    }
1495
1496    // Skip common patterns that look like local variable accesses
1497    let last_segment = segments.last().unwrap_or(&path);
1498    let common_locals = [
1499        "request",
1500        "response",
1501        "result",
1502        "content",
1503        "config",
1504        "proto",
1505        "domain",
1506        "info",
1507        "data",
1508        "item",
1509        "value",
1510        "error",
1511        "message",
1512        "expected",
1513        "actual",
1514        "status",
1515        "state",
1516        "context",
1517        "params",
1518        "args",
1519        "options",
1520        "settings",
1521        "violation",
1522        "page_token",
1523    ];
1524    if common_locals.contains(last_segment) && segments.len() <= 2 {
1525        return false;
1526    }
1527
1528    true
1529}
1530
1531/// Calculate distance based on dependency path
1532fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1533    if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1534        // Internal dependency
1535        Distance::DifferentModule
1536    } else if dep_path.starts_with("self::") {
1537        Distance::SameModule
1538    } else {
1539        // External crate
1540        Distance::DifferentCrate
1541    }
1542}
1543
1544/// Analyze a single Rust file
1545/// Result of analyzing a single Rust file
1546pub struct AnalyzedFileResult {
1547    pub metrics: ModuleMetrics,
1548    pub dependencies: Vec<Dependency>,
1549    pub type_visibility: HashMap<String, Visibility>,
1550    pub item_dependencies: Vec<ItemDependency>,
1551}
1552
1553pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1554    let result = analyze_rust_file_full(path)?;
1555    Ok((result.metrics, result.dependencies))
1556}
1557
1558/// Analyze a Rust file and return full results including visibility
1559pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1560    let content = fs::read_to_string(path)?;
1561
1562    let module_name = path
1563        .file_stem()
1564        .and_then(|s| s.to_str())
1565        .unwrap_or("unknown")
1566        .to_string();
1567
1568    let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1569    analyzer.analyze_file(&content)?;
1570
1571    Ok(AnalyzedFileResult {
1572        metrics: analyzer.metrics,
1573        dependencies: analyzer.dependencies,
1574        type_visibility: analyzer.type_visibility,
1575        item_dependencies: analyzer.item_dependencies,
1576    })
1577}
1578
1579#[cfg(test)]
1580mod tests {
1581    use super::*;
1582
1583    #[test]
1584    fn test_analyzer_creation() {
1585        let analyzer = CouplingAnalyzer::new(
1586            "test_module".to_string(),
1587            std::path::PathBuf::from("test.rs"),
1588        );
1589        assert_eq!(analyzer.current_module, "test_module");
1590    }
1591
1592    #[test]
1593    fn test_analyze_simple_file() {
1594        let mut analyzer =
1595            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1596
1597        let code = r#"
1598            pub struct User {
1599                name: String,
1600                email: String,
1601            }
1602
1603            impl User {
1604                pub fn new(name: String, email: String) -> Self {
1605                    Self { name, email }
1606                }
1607            }
1608        "#;
1609
1610        let result = analyzer.analyze_file(code);
1611        assert!(result.is_ok());
1612        assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1613    }
1614
1615    #[test]
1616    fn test_item_dependencies() {
1617        let mut analyzer =
1618            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1619
1620        let code = r#"
1621            pub struct Config {
1622                pub value: i32,
1623            }
1624
1625            pub fn process(config: Config) -> i32 {
1626                let x = config.value;
1627                helper(x)
1628            }
1629
1630            fn helper(n: i32) -> i32 {
1631                n * 2
1632            }
1633        "#;
1634
1635        let result = analyzer.analyze_file(code);
1636        assert!(result.is_ok());
1637
1638        // Check that functions are recorded
1639        assert!(analyzer.defined_functions.contains_key("process"));
1640        assert!(analyzer.defined_functions.contains_key("helper"));
1641
1642        // Check item dependencies - process should have deps
1643        println!(
1644            "Item dependencies count: {}",
1645            analyzer.item_dependencies.len()
1646        );
1647        for dep in &analyzer.item_dependencies {
1648            println!(
1649                "  {} -> {} ({:?})",
1650                dep.source_item, dep.target, dep.dep_type
1651            );
1652        }
1653
1654        // process function should have dependencies
1655        let process_deps: Vec<_> = analyzer
1656            .item_dependencies
1657            .iter()
1658            .filter(|d| d.source_item == "process")
1659            .collect();
1660
1661        assert!(
1662            !process_deps.is_empty(),
1663            "process function should have item dependencies"
1664        );
1665    }
1666
1667    #[test]
1668    fn test_analyze_trait_impl() {
1669        let mut analyzer =
1670            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1671
1672        let code = r#"
1673            trait Printable {
1674                fn print(&self);
1675            }
1676
1677            struct Document;
1678
1679            impl Printable for Document {
1680                fn print(&self) {}
1681            }
1682        "#;
1683
1684        let result = analyzer.analyze_file(code);
1685        assert!(result.is_ok());
1686        assert!(analyzer.metrics.trait_impl_count >= 1);
1687    }
1688
1689    #[test]
1690    fn test_analyze_use_statements() {
1691        let mut analyzer =
1692            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1693
1694        let code = r#"
1695            use std::collections::HashMap;
1696            use serde::Serialize;
1697            use crate::utils;
1698            use crate::models::{User, Post};
1699        "#;
1700
1701        let result = analyzer.analyze_file(code);
1702        assert!(result.is_ok());
1703        assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1704        assert!(
1705            analyzer
1706                .metrics
1707                .external_deps
1708                .contains(&"serde".to_string())
1709        );
1710        assert!(!analyzer.dependencies.is_empty());
1711
1712        // Check internal dependencies
1713        let internal_deps: Vec<_> = analyzer
1714            .dependencies
1715            .iter()
1716            .filter(|d| d.kind == DependencyKind::InternalUse)
1717            .collect();
1718        assert!(!internal_deps.is_empty());
1719    }
1720
1721    #[test]
1722    fn test_extract_use_paths() {
1723        let analyzer =
1724            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1725
1726        // Test simple path
1727        let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1728        let paths = analyzer.extract_use_paths(&tree, "");
1729        assert_eq!(paths.len(), 1);
1730        assert_eq!(paths[0].0, "std::collections::HashMap");
1731
1732        // Test grouped path
1733        let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1734        let paths = analyzer.extract_use_paths(&tree, "");
1735        assert_eq!(paths.len(), 2);
1736    }
1737
1738    #[test]
1739    fn test_extract_target_module() {
1740        assert_eq!(extract_target_module("crate::models::user"), "models");
1741        assert_eq!(extract_target_module("super::utils"), "utils");
1742        assert_eq!(extract_target_module("std::collections"), "std");
1743    }
1744
1745    #[test]
1746    fn test_field_access_detection() {
1747        let mut analyzer =
1748            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1749
1750        let code = r#"
1751            use crate::models::User;
1752
1753            fn get_name(user: &User) -> String {
1754                user.name.clone()
1755            }
1756        "#;
1757
1758        let result = analyzer.analyze_file(code);
1759        assert!(result.is_ok());
1760
1761        // Should detect User as a dependency with field access
1762        let _field_deps: Vec<_> = analyzer
1763            .dependencies
1764            .iter()
1765            .filter(|d| d.usage == UsageContext::FieldAccess)
1766            .collect();
1767        // Note: This may not detect field access on function parameters
1768        // as the type info isn't fully available without type inference
1769    }
1770
1771    #[test]
1772    fn test_method_call_detection() {
1773        let mut analyzer =
1774            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1775
1776        let code = r#"
1777            fn process() {
1778                let data = String::new();
1779                data.push_str("hello");
1780            }
1781        "#;
1782
1783        let result = analyzer.analyze_file(code);
1784        assert!(result.is_ok());
1785        // Method calls on local variables are detected
1786    }
1787
1788    #[test]
1789    fn test_struct_construction_detection() {
1790        let mut analyzer =
1791            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1792
1793        let code = r#"
1794            use crate::config::Config;
1795
1796            fn create_config() {
1797                let c = Config { value: 42 };
1798            }
1799        "#;
1800
1801        let result = analyzer.analyze_file(code);
1802        assert!(result.is_ok());
1803
1804        // Should detect Config struct construction
1805        let struct_deps: Vec<_> = analyzer
1806            .dependencies
1807            .iter()
1808            .filter(|d| d.usage == UsageContext::StructConstruction)
1809            .collect();
1810        assert!(!struct_deps.is_empty());
1811    }
1812
1813    #[test]
1814    fn test_usage_context_to_strength() {
1815        assert_eq!(
1816            UsageContext::FieldAccess.to_strength(),
1817            IntegrationStrength::Intrusive
1818        );
1819        assert_eq!(
1820            UsageContext::MethodCall.to_strength(),
1821            IntegrationStrength::Functional
1822        );
1823        assert_eq!(
1824            UsageContext::TypeParameter.to_strength(),
1825            IntegrationStrength::Model
1826        );
1827        assert_eq!(
1828            UsageContext::TraitBound.to_strength(),
1829            IntegrationStrength::Contract
1830        );
1831    }
1832
1833    /// Test that rs_files correctly handles paths with hidden parent directories.
1834    /// Regression test for https://github.com/nwiizo/cargo-coupling/issues/7
1835    #[test]
1836    fn test_rs_files_with_hidden_parent_directory() {
1837        use std::fs;
1838        use tempfile::TempDir;
1839
1840        // Create a temporary directory structure that simulates a project
1841        // inside a hidden parent directory (e.g., /home/user/.local/projects/myproject)
1842        let temp = TempDir::new().unwrap();
1843        let hidden_parent = temp.path().join(".hidden-parent");
1844        let project_dir = hidden_parent.join("myproject").join("src");
1845        fs::create_dir_all(&project_dir).unwrap();
1846
1847        // Create some Rust files
1848        fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1849        fs::write(project_dir.join("main.rs"), "fn main() {}").unwrap();
1850
1851        // rs_files should find both files even though there's a hidden parent
1852        let files: Vec<_> = rs_files(&project_dir).collect();
1853        assert_eq!(
1854            files.len(),
1855            2,
1856            "Should find 2 .rs files in hidden parent path"
1857        );
1858
1859        // Verify the files are the ones we created
1860        let file_names: Vec<_> = files
1861            .iter()
1862            .filter_map(|p| p.file_name())
1863            .filter_map(|n| n.to_str())
1864            .collect();
1865        assert!(file_names.contains(&"lib.rs"));
1866        assert!(file_names.contains(&"main.rs"));
1867    }
1868
1869    /// Test that rs_files correctly excludes hidden directories within the project.
1870    #[test]
1871    fn test_rs_files_excludes_hidden_dirs_in_project() {
1872        use std::fs;
1873        use tempfile::TempDir;
1874
1875        let temp = TempDir::new().unwrap();
1876        let project_dir = temp.path().join("myproject").join("src");
1877        let hidden_dir = project_dir.join(".hidden");
1878        fs::create_dir_all(&hidden_dir).unwrap();
1879
1880        // Create files in both regular and hidden directories
1881        fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1882        fs::write(hidden_dir.join("secret.rs"), "fn secret() {}").unwrap();
1883
1884        // rs_files should only find lib.rs, not the file in .hidden
1885        let files: Vec<_> = rs_files(&project_dir).collect();
1886        assert_eq!(
1887            files.len(),
1888            1,
1889            "Should find only 1 .rs file (excluding .hidden/)"
1890        );
1891
1892        let file_names: Vec<_> = files
1893            .iter()
1894            .filter_map(|p| p.file_name())
1895            .filter_map(|n| n.to_str())
1896            .collect();
1897        assert!(file_names.contains(&"lib.rs"));
1898        assert!(!file_names.contains(&"secret.rs"));
1899    }
1900
1901    /// Test that rs_files correctly excludes the target directory.
1902    #[test]
1903    fn test_rs_files_excludes_target_directory() {
1904        use std::fs;
1905        use tempfile::TempDir;
1906
1907        let temp = TempDir::new().unwrap();
1908        let project_dir = temp.path().join("myproject");
1909        let src_dir = project_dir.join("src");
1910        let target_dir = project_dir.join("target").join("debug");
1911        fs::create_dir_all(&src_dir).unwrap();
1912        fs::create_dir_all(&target_dir).unwrap();
1913
1914        // Create files in both src and target directories
1915        fs::write(src_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1916        fs::write(target_dir.join("generated.rs"), "// generated").unwrap();
1917
1918        // rs_files should only find lib.rs, not the file in target/
1919        let files: Vec<_> = rs_files(&project_dir).collect();
1920        assert_eq!(
1921            files.len(),
1922            1,
1923            "Should find only 1 .rs file (excluding target/)"
1924        );
1925
1926        let file_names: Vec<_> = files
1927            .iter()
1928            .filter_map(|p| p.file_name())
1929            .filter_map(|n| n.to_str())
1930            .collect();
1931        assert!(file_names.contains(&"lib.rs"));
1932        assert!(!file_names.contains(&"generated.rs"));
1933    }
1934}