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::connascence::{ConnascenceAnalyzer, ConnascenceType, detect_algorithm_patterns};
22use crate::metrics::{
23    CouplingMetrics, Distance, IntegrationStrength, ModuleMetrics, ProjectMetrics, Visibility,
24    Volatility,
25};
26use crate::workspace::{WorkspaceError, WorkspaceInfo, resolve_crate_from_path};
27
28/// Convert syn's Visibility to our Visibility enum
29fn convert_visibility(vis: &syn::Visibility) -> Visibility {
30    match vis {
31        syn::Visibility::Public(_) => Visibility::Public,
32        syn::Visibility::Restricted(restricted) => {
33            // Check the path to determine the restriction type
34            let path_str = restricted
35                .path
36                .segments
37                .iter()
38                .map(|s| s.ident.to_string())
39                .collect::<Vec<_>>()
40                .join("::");
41
42            match path_str.as_str() {
43                "crate" => Visibility::PubCrate,
44                "super" => Visibility::PubSuper,
45                "self" => Visibility::Private, // pub(self) is effectively private
46                _ => Visibility::PubIn,        // pub(in path)
47            }
48        }
49        syn::Visibility::Inherited => Visibility::Private,
50    }
51}
52
53/// Errors that can occur during analysis
54#[derive(Error, Debug)]
55pub enum AnalyzerError {
56    #[error("Failed to read file: {0}")]
57    IoError(#[from] std::io::Error),
58
59    #[error("Failed to parse Rust file: {0}")]
60    ParseError(String),
61
62    #[error("Invalid path: {0}")]
63    InvalidPath(String),
64
65    #[error("Workspace error: {0}")]
66    WorkspaceError(#[from] WorkspaceError),
67}
68
69/// Represents a detected dependency
70#[derive(Debug, Clone)]
71pub struct Dependency {
72    /// Full path of the dependency (e.g., "crate::models::user")
73    pub path: String,
74    /// Type of dependency
75    pub kind: DependencyKind,
76    /// Line number where the dependency is declared
77    pub line: usize,
78    /// Usage context for more accurate strength determination
79    pub usage: UsageContext,
80}
81
82/// Kind of dependency
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum DependencyKind {
85    /// use crate::xxx or use super::xxx
86    InternalUse,
87    /// use external_crate::xxx
88    ExternalUse,
89    /// impl Trait for Type
90    TraitImpl,
91    /// impl Type
92    InherentImpl,
93    /// Type reference in struct fields, function params, etc.
94    TypeRef,
95}
96
97/// Context of how a dependency is used - determines Integration Strength
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub enum UsageContext {
100    /// Just imported, usage unknown
101    Import,
102    /// Used as a trait bound or trait impl
103    TraitBound,
104    /// Field access: `foo.bar`
105    FieldAccess,
106    /// Method call: `foo.method()`
107    MethodCall,
108    /// Function call: `Foo::new()` or `foo()`
109    FunctionCall,
110    /// Struct construction: `Foo { field: value }`
111    StructConstruction,
112    /// Type parameter: `Vec<Foo>`
113    TypeParameter,
114    /// Function parameter type
115    FunctionParameter,
116    /// Return type
117    ReturnType,
118    /// Inherent impl block
119    InherentImplBlock,
120}
121
122impl UsageContext {
123    /// Convert usage context to integration strength
124    pub fn to_strength(&self) -> IntegrationStrength {
125        match self {
126            // Intrusive: Direct access to internals
127            UsageContext::FieldAccess => IntegrationStrength::Intrusive,
128            UsageContext::StructConstruction => IntegrationStrength::Intrusive,
129            UsageContext::InherentImplBlock => IntegrationStrength::Intrusive,
130
131            // Functional: Depends on function signatures
132            UsageContext::MethodCall => IntegrationStrength::Functional,
133            UsageContext::FunctionCall => IntegrationStrength::Functional,
134            UsageContext::FunctionParameter => IntegrationStrength::Functional,
135            UsageContext::ReturnType => IntegrationStrength::Functional,
136
137            // Model: Uses data types
138            UsageContext::TypeParameter => IntegrationStrength::Model,
139            UsageContext::Import => IntegrationStrength::Model,
140
141            // Contract: Uses traits/interfaces
142            UsageContext::TraitBound => IntegrationStrength::Contract,
143        }
144    }
145}
146
147impl DependencyKind {
148    pub fn to_strength(&self) -> IntegrationStrength {
149        match self {
150            DependencyKind::TraitImpl => IntegrationStrength::Contract,
151            DependencyKind::InternalUse => IntegrationStrength::Model,
152            DependencyKind::ExternalUse => IntegrationStrength::Model,
153            DependencyKind::TypeRef => IntegrationStrength::Model,
154            DependencyKind::InherentImpl => IntegrationStrength::Intrusive,
155        }
156    }
157}
158
159/// AST visitor for coupling analysis
160#[derive(Debug)]
161pub struct CouplingAnalyzer {
162    /// Current module being analyzed
163    pub current_module: String,
164    /// File path
165    pub file_path: std::path::PathBuf,
166    /// Collected metrics
167    pub metrics: ModuleMetrics,
168    /// Detected dependencies
169    pub dependencies: Vec<Dependency>,
170    /// Defined types in this module
171    pub defined_types: HashSet<String>,
172    /// Defined traits in this module
173    pub defined_traits: HashSet<String>,
174    /// Imported types (name -> full path)
175    imported_types: HashMap<String, String>,
176    /// Track unique dependencies to avoid duplicates
177    seen_dependencies: HashSet<(String, UsageContext)>,
178    /// Counts of each usage type for statistics
179    pub usage_counts: UsageCounts,
180    /// Type visibility map: type name -> visibility
181    pub type_visibility: HashMap<String, Visibility>,
182    /// Connascence analyzer for detailed coupling type detection
183    pub connascence: ConnascenceAnalyzer,
184}
185
186/// Statistics about usage patterns
187#[derive(Debug, Default, Clone)]
188pub struct UsageCounts {
189    pub field_accesses: usize,
190    pub method_calls: usize,
191    pub function_calls: usize,
192    pub struct_constructions: usize,
193    pub trait_bounds: usize,
194    pub type_parameters: usize,
195}
196
197impl CouplingAnalyzer {
198    /// Create a new analyzer for a module
199    pub fn new(module_name: String, path: std::path::PathBuf) -> Self {
200        let mut connascence = ConnascenceAnalyzer::new();
201        connascence.set_module(module_name.clone());
202
203        Self {
204            current_module: module_name.clone(),
205            file_path: path.clone(),
206            metrics: ModuleMetrics::new(path, module_name),
207            dependencies: Vec::new(),
208            defined_types: HashSet::new(),
209            defined_traits: HashSet::new(),
210            imported_types: HashMap::new(),
211            seen_dependencies: HashSet::new(),
212            usage_counts: UsageCounts::default(),
213            type_visibility: HashMap::new(),
214            connascence,
215        }
216    }
217
218    /// Analyze a Rust source file
219    pub fn analyze_file(&mut self, content: &str) -> Result<(), AnalyzerError> {
220        let syntax: File =
221            syn::parse_file(content).map_err(|e| AnalyzerError::ParseError(e.to_string()))?;
222
223        self.visit_file(&syntax);
224
225        // Detect algorithm patterns (encode/decode, serialize/deserialize, etc.)
226        for (pattern, context) in detect_algorithm_patterns(content) {
227            self.connascence
228                .record_algorithm_dependency(pattern, &context);
229        }
230
231        Ok(())
232    }
233
234    /// Add a dependency with deduplication
235    fn add_dependency(&mut self, path: String, kind: DependencyKind, usage: UsageContext) {
236        let key = (path.clone(), usage);
237        if self.seen_dependencies.contains(&key) {
238            return;
239        }
240        self.seen_dependencies.insert(key);
241
242        // Record connascence based on usage context
243        let connascence_type = match usage {
244            UsageContext::Import => ConnascenceType::Name,
245            UsageContext::TraitBound => ConnascenceType::Type,
246            UsageContext::TypeParameter => ConnascenceType::Type,
247            UsageContext::FunctionParameter => ConnascenceType::Type,
248            UsageContext::ReturnType => ConnascenceType::Type,
249            UsageContext::MethodCall | UsageContext::FunctionCall => ConnascenceType::Name,
250            UsageContext::FieldAccess => ConnascenceType::Name,
251            UsageContext::StructConstruction => ConnascenceType::Position, // Positional fields
252            UsageContext::InherentImplBlock => ConnascenceType::Type,
253        };
254
255        // Record the connascence instance
256        match connascence_type {
257            ConnascenceType::Name => {
258                self.connascence
259                    .record_name_dependency(&path, &format!("{:?}", usage));
260            }
261            ConnascenceType::Type => {
262                self.connascence
263                    .record_type_dependency(&path, &format!("{:?}", usage));
264            }
265            ConnascenceType::Position => {
266                // Will be handled separately for function args
267                self.connascence
268                    .record_name_dependency(&path, &format!("{:?}", usage));
269            }
270            _ => {}
271        }
272
273        self.dependencies.push(Dependency {
274            path,
275            kind,
276            line: 0,
277            usage,
278        });
279    }
280
281    /// Extract full path from UseTree recursively
282    fn extract_use_paths(&self, tree: &UseTree, prefix: &str) -> Vec<(String, DependencyKind)> {
283        let mut paths = Vec::new();
284
285        match tree {
286            UseTree::Path(path) => {
287                let new_prefix = if prefix.is_empty() {
288                    path.ident.to_string()
289                } else {
290                    format!("{}::{}", prefix, path.ident)
291                };
292                paths.extend(self.extract_use_paths(&path.tree, &new_prefix));
293            }
294            UseTree::Name(name) => {
295                let full_path = if prefix.is_empty() {
296                    name.ident.to_string()
297                } else {
298                    format!("{}::{}", prefix, name.ident)
299                };
300                let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
301                    DependencyKind::InternalUse
302                } else {
303                    DependencyKind::ExternalUse
304                };
305                paths.push((full_path, kind));
306            }
307            UseTree::Rename(rename) => {
308                let full_path = if prefix.is_empty() {
309                    rename.ident.to_string()
310                } else {
311                    format!("{}::{}", prefix, rename.ident)
312                };
313                let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
314                    DependencyKind::InternalUse
315                } else {
316                    DependencyKind::ExternalUse
317                };
318                paths.push((full_path, kind));
319            }
320            UseTree::Glob(_) => {
321                let full_path = format!("{}::*", prefix);
322                let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
323                    DependencyKind::InternalUse
324                } else {
325                    DependencyKind::ExternalUse
326                };
327                paths.push((full_path, kind));
328            }
329            UseTree::Group(group) => {
330                for item in &group.items {
331                    paths.extend(self.extract_use_paths(item, prefix));
332                }
333            }
334        }
335
336        paths
337    }
338
339    /// Extract type name from a Type
340    fn extract_type_name(&self, ty: &Type) -> Option<String> {
341        match ty {
342            Type::Path(type_path) => {
343                let segments: Vec<_> = type_path
344                    .path
345                    .segments
346                    .iter()
347                    .map(|s| s.ident.to_string())
348                    .collect();
349                Some(segments.join("::"))
350            }
351            Type::Reference(ref_type) => self.extract_type_name(&ref_type.elem),
352            Type::Slice(slice_type) => self.extract_type_name(&slice_type.elem),
353            Type::Array(array_type) => self.extract_type_name(&array_type.elem),
354            Type::Ptr(ptr_type) => self.extract_type_name(&ptr_type.elem),
355            Type::Paren(paren_type) => self.extract_type_name(&paren_type.elem),
356            Type::Group(group_type) => self.extract_type_name(&group_type.elem),
357            _ => None,
358        }
359    }
360
361    /// Analyze function signature for dependencies
362    fn analyze_signature(&mut self, sig: &Signature) {
363        let fn_name = sig.ident.to_string();
364
365        // Count non-self arguments for position connascence detection
366        let arg_count = sig
367            .inputs
368            .iter()
369            .filter(|arg| !matches!(arg, FnArg::Receiver(_)))
370            .count();
371
372        // Record position connascence for functions with many arguments (4+)
373        self.connascence
374            .record_position_dependency(&fn_name, arg_count);
375
376        // Analyze parameters
377        for arg in &sig.inputs {
378            if let FnArg::Typed(pat_type) = arg
379                && let Some(type_name) = self.extract_type_name(&pat_type.ty)
380                && !self.is_primitive_type(&type_name)
381            {
382                self.add_dependency(
383                    type_name,
384                    DependencyKind::TypeRef,
385                    UsageContext::FunctionParameter,
386                );
387            }
388        }
389
390        // Analyze return type
391        if let ReturnType::Type(_, ty) = &sig.output
392            && let Some(type_name) = self.extract_type_name(ty)
393            && !self.is_primitive_type(&type_name)
394        {
395            self.add_dependency(type_name, DependencyKind::TypeRef, UsageContext::ReturnType);
396        }
397    }
398
399    /// Check if a type should be ignored (primitives, self, or short variable names)
400    fn is_primitive_type(&self, type_name: &str) -> bool {
401        // Primitive types
402        if matches!(
403            type_name,
404            "bool"
405                | "char"
406                | "str"
407                | "u8"
408                | "u16"
409                | "u32"
410                | "u64"
411                | "u128"
412                | "usize"
413                | "i8"
414                | "i16"
415                | "i32"
416                | "i64"
417                | "i128"
418                | "isize"
419                | "f32"
420                | "f64"
421                | "String"
422                | "Self"
423                | "()"
424                | "Option"
425                | "Result"
426                | "Vec"
427                | "Box"
428                | "Rc"
429                | "Arc"
430                | "RefCell"
431                | "Cell"
432                | "Mutex"
433                | "RwLock"
434        ) {
435            return true;
436        }
437
438        // Short variable names (likely local variables, not types)
439        // Type names in Rust are typically PascalCase and longer
440        if type_name.len() <= 3 && type_name.chars().all(|c| c.is_lowercase()) {
441            return true;
442        }
443
444        // Self-references or obviously local
445        if type_name.starts_with("self") || type_name == "self" {
446            return true;
447        }
448
449        false
450    }
451}
452
453impl<'ast> Visit<'ast> for CouplingAnalyzer {
454    fn visit_item_use(&mut self, node: &'ast ItemUse) {
455        let paths = self.extract_use_paths(&node.tree, "");
456
457        for (path, kind) in paths {
458            // Skip self references
459            if path == "self" || path.starts_with("self::") {
460                continue;
461            }
462
463            // Track imported types for later resolution
464            if let Some(type_name) = path.split("::").last() {
465                self.imported_types
466                    .insert(type_name.to_string(), path.clone());
467            }
468
469            self.add_dependency(path.clone(), kind, UsageContext::Import);
470
471            // Update metrics
472            if kind == DependencyKind::InternalUse {
473                if !self.metrics.internal_deps.contains(&path) {
474                    self.metrics.internal_deps.push(path.clone());
475                }
476            } else if kind == DependencyKind::ExternalUse {
477                // Extract crate name
478                let crate_name = path.split("::").next().unwrap_or(&path).to_string();
479                if !self.metrics.external_deps.contains(&crate_name) {
480                    self.metrics.external_deps.push(crate_name);
481                }
482            }
483        }
484
485        syn::visit::visit_item_use(self, node);
486    }
487
488    fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
489        if let Some((_, trait_path, _)) = &node.trait_ {
490            // Trait implementation = Contract coupling
491            self.metrics.trait_impl_count += 1;
492
493            // Extract trait path
494            let trait_name: String = trait_path
495                .segments
496                .iter()
497                .map(|s| s.ident.to_string())
498                .collect::<Vec<_>>()
499                .join("::");
500
501            self.add_dependency(
502                trait_name,
503                DependencyKind::TraitImpl,
504                UsageContext::TraitBound,
505            );
506            self.usage_counts.trait_bounds += 1;
507        } else {
508            // Inherent implementation = Intrusive coupling
509            self.metrics.inherent_impl_count += 1;
510
511            // Get the type being implemented
512            if let Some(type_name) = self.extract_type_name(&node.self_ty)
513                && !self.defined_types.contains(&type_name)
514            {
515                self.add_dependency(
516                    type_name,
517                    DependencyKind::InherentImpl,
518                    UsageContext::InherentImplBlock,
519                );
520            }
521        }
522        syn::visit::visit_item_impl(self, node);
523    }
524
525    fn visit_item_fn(&mut self, node: &'ast ItemFn) {
526        // Analyze function signature
527        self.analyze_signature(&node.sig);
528        syn::visit::visit_item_fn(self, node);
529    }
530
531    fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
532        let name = node.ident.to_string();
533        let visibility = convert_visibility(&node.vis);
534
535        self.defined_types.insert(name.clone());
536        self.type_visibility.insert(name.clone(), visibility);
537
538        // Register in module metrics with visibility
539        self.metrics.add_type_definition(name, visibility, false);
540
541        // Analyze struct fields for type dependencies
542        match &node.fields {
543            syn::Fields::Named(fields) => {
544                self.metrics.type_usage_count += fields.named.len();
545                for field in &fields.named {
546                    if let Some(type_name) = self.extract_type_name(&field.ty)
547                        && !self.is_primitive_type(&type_name)
548                    {
549                        self.add_dependency(
550                            type_name,
551                            DependencyKind::TypeRef,
552                            UsageContext::TypeParameter,
553                        );
554                        self.usage_counts.type_parameters += 1;
555                    }
556                }
557            }
558            syn::Fields::Unnamed(fields) => {
559                for field in &fields.unnamed {
560                    if let Some(type_name) = self.extract_type_name(&field.ty)
561                        && !self.is_primitive_type(&type_name)
562                    {
563                        self.add_dependency(
564                            type_name,
565                            DependencyKind::TypeRef,
566                            UsageContext::TypeParameter,
567                        );
568                    }
569                }
570            }
571            syn::Fields::Unit => {}
572        }
573        syn::visit::visit_item_struct(self, node);
574    }
575
576    fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
577        let name = node.ident.to_string();
578        let visibility = convert_visibility(&node.vis);
579
580        self.defined_types.insert(name.clone());
581        self.type_visibility.insert(name.clone(), visibility);
582
583        // Register in module metrics with visibility
584        self.metrics.add_type_definition(name, visibility, false);
585
586        // Analyze enum variants for type dependencies
587        for variant in &node.variants {
588            match &variant.fields {
589                syn::Fields::Named(fields) => {
590                    for field in &fields.named {
591                        if let Some(type_name) = self.extract_type_name(&field.ty)
592                            && !self.is_primitive_type(&type_name)
593                        {
594                            self.add_dependency(
595                                type_name,
596                                DependencyKind::TypeRef,
597                                UsageContext::TypeParameter,
598                            );
599                        }
600                    }
601                }
602                syn::Fields::Unnamed(fields) => {
603                    for field in &fields.unnamed {
604                        if let Some(type_name) = self.extract_type_name(&field.ty)
605                            && !self.is_primitive_type(&type_name)
606                        {
607                            self.add_dependency(
608                                type_name,
609                                DependencyKind::TypeRef,
610                                UsageContext::TypeParameter,
611                            );
612                        }
613                    }
614                }
615                syn::Fields::Unit => {}
616            }
617        }
618        syn::visit::visit_item_enum(self, node);
619    }
620
621    fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
622        let name = node.ident.to_string();
623        let visibility = convert_visibility(&node.vis);
624
625        self.defined_traits.insert(name.clone());
626        self.type_visibility.insert(name.clone(), visibility);
627
628        // Register in module metrics with visibility (is_trait = true)
629        self.metrics.add_type_definition(name, visibility, true);
630
631        self.metrics.trait_impl_count += 1;
632        syn::visit::visit_item_trait(self, node);
633    }
634
635    fn visit_item_mod(&mut self, node: &'ast ItemMod) {
636        if node.content.is_some() {
637            self.metrics.internal_deps.push(node.ident.to_string());
638        }
639        syn::visit::visit_item_mod(self, node);
640    }
641
642    // Detect field access: `foo.bar`
643    fn visit_expr_field(&mut self, node: &'ast ExprField) {
644        // This is a field access - Intrusive coupling
645        if let Expr::Path(path_expr) = &*node.base {
646            let base_name = path_expr
647                .path
648                .segments
649                .iter()
650                .map(|s| s.ident.to_string())
651                .collect::<Vec<_>>()
652                .join("::");
653
654            // Resolve to full path if imported
655            let full_path = self
656                .imported_types
657                .get(&base_name)
658                .cloned()
659                .unwrap_or(base_name);
660
661            if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
662                self.add_dependency(
663                    full_path,
664                    DependencyKind::TypeRef,
665                    UsageContext::FieldAccess,
666                );
667                self.usage_counts.field_accesses += 1;
668            }
669        }
670        syn::visit::visit_expr_field(self, node);
671    }
672
673    // Detect method calls: `foo.method()`
674    fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
675        // This is a method call - Functional coupling
676        if let Expr::Path(path_expr) = &*node.receiver {
677            let receiver_name = path_expr
678                .path
679                .segments
680                .iter()
681                .map(|s| s.ident.to_string())
682                .collect::<Vec<_>>()
683                .join("::");
684
685            let full_path = self
686                .imported_types
687                .get(&receiver_name)
688                .cloned()
689                .unwrap_or(receiver_name);
690
691            if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
692                self.add_dependency(full_path, DependencyKind::TypeRef, UsageContext::MethodCall);
693                self.usage_counts.method_calls += 1;
694            }
695        }
696        syn::visit::visit_expr_method_call(self, node);
697    }
698
699    // Detect function calls: `Foo::new()` or `foo()`
700    fn visit_expr_call(&mut self, node: &'ast ExprCall) {
701        if let Expr::Path(path_expr) = &*node.func {
702            let path_str = path_expr
703                .path
704                .segments
705                .iter()
706                .map(|s| s.ident.to_string())
707                .collect::<Vec<_>>()
708                .join("::");
709
710            // Check if this is a constructor or associated function call
711            if path_str.contains("::") || path_str.chars().next().is_some_and(|c| c.is_uppercase())
712            {
713                let full_path = self
714                    .imported_types
715                    .get(&path_str)
716                    .cloned()
717                    .unwrap_or(path_str.clone());
718
719                if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
720                    self.add_dependency(
721                        full_path,
722                        DependencyKind::TypeRef,
723                        UsageContext::FunctionCall,
724                    );
725                    self.usage_counts.function_calls += 1;
726                }
727            }
728        }
729        syn::visit::visit_expr_call(self, node);
730    }
731
732    // Detect struct construction: `Foo { field: value }`
733    fn visit_expr_struct(&mut self, node: &'ast ExprStruct) {
734        let struct_name = node
735            .path
736            .segments
737            .iter()
738            .map(|s| s.ident.to_string())
739            .collect::<Vec<_>>()
740            .join("::");
741
742        // Skip Self and self constructions
743        if struct_name == "Self" || struct_name.starts_with("Self::") {
744            syn::visit::visit_expr_struct(self, node);
745            return;
746        }
747
748        let full_path = self
749            .imported_types
750            .get(&struct_name)
751            .cloned()
752            .unwrap_or(struct_name.clone());
753
754        if !self.defined_types.contains(&full_path) && !self.is_primitive_type(&struct_name) {
755            self.add_dependency(
756                full_path,
757                DependencyKind::TypeRef,
758                UsageContext::StructConstruction,
759            );
760            self.usage_counts.struct_constructions += 1;
761        }
762        syn::visit::visit_expr_struct(self, node);
763    }
764}
765
766/// Analyzed file data
767#[derive(Debug, Clone)]
768struct AnalyzedFile {
769    module_name: String,
770    #[allow(dead_code)]
771    file_path: PathBuf,
772    metrics: ModuleMetrics,
773    dependencies: Vec<Dependency>,
774    /// Type visibility information from this file
775    type_visibility: HashMap<String, Visibility>,
776    /// Connascence analysis from this file
777    connascence: ConnascenceAnalyzer,
778}
779
780/// Analyze an entire project (parallel version)
781pub fn analyze_project(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
782    analyze_project_parallel(path)
783}
784
785/// Analyze a project using parallel processing with Rayon
786///
787/// Automatically scales to available CPU cores. The parallel processing
788/// uses work-stealing for optimal load balancing across cores.
789pub fn analyze_project_parallel(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
790    if !path.exists() {
791        return Err(AnalyzerError::InvalidPath(path.display().to_string()));
792    }
793
794    // Collect all .rs file paths first (sequential, but fast)
795    let file_paths: Vec<PathBuf> = WalkDir::new(path)
796        .follow_links(true)
797        .into_iter()
798        .filter_map(|e| e.ok())
799        .filter(|entry| {
800            let file_path = entry.path();
801            // Skip target directory and hidden directories
802            !file_path.components().any(|c| {
803                let s = c.as_os_str().to_string_lossy();
804                s == "target" || s.starts_with('.')
805            }) && file_path.extension() == Some(OsStr::new("rs"))
806        })
807        .map(|e| e.path().to_path_buf())
808        .collect();
809
810    // Calculate optimal chunk size based on file count and available parallelism
811    // Smaller chunks = better load balancing, but more overhead
812    // Larger chunks = less overhead, but potential load imbalance
813    let num_threads = rayon::current_num_threads();
814    let file_count = file_paths.len();
815
816    // Use smaller chunks for better load balancing with work-stealing
817    // Minimum chunk size of 1, maximum of file_count / (num_threads * 4)
818    let chunk_size = if file_count < num_threads * 2 {
819        1 // Small projects: process one file at a time
820    } else {
821        // Larger projects: balance between parallelism and overhead
822        // Use ~4 chunks per thread for good work-stealing behavior
823        (file_count / (num_threads * 4)).max(1)
824    };
825
826    // Parallel file analysis with optimized chunking
827    let analyzed_results: Vec<_> = file_paths
828        .par_chunks(chunk_size)
829        .flat_map(|chunk| {
830            chunk
831                .iter()
832                .filter_map(|file_path| match analyze_rust_file_full(file_path) {
833                    Ok(result) => Some(AnalyzedFile {
834                        module_name: result.metrics.name.clone(),
835                        file_path: file_path.clone(),
836                        metrics: result.metrics,
837                        dependencies: result.dependencies,
838                        type_visibility: result.type_visibility,
839                        connascence: result.connascence,
840                    }),
841                    Err(e) => {
842                        eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
843                        None
844                    }
845                })
846                .collect::<Vec<_>>()
847        })
848        .collect();
849
850    // Build module names set
851    let module_names: HashSet<String> = analyzed_results
852        .iter()
853        .map(|a| a.module_name.clone())
854        .collect();
855
856    // Build project metrics (sequential, but fast)
857    let mut project = ProjectMetrics::new();
858    project.total_files = analyzed_results.len();
859
860    // First pass: register all types with their visibility
861    for analyzed in &analyzed_results {
862        for (type_name, visibility) in &analyzed.type_visibility {
863            project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
864        }
865    }
866
867    // Second pass: add modules and couplings
868    for analyzed in &analyzed_results {
869        project.add_module(analyzed.metrics.clone());
870
871        for dep in &analyzed.dependencies {
872            // Skip invalid dependency paths (local variables, Self, etc.)
873            if !is_valid_dependency_path(&dep.path) {
874                continue;
875            }
876
877            // Determine if this is an internal coupling
878            let target_module = extract_target_module(&dep.path);
879
880            // Skip if target module looks invalid
881            if !is_valid_dependency_path(&target_module) {
882                continue;
883            }
884
885            // Calculate distance
886            let distance = calculate_distance(&dep.path, &module_names);
887
888            // Determine strength from usage context
889            let strength = dep.usage.to_strength();
890
891            // Default volatility
892            let volatility = Volatility::Low;
893
894            // Look up target visibility from the type registry
895            let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
896            let visibility = project
897                .get_type_visibility(target_type)
898                .unwrap_or(Visibility::Public); // Default to public if unknown
899
900            // Create coupling metric with visibility
901            let coupling = CouplingMetrics::with_visibility(
902                analyzed.module_name.clone(),
903                target_module.clone(),
904                strength,
905                distance,
906                volatility,
907                visibility,
908            );
909
910            project.add_coupling(coupling);
911        }
912    }
913
914    // Update any remaining coupling visibility information
915    project.update_coupling_visibility();
916
917    // Merge connascence stats from all analyzed files
918    for analyzed in &analyzed_results {
919        for (conn_type, count) in &analyzed.connascence.stats.by_type {
920            for _ in 0..*count {
921                project.connascence_stats.add(*conn_type);
922            }
923        }
924    }
925
926    Ok(project)
927}
928
929/// Analyze a workspace using cargo metadata for better accuracy
930pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
931    // Try to get workspace info
932    let workspace = match WorkspaceInfo::from_path(path) {
933        Ok(ws) => Some(ws),
934        Err(e) => {
935            eprintln!("Note: Could not load workspace metadata: {}", e);
936            eprintln!("Falling back to basic analysis...");
937            None
938        }
939    };
940
941    if let Some(ws) = workspace {
942        analyze_with_workspace(path, &ws)
943    } else {
944        // Fall back to basic analysis
945        analyze_project(path)
946    }
947}
948
949/// Analyze project with workspace information (parallel version)
950fn analyze_with_workspace(
951    _path: &Path,
952    workspace: &WorkspaceInfo,
953) -> Result<ProjectMetrics, AnalyzerError> {
954    let mut project = ProjectMetrics::new();
955
956    // Store workspace info for the report
957    project.workspace_name = Some(
958        workspace
959            .root
960            .file_name()
961            .and_then(|n| n.to_str())
962            .unwrap_or("workspace")
963            .to_string(),
964    );
965    project.workspace_members = workspace.members.clone();
966
967    // Collect all file paths with their crate names (sequential, fast)
968    let mut file_crate_pairs: Vec<(PathBuf, String)> = Vec::new();
969
970    for member_name in &workspace.members {
971        if let Some(crate_info) = workspace.get_crate(member_name) {
972            if !crate_info.src_path.exists() {
973                continue;
974            }
975
976            for entry in WalkDir::new(&crate_info.src_path)
977                .follow_links(true)
978                .into_iter()
979                .filter_map(|e| e.ok())
980            {
981                let file_path = entry.path();
982
983                // Skip target directory and hidden directories
984                if file_path.components().any(|c| {
985                    let s = c.as_os_str().to_string_lossy();
986                    s == "target" || s.starts_with('.')
987                }) {
988                    continue;
989                }
990
991                // Only process .rs files
992                if file_path.extension() == Some(OsStr::new("rs")) {
993                    file_crate_pairs.push((file_path.to_path_buf(), member_name.clone()));
994                }
995            }
996        }
997    }
998
999    // Calculate optimal chunk size for parallel processing
1000    let num_threads = rayon::current_num_threads();
1001    let file_count = file_crate_pairs.len();
1002    let chunk_size = if file_count < num_threads * 2 {
1003        1
1004    } else {
1005        (file_count / (num_threads * 4)).max(1)
1006    };
1007
1008    // Parallel file analysis with optimized chunking
1009    let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1010        .par_chunks(chunk_size)
1011        .flat_map(|chunk| {
1012            chunk
1013                .iter()
1014                .filter_map(
1015                    |(file_path, crate_name)| match analyze_rust_file_full(file_path) {
1016                        Ok(result) => Some(AnalyzedFileWithCrate {
1017                            module_name: result.metrics.name.clone(),
1018                            crate_name: crate_name.clone(),
1019                            file_path: file_path.clone(),
1020                            metrics: result.metrics,
1021                            dependencies: result.dependencies,
1022                            connascence: result.connascence,
1023                        }),
1024                        Err(e) => {
1025                            eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1026                            None
1027                        }
1028                    },
1029                )
1030                .collect::<Vec<_>>()
1031        })
1032        .collect();
1033
1034    project.total_files = analyzed_files.len();
1035
1036    // Second pass: build coupling relationships with workspace context
1037    for analyzed in &analyzed_files {
1038        project.add_module(analyzed.metrics.clone());
1039
1040        for dep in &analyzed.dependencies {
1041            // Skip invalid dependency paths (local variables, Self, etc.)
1042            if !is_valid_dependency_path(&dep.path) {
1043                continue;
1044            }
1045
1046            // Resolve the target crate using workspace info
1047            let resolved_crate =
1048                resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1049
1050            let target_module = extract_target_module(&dep.path);
1051
1052            // Skip if target module looks invalid
1053            if !is_valid_dependency_path(&target_module) {
1054                continue;
1055            }
1056
1057            // Calculate distance with workspace awareness
1058            let distance =
1059                calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1060
1061            // Determine strength from usage context (more accurate)
1062            let strength = dep.usage.to_strength();
1063
1064            // Default volatility
1065            let volatility = Volatility::Low;
1066
1067            // Create coupling metric with more info
1068            let mut coupling = CouplingMetrics::new(
1069                format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1070                if let Some(ref crate_name) = resolved_crate {
1071                    format!("{}::{}", crate_name, target_module)
1072                } else {
1073                    target_module.clone()
1074                },
1075                strength,
1076                distance,
1077                volatility,
1078            );
1079
1080            // Add crate-level info
1081            coupling.source_crate = Some(analyzed.crate_name.clone());
1082            coupling.target_crate = resolved_crate;
1083
1084            project.add_coupling(coupling);
1085        }
1086    }
1087
1088    // Add crate-level dependency information
1089    for (crate_name, deps) in &workspace.dependency_graph {
1090        if workspace.is_workspace_member(crate_name) {
1091            for dep in deps {
1092                // Track crate-level dependencies
1093                project
1094                    .crate_dependencies
1095                    .entry(crate_name.clone())
1096                    .or_default()
1097                    .push(dep.clone());
1098            }
1099        }
1100    }
1101
1102    // Merge connascence stats from all analyzed files
1103    for analyzed in &analyzed_files {
1104        for (conn_type, count) in &analyzed.connascence.stats.by_type {
1105            for _ in 0..*count {
1106                project.connascence_stats.add(*conn_type);
1107            }
1108        }
1109    }
1110
1111    Ok(project)
1112}
1113
1114/// Calculate distance using workspace information
1115fn calculate_distance_with_workspace(
1116    dep_path: &str,
1117    current_crate: &str,
1118    workspace: &WorkspaceInfo,
1119) -> Distance {
1120    if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1121        // Same crate
1122        Distance::SameModule
1123    } else if dep_path.starts_with("super::") {
1124        // Could be same crate or parent module
1125        Distance::DifferentModule
1126    } else {
1127        // Resolve the target crate
1128        if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1129            if target_crate == current_crate {
1130                Distance::SameModule
1131            } else if workspace.is_workspace_member(&target_crate) {
1132                // Another workspace member
1133                Distance::DifferentModule
1134            } else {
1135                // External crate
1136                Distance::DifferentCrate
1137            }
1138        } else {
1139            Distance::DifferentCrate
1140        }
1141    }
1142}
1143
1144/// Analyzed file with crate information
1145#[derive(Debug, Clone)]
1146struct AnalyzedFileWithCrate {
1147    module_name: String,
1148    crate_name: String,
1149    #[allow(dead_code)]
1150    file_path: PathBuf,
1151    metrics: ModuleMetrics,
1152    dependencies: Vec<Dependency>,
1153    connascence: ConnascenceAnalyzer,
1154}
1155
1156/// Extract target module name from a path
1157fn extract_target_module(path: &str) -> String {
1158    // Remove common prefixes and get the module name
1159    let cleaned = path
1160        .trim_start_matches("crate::")
1161        .trim_start_matches("super::")
1162        .trim_start_matches("::");
1163
1164    // Get first significant segment
1165    cleaned.split("::").next().unwrap_or(path).to_string()
1166}
1167
1168/// Check if a path looks like a valid module/type reference (not a local variable)
1169fn is_valid_dependency_path(path: &str) -> bool {
1170    // Skip empty paths
1171    if path.is_empty() {
1172        return false;
1173    }
1174
1175    // Skip Self references
1176    if path == "Self" || path.starts_with("Self::") {
1177        return false;
1178    }
1179
1180    let segments: Vec<&str> = path.split("::").collect();
1181
1182    // Skip short single-segment lowercase names (likely local variables)
1183    if segments.len() == 1 {
1184        let name = segments[0];
1185        if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1186            return false;
1187        }
1188    }
1189
1190    // Skip patterns where last two segments are the same (likely module::type patterns from variables)
1191    if segments.len() >= 2 {
1192        let last = segments.last().unwrap();
1193        let second_last = segments.get(segments.len() - 2).unwrap();
1194        if last == second_last {
1195            return false;
1196        }
1197    }
1198
1199    // Skip common patterns that look like local variable accesses
1200    let last_segment = segments.last().unwrap_or(&path);
1201    let common_locals = [
1202        "request",
1203        "response",
1204        "result",
1205        "content",
1206        "config",
1207        "proto",
1208        "domain",
1209        "info",
1210        "data",
1211        "item",
1212        "value",
1213        "error",
1214        "message",
1215        "expected",
1216        "actual",
1217        "status",
1218        "state",
1219        "context",
1220        "params",
1221        "args",
1222        "options",
1223        "settings",
1224        "violation",
1225        "page_token",
1226    ];
1227    if common_locals.contains(last_segment) && segments.len() <= 2 {
1228        return false;
1229    }
1230
1231    true
1232}
1233
1234/// Calculate distance based on dependency path
1235fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1236    if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1237        // Internal dependency
1238        Distance::DifferentModule
1239    } else if dep_path.starts_with("self::") {
1240        Distance::SameModule
1241    } else {
1242        // External crate
1243        Distance::DifferentCrate
1244    }
1245}
1246
1247/// Analyze a single Rust file
1248/// Result of analyzing a single Rust file
1249pub struct AnalyzedFileResult {
1250    pub metrics: ModuleMetrics,
1251    pub dependencies: Vec<Dependency>,
1252    pub type_visibility: HashMap<String, Visibility>,
1253    pub connascence: ConnascenceAnalyzer,
1254}
1255
1256pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1257    let result = analyze_rust_file_full(path)?;
1258    Ok((result.metrics, result.dependencies))
1259}
1260
1261/// Analyze a Rust file and return full results including visibility
1262pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1263    let content = fs::read_to_string(path)?;
1264
1265    let module_name = path
1266        .file_stem()
1267        .and_then(|s| s.to_str())
1268        .unwrap_or("unknown")
1269        .to_string();
1270
1271    let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1272    analyzer.analyze_file(&content)?;
1273
1274    Ok(AnalyzedFileResult {
1275        metrics: analyzer.metrics,
1276        dependencies: analyzer.dependencies,
1277        type_visibility: analyzer.type_visibility,
1278        connascence: analyzer.connascence,
1279    })
1280}
1281
1282#[cfg(test)]
1283mod tests {
1284    use super::*;
1285
1286    #[test]
1287    fn test_analyzer_creation() {
1288        let analyzer = CouplingAnalyzer::new(
1289            "test_module".to_string(),
1290            std::path::PathBuf::from("test.rs"),
1291        );
1292        assert_eq!(analyzer.current_module, "test_module");
1293    }
1294
1295    #[test]
1296    fn test_analyze_simple_file() {
1297        let mut analyzer =
1298            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1299
1300        let code = r#"
1301            pub struct User {
1302                name: String,
1303                email: String,
1304            }
1305
1306            impl User {
1307                pub fn new(name: String, email: String) -> Self {
1308                    Self { name, email }
1309                }
1310            }
1311        "#;
1312
1313        let result = analyzer.analyze_file(code);
1314        assert!(result.is_ok());
1315        assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1316    }
1317
1318    #[test]
1319    fn test_analyze_trait_impl() {
1320        let mut analyzer =
1321            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1322
1323        let code = r#"
1324            trait Printable {
1325                fn print(&self);
1326            }
1327
1328            struct Document;
1329
1330            impl Printable for Document {
1331                fn print(&self) {}
1332            }
1333        "#;
1334
1335        let result = analyzer.analyze_file(code);
1336        assert!(result.is_ok());
1337        assert!(analyzer.metrics.trait_impl_count >= 1);
1338    }
1339
1340    #[test]
1341    fn test_analyze_use_statements() {
1342        let mut analyzer =
1343            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1344
1345        let code = r#"
1346            use std::collections::HashMap;
1347            use serde::Serialize;
1348            use crate::utils;
1349            use crate::models::{User, Post};
1350        "#;
1351
1352        let result = analyzer.analyze_file(code);
1353        assert!(result.is_ok());
1354        assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1355        assert!(
1356            analyzer
1357                .metrics
1358                .external_deps
1359                .contains(&"serde".to_string())
1360        );
1361        assert!(!analyzer.dependencies.is_empty());
1362
1363        // Check internal dependencies
1364        let internal_deps: Vec<_> = analyzer
1365            .dependencies
1366            .iter()
1367            .filter(|d| d.kind == DependencyKind::InternalUse)
1368            .collect();
1369        assert!(!internal_deps.is_empty());
1370    }
1371
1372    #[test]
1373    fn test_extract_use_paths() {
1374        let analyzer =
1375            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1376
1377        // Test simple path
1378        let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1379        let paths = analyzer.extract_use_paths(&tree, "");
1380        assert_eq!(paths.len(), 1);
1381        assert_eq!(paths[0].0, "std::collections::HashMap");
1382
1383        // Test grouped path
1384        let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1385        let paths = analyzer.extract_use_paths(&tree, "");
1386        assert_eq!(paths.len(), 2);
1387    }
1388
1389    #[test]
1390    fn test_extract_target_module() {
1391        assert_eq!(extract_target_module("crate::models::user"), "models");
1392        assert_eq!(extract_target_module("super::utils"), "utils");
1393        assert_eq!(extract_target_module("std::collections"), "std");
1394    }
1395
1396    #[test]
1397    fn test_field_access_detection() {
1398        let mut analyzer =
1399            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1400
1401        let code = r#"
1402            use crate::models::User;
1403
1404            fn get_name(user: &User) -> String {
1405                user.name.clone()
1406            }
1407        "#;
1408
1409        let result = analyzer.analyze_file(code);
1410        assert!(result.is_ok());
1411
1412        // Should detect User as a dependency with field access
1413        let _field_deps: Vec<_> = analyzer
1414            .dependencies
1415            .iter()
1416            .filter(|d| d.usage == UsageContext::FieldAccess)
1417            .collect();
1418        // Note: This may not detect field access on function parameters
1419        // as the type info isn't fully available without type inference
1420    }
1421
1422    #[test]
1423    fn test_method_call_detection() {
1424        let mut analyzer =
1425            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1426
1427        let code = r#"
1428            fn process() {
1429                let data = String::new();
1430                data.push_str("hello");
1431            }
1432        "#;
1433
1434        let result = analyzer.analyze_file(code);
1435        assert!(result.is_ok());
1436        // Method calls on local variables are detected
1437    }
1438
1439    #[test]
1440    fn test_struct_construction_detection() {
1441        let mut analyzer =
1442            CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1443
1444        let code = r#"
1445            use crate::config::Config;
1446
1447            fn create_config() {
1448                let c = Config { value: 42 };
1449            }
1450        "#;
1451
1452        let result = analyzer.analyze_file(code);
1453        assert!(result.is_ok());
1454
1455        // Should detect Config struct construction
1456        let struct_deps: Vec<_> = analyzer
1457            .dependencies
1458            .iter()
1459            .filter(|d| d.usage == UsageContext::StructConstruction)
1460            .collect();
1461        assert!(!struct_deps.is_empty());
1462    }
1463
1464    #[test]
1465    fn test_usage_context_to_strength() {
1466        assert_eq!(
1467            UsageContext::FieldAccess.to_strength(),
1468            IntegrationStrength::Intrusive
1469        );
1470        assert_eq!(
1471            UsageContext::MethodCall.to_strength(),
1472            IntegrationStrength::Functional
1473        );
1474        assert_eq!(
1475            UsageContext::TypeParameter.to_strength(),
1476            IntegrationStrength::Model
1477        );
1478        assert_eq!(
1479            UsageContext::TraitBound.to_strength(),
1480            IntegrationStrength::Contract
1481        );
1482    }
1483}