1use 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
27fn convert_visibility(vis: &syn::Visibility) -> Visibility {
29 match vis {
30 syn::Visibility::Public(_) => Visibility::Public,
31 syn::Visibility::Restricted(restricted) => {
32 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, _ => Visibility::PubIn, }
47 }
48 syn::Visibility::Inherited => Visibility::Private,
49 }
50}
51
52#[derive(Error, Debug)]
54pub enum AnalyzerError {
55 #[error("Failed to read file: {0}")]
56 IoError(#[from] std::io::Error),
57
58 #[error("Failed to parse Rust file: {0}")]
59 ParseError(String),
60
61 #[error("Invalid path: {0}")]
62 InvalidPath(String),
63
64 #[error("Workspace error: {0}")]
65 WorkspaceError(#[from] WorkspaceError),
66}
67
68#[derive(Debug, Clone)]
70pub struct Dependency {
71 pub path: String,
73 pub kind: DependencyKind,
75 pub line: usize,
77 pub usage: UsageContext,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum DependencyKind {
84 InternalUse,
86 ExternalUse,
88 TraitImpl,
90 InherentImpl,
92 TypeRef,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub enum UsageContext {
99 Import,
101 TraitBound,
103 FieldAccess,
105 MethodCall,
107 FunctionCall,
109 StructConstruction,
111 TypeParameter,
113 FunctionParameter,
115 ReturnType,
117 InherentImplBlock,
119}
120
121impl UsageContext {
122 pub fn to_strength(&self) -> IntegrationStrength {
124 match self {
125 UsageContext::FieldAccess => IntegrationStrength::Intrusive,
127 UsageContext::StructConstruction => IntegrationStrength::Intrusive,
128 UsageContext::InherentImplBlock => IntegrationStrength::Intrusive,
129
130 UsageContext::MethodCall => IntegrationStrength::Functional,
132 UsageContext::FunctionCall => IntegrationStrength::Functional,
133 UsageContext::FunctionParameter => IntegrationStrength::Functional,
134 UsageContext::ReturnType => IntegrationStrength::Functional,
135
136 UsageContext::TypeParameter => IntegrationStrength::Model,
138 UsageContext::Import => IntegrationStrength::Model,
139
140 UsageContext::TraitBound => IntegrationStrength::Contract,
142 }
143 }
144}
145
146impl DependencyKind {
147 pub fn to_strength(&self) -> IntegrationStrength {
148 match self {
149 DependencyKind::TraitImpl => IntegrationStrength::Contract,
150 DependencyKind::InternalUse => IntegrationStrength::Model,
151 DependencyKind::ExternalUse => IntegrationStrength::Model,
152 DependencyKind::TypeRef => IntegrationStrength::Model,
153 DependencyKind::InherentImpl => IntegrationStrength::Intrusive,
154 }
155 }
156}
157
158#[derive(Debug)]
160pub struct CouplingAnalyzer {
161 pub current_module: String,
163 pub file_path: std::path::PathBuf,
165 pub metrics: ModuleMetrics,
167 pub dependencies: Vec<Dependency>,
169 pub defined_types: HashSet<String>,
171 pub defined_traits: HashSet<String>,
173 pub defined_functions: HashMap<String, Visibility>,
175 imported_types: HashMap<String, String>,
177 seen_dependencies: HashSet<(String, UsageContext)>,
179 pub usage_counts: UsageCounts,
181 pub type_visibility: HashMap<String, Visibility>,
183 current_item: Option<(String, ItemKind)>,
185 pub item_dependencies: Vec<ItemDependency>,
187}
188
189#[derive(Debug, Default, Clone)]
191pub struct UsageCounts {
192 pub field_accesses: usize,
193 pub method_calls: usize,
194 pub function_calls: usize,
195 pub struct_constructions: usize,
196 pub trait_bounds: usize,
197 pub type_parameters: usize,
198}
199
200#[derive(Debug, Clone)]
202pub struct ItemDependency {
203 pub source_item: String,
205 pub source_kind: ItemKind,
207 pub target: String,
209 pub target_module: Option<String>,
211 pub dep_type: ItemDepType,
213 pub line: usize,
215 pub expression: Option<String>,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum ItemKind {
222 Function,
223 Method,
224 Struct,
225 Enum,
226 Trait,
227 Impl,
228 Module,
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum ItemDepType {
234 FunctionCall,
236 MethodCall,
238 TypeUsage,
240 FieldAccess,
242 StructConstruction,
244 TraitImpl,
246 TraitBound,
248 Import,
250}
251
252impl CouplingAnalyzer {
253 pub fn new(module_name: String, path: std::path::PathBuf) -> Self {
255 Self {
256 current_module: module_name.clone(),
257 file_path: path.clone(),
258 metrics: ModuleMetrics::new(path, module_name),
259 dependencies: Vec::new(),
260 defined_types: HashSet::new(),
261 defined_traits: HashSet::new(),
262 defined_functions: HashMap::new(),
263 imported_types: HashMap::new(),
264 seen_dependencies: HashSet::new(),
265 usage_counts: UsageCounts::default(),
266 type_visibility: HashMap::new(),
267 current_item: None,
268 item_dependencies: Vec::new(),
269 }
270 }
271
272 pub fn analyze_file(&mut self, content: &str) -> Result<(), AnalyzerError> {
274 let syntax: File =
275 syn::parse_file(content).map_err(|e| AnalyzerError::ParseError(e.to_string()))?;
276
277 self.visit_file(&syntax);
278
279 Ok(())
280 }
281
282 fn add_dependency(&mut self, path: String, kind: DependencyKind, usage: UsageContext) {
284 let key = (path.clone(), usage);
285 if self.seen_dependencies.contains(&key) {
286 return;
287 }
288 self.seen_dependencies.insert(key);
289
290 self.dependencies.push(Dependency {
291 path,
292 kind,
293 line: 0,
294 usage,
295 });
296 }
297
298 fn add_item_dependency(
300 &mut self,
301 target: String,
302 dep_type: ItemDepType,
303 line: usize,
304 expression: Option<String>,
305 ) {
306 if let Some((ref source_item, source_kind)) = self.current_item {
307 let target_module = self.imported_types.get(&target).cloned().or_else(|| {
309 if self.defined_types.contains(&target)
310 || self.defined_functions.contains_key(&target)
311 {
312 Some(self.current_module.clone())
313 } else {
314 None
315 }
316 });
317
318 self.item_dependencies.push(ItemDependency {
319 source_item: source_item.clone(),
320 source_kind,
321 target,
322 target_module,
323 dep_type,
324 line,
325 expression,
326 });
327 }
328 }
329
330 fn extract_use_paths(&self, tree: &UseTree, prefix: &str) -> Vec<(String, DependencyKind)> {
332 let mut paths = Vec::new();
333
334 match tree {
335 UseTree::Path(path) => {
336 let new_prefix = if prefix.is_empty() {
337 path.ident.to_string()
338 } else {
339 format!("{}::{}", prefix, path.ident)
340 };
341 paths.extend(self.extract_use_paths(&path.tree, &new_prefix));
342 }
343 UseTree::Name(name) => {
344 let full_path = if prefix.is_empty() {
345 name.ident.to_string()
346 } else {
347 format!("{}::{}", prefix, name.ident)
348 };
349 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
350 DependencyKind::InternalUse
351 } else {
352 DependencyKind::ExternalUse
353 };
354 paths.push((full_path, kind));
355 }
356 UseTree::Rename(rename) => {
357 let full_path = if prefix.is_empty() {
358 rename.ident.to_string()
359 } else {
360 format!("{}::{}", prefix, rename.ident)
361 };
362 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
363 DependencyKind::InternalUse
364 } else {
365 DependencyKind::ExternalUse
366 };
367 paths.push((full_path, kind));
368 }
369 UseTree::Glob(_) => {
370 let full_path = format!("{}::*", prefix);
371 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
372 DependencyKind::InternalUse
373 } else {
374 DependencyKind::ExternalUse
375 };
376 paths.push((full_path, kind));
377 }
378 UseTree::Group(group) => {
379 for item in &group.items {
380 paths.extend(self.extract_use_paths(item, prefix));
381 }
382 }
383 }
384
385 paths
386 }
387
388 fn extract_type_name(&self, ty: &Type) -> Option<String> {
390 match ty {
391 Type::Path(type_path) => {
392 let segments: Vec<_> = type_path
393 .path
394 .segments
395 .iter()
396 .map(|s| s.ident.to_string())
397 .collect();
398 Some(segments.join("::"))
399 }
400 Type::Reference(ref_type) => self.extract_type_name(&ref_type.elem),
401 Type::Slice(slice_type) => self.extract_type_name(&slice_type.elem),
402 Type::Array(array_type) => self.extract_type_name(&array_type.elem),
403 Type::Ptr(ptr_type) => self.extract_type_name(&ptr_type.elem),
404 Type::Paren(paren_type) => self.extract_type_name(&paren_type.elem),
405 Type::Group(group_type) => self.extract_type_name(&group_type.elem),
406 _ => None,
407 }
408 }
409
410 fn analyze_signature(&mut self, sig: &Signature) {
412 for arg in &sig.inputs {
414 if let FnArg::Typed(pat_type) = arg
415 && let Some(type_name) = self.extract_type_name(&pat_type.ty)
416 && !self.is_primitive_type(&type_name)
417 {
418 self.add_dependency(
419 type_name,
420 DependencyKind::TypeRef,
421 UsageContext::FunctionParameter,
422 );
423 }
424 }
425
426 if let ReturnType::Type(_, ty) = &sig.output
428 && let Some(type_name) = self.extract_type_name(ty)
429 && !self.is_primitive_type(&type_name)
430 {
431 self.add_dependency(type_name, DependencyKind::TypeRef, UsageContext::ReturnType);
432 }
433 }
434
435 fn is_primitive_type(&self, type_name: &str) -> bool {
437 if matches!(
439 type_name,
440 "bool"
441 | "char"
442 | "str"
443 | "u8"
444 | "u16"
445 | "u32"
446 | "u64"
447 | "u128"
448 | "usize"
449 | "i8"
450 | "i16"
451 | "i32"
452 | "i64"
453 | "i128"
454 | "isize"
455 | "f32"
456 | "f64"
457 | "String"
458 | "Self"
459 | "()"
460 | "Option"
461 | "Result"
462 | "Vec"
463 | "Box"
464 | "Rc"
465 | "Arc"
466 | "RefCell"
467 | "Cell"
468 | "Mutex"
469 | "RwLock"
470 ) {
471 return true;
472 }
473
474 if type_name.len() <= 3 && type_name.chars().all(|c| c.is_lowercase()) {
477 return true;
478 }
479
480 if type_name.starts_with("self") || type_name == "self" {
482 return true;
483 }
484
485 false
486 }
487}
488
489impl<'ast> Visit<'ast> for CouplingAnalyzer {
490 fn visit_item_use(&mut self, node: &'ast ItemUse) {
491 let paths = self.extract_use_paths(&node.tree, "");
492
493 for (path, kind) in paths {
494 if path == "self" || path.starts_with("self::") {
496 continue;
497 }
498
499 if let Some(type_name) = path.split("::").last() {
501 self.imported_types
502 .insert(type_name.to_string(), path.clone());
503 }
504
505 self.add_dependency(path.clone(), kind, UsageContext::Import);
506
507 if kind == DependencyKind::InternalUse {
509 if !self.metrics.internal_deps.contains(&path) {
510 self.metrics.internal_deps.push(path.clone());
511 }
512 } else if kind == DependencyKind::ExternalUse {
513 let crate_name = path.split("::").next().unwrap_or(&path).to_string();
515 if !self.metrics.external_deps.contains(&crate_name) {
516 self.metrics.external_deps.push(crate_name);
517 }
518 }
519 }
520
521 syn::visit::visit_item_use(self, node);
522 }
523
524 fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
525 if let Some((_, trait_path, _)) = &node.trait_ {
526 self.metrics.trait_impl_count += 1;
528
529 let trait_name: String = trait_path
531 .segments
532 .iter()
533 .map(|s| s.ident.to_string())
534 .collect::<Vec<_>>()
535 .join("::");
536
537 self.add_dependency(
538 trait_name,
539 DependencyKind::TraitImpl,
540 UsageContext::TraitBound,
541 );
542 self.usage_counts.trait_bounds += 1;
543 } else {
544 self.metrics.inherent_impl_count += 1;
546
547 if let Some(type_name) = self.extract_type_name(&node.self_ty)
549 && !self.defined_types.contains(&type_name)
550 {
551 self.add_dependency(
552 type_name,
553 DependencyKind::InherentImpl,
554 UsageContext::InherentImplBlock,
555 );
556 }
557 }
558 syn::visit::visit_item_impl(self, node);
559 }
560
561 fn visit_item_fn(&mut self, node: &'ast ItemFn) {
562 let fn_name = node.sig.ident.to_string();
564 let visibility = convert_visibility(&node.vis);
565 self.defined_functions.insert(fn_name.clone(), visibility);
566
567 let mut param_count = 0;
569 let mut primitive_param_count = 0;
570 let mut param_types = Vec::new();
571
572 for arg in &node.sig.inputs {
573 if let FnArg::Typed(pat_type) = arg {
574 param_count += 1;
575 if let Some(type_name) = self.extract_type_name(&pat_type.ty) {
576 param_types.push(type_name.clone());
577 if self.is_primitive_type(&type_name) {
578 primitive_param_count += 1;
579 }
580 }
581 }
582 }
583
584 self.metrics.add_function_definition_full(
586 fn_name.clone(),
587 visibility,
588 param_count,
589 primitive_param_count,
590 param_types,
591 );
592
593 let previous_item = self.current_item.take();
595 self.current_item = Some((fn_name, ItemKind::Function));
596
597 self.analyze_signature(&node.sig);
599 syn::visit::visit_item_fn(self, node);
600
601 self.current_item = previous_item;
603 }
604
605 fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
606 let name = node.ident.to_string();
607 let visibility = convert_visibility(&node.vis);
608
609 self.defined_types.insert(name.clone());
610 self.type_visibility.insert(name.clone(), visibility);
611
612 let (is_newtype, inner_type) = match &node.fields {
614 syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
615 let inner = fields
616 .unnamed
617 .first()
618 .and_then(|f| self.extract_type_name(&f.ty));
619 (true, inner)
620 }
621 _ => (false, None),
622 };
623
624 let has_serde_derive = node.attrs.iter().any(|attr| {
626 if attr.path().is_ident("derive")
627 && let Ok(nested) = attr.parse_args_with(
628 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
629 )
630 {
631 return nested.iter().any(|path| {
632 let path_str = path
633 .segments
634 .iter()
635 .map(|s| s.ident.to_string())
636 .collect::<Vec<_>>()
637 .join("::");
638 path_str == "Serialize"
639 || path_str == "Deserialize"
640 || path_str == "serde::Serialize"
641 || path_str == "serde::Deserialize"
642 });
643 }
644 false
645 });
646
647 let (total_field_count, public_field_count) = match &node.fields {
649 syn::Fields::Named(fields) => {
650 let total = fields.named.len();
651 let public = fields
652 .named
653 .iter()
654 .filter(|f| matches!(f.vis, syn::Visibility::Public(_)))
655 .count();
656 (total, public)
657 }
658 syn::Fields::Unnamed(fields) => {
659 let total = fields.unnamed.len();
660 let public = fields
661 .unnamed
662 .iter()
663 .filter(|f| matches!(f.vis, syn::Visibility::Public(_)))
664 .count();
665 (total, public)
666 }
667 syn::Fields::Unit => (0, 0),
668 };
669
670 self.metrics.add_type_definition_full(
672 name,
673 visibility,
674 false, is_newtype,
676 inner_type,
677 has_serde_derive,
678 public_field_count,
679 total_field_count,
680 );
681
682 match &node.fields {
684 syn::Fields::Named(fields) => {
685 self.metrics.type_usage_count += fields.named.len();
686 for field in &fields.named {
687 if let Some(type_name) = self.extract_type_name(&field.ty)
688 && !self.is_primitive_type(&type_name)
689 {
690 self.add_dependency(
691 type_name,
692 DependencyKind::TypeRef,
693 UsageContext::TypeParameter,
694 );
695 self.usage_counts.type_parameters += 1;
696 }
697 }
698 }
699 syn::Fields::Unnamed(fields) => {
700 for field in &fields.unnamed {
701 if let Some(type_name) = self.extract_type_name(&field.ty)
702 && !self.is_primitive_type(&type_name)
703 {
704 self.add_dependency(
705 type_name,
706 DependencyKind::TypeRef,
707 UsageContext::TypeParameter,
708 );
709 }
710 }
711 }
712 syn::Fields::Unit => {}
713 }
714 syn::visit::visit_item_struct(self, node);
715 }
716
717 fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
718 let name = node.ident.to_string();
719 let visibility = convert_visibility(&node.vis);
720
721 self.defined_types.insert(name.clone());
722 self.type_visibility.insert(name.clone(), visibility);
723
724 self.metrics.add_type_definition(name, visibility, false);
726
727 for variant in &node.variants {
729 match &variant.fields {
730 syn::Fields::Named(fields) => {
731 for field in &fields.named {
732 if let Some(type_name) = self.extract_type_name(&field.ty)
733 && !self.is_primitive_type(&type_name)
734 {
735 self.add_dependency(
736 type_name,
737 DependencyKind::TypeRef,
738 UsageContext::TypeParameter,
739 );
740 }
741 }
742 }
743 syn::Fields::Unnamed(fields) => {
744 for field in &fields.unnamed {
745 if let Some(type_name) = self.extract_type_name(&field.ty)
746 && !self.is_primitive_type(&type_name)
747 {
748 self.add_dependency(
749 type_name,
750 DependencyKind::TypeRef,
751 UsageContext::TypeParameter,
752 );
753 }
754 }
755 }
756 syn::Fields::Unit => {}
757 }
758 }
759 syn::visit::visit_item_enum(self, node);
760 }
761
762 fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
763 let name = node.ident.to_string();
764 let visibility = convert_visibility(&node.vis);
765
766 self.defined_traits.insert(name.clone());
767 self.type_visibility.insert(name.clone(), visibility);
768
769 self.metrics.add_type_definition(name, visibility, true);
771
772 self.metrics.trait_impl_count += 1;
773 syn::visit::visit_item_trait(self, node);
774 }
775
776 fn visit_item_mod(&mut self, node: &'ast ItemMod) {
777 if node.content.is_some() {
778 self.metrics.internal_deps.push(node.ident.to_string());
779 }
780 syn::visit::visit_item_mod(self, node);
781 }
782
783 fn visit_expr_field(&mut self, node: &'ast ExprField) {
785 let field_name = match &node.member {
786 syn::Member::Named(ident) => ident.to_string(),
787 syn::Member::Unnamed(idx) => format!("{}", idx.index),
788 };
789
790 if let Expr::Path(path_expr) = &*node.base {
792 let base_name = path_expr
793 .path
794 .segments
795 .iter()
796 .map(|s| s.ident.to_string())
797 .collect::<Vec<_>>()
798 .join("::");
799
800 let full_path = self
802 .imported_types
803 .get(&base_name)
804 .cloned()
805 .unwrap_or(base_name.clone());
806
807 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
808 self.add_dependency(
809 full_path.clone(),
810 DependencyKind::TypeRef,
811 UsageContext::FieldAccess,
812 );
813 self.usage_counts.field_accesses += 1;
814 }
815
816 let expr = format!("{}.{}", base_name, field_name);
818 self.add_item_dependency(
819 format!("{}.{}", full_path, field_name),
820 ItemDepType::FieldAccess,
821 0,
822 Some(expr),
823 );
824 }
825 syn::visit::visit_expr_field(self, node);
826 }
827
828 fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
830 let method_name = node.method.to_string();
831
832 if let Expr::Path(path_expr) = &*node.receiver {
834 let receiver_name = path_expr
835 .path
836 .segments
837 .iter()
838 .map(|s| s.ident.to_string())
839 .collect::<Vec<_>>()
840 .join("::");
841
842 let full_path = self
843 .imported_types
844 .get(&receiver_name)
845 .cloned()
846 .unwrap_or(receiver_name.clone());
847
848 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
849 self.add_dependency(
850 full_path.clone(),
851 DependencyKind::TypeRef,
852 UsageContext::MethodCall,
853 );
854 self.usage_counts.method_calls += 1;
855 }
856
857 let expr = format!("{}.{}()", receiver_name, method_name);
859 self.add_item_dependency(
860 format!("{}::{}", full_path, method_name),
861 ItemDepType::MethodCall,
862 0, Some(expr),
864 );
865 }
866 syn::visit::visit_expr_method_call(self, node);
867 }
868
869 fn visit_expr_call(&mut self, node: &'ast ExprCall) {
871 if let Expr::Path(path_expr) = &*node.func {
872 let path_str = path_expr
873 .path
874 .segments
875 .iter()
876 .map(|s| s.ident.to_string())
877 .collect::<Vec<_>>()
878 .join("::");
879
880 if path_str.contains("::") || path_str.chars().next().is_some_and(|c| c.is_uppercase())
882 {
883 let full_path = self
884 .imported_types
885 .get(&path_str)
886 .cloned()
887 .unwrap_or(path_str.clone());
888
889 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
890 self.add_dependency(
891 full_path.clone(),
892 DependencyKind::TypeRef,
893 UsageContext::FunctionCall,
894 );
895 self.usage_counts.function_calls += 1;
896 }
897
898 self.add_item_dependency(
900 full_path,
901 ItemDepType::FunctionCall,
902 0,
903 Some(format!("{}()", path_str)),
904 );
905 } else {
906 self.add_item_dependency(
908 path_str.clone(),
909 ItemDepType::FunctionCall,
910 0,
911 Some(format!("{}()", path_str)),
912 );
913 }
914 }
915 syn::visit::visit_expr_call(self, node);
916 }
917
918 fn visit_expr_struct(&mut self, node: &'ast ExprStruct) {
920 let struct_name = node
921 .path
922 .segments
923 .iter()
924 .map(|s| s.ident.to_string())
925 .collect::<Vec<_>>()
926 .join("::");
927
928 if struct_name == "Self" || struct_name.starts_with("Self::") {
930 syn::visit::visit_expr_struct(self, node);
931 return;
932 }
933
934 let full_path = self
935 .imported_types
936 .get(&struct_name)
937 .cloned()
938 .unwrap_or(struct_name.clone());
939
940 if !self.defined_types.contains(&full_path) && !self.is_primitive_type(&struct_name) {
941 self.add_dependency(
942 full_path,
943 DependencyKind::TypeRef,
944 UsageContext::StructConstruction,
945 );
946 self.usage_counts.struct_constructions += 1;
947 }
948 syn::visit::visit_expr_struct(self, node);
949 }
950}
951
952#[derive(Debug, Clone)]
954struct AnalyzedFile {
955 module_name: String,
956 #[allow(dead_code)]
957 file_path: PathBuf,
958 metrics: ModuleMetrics,
959 dependencies: Vec<Dependency>,
960 type_visibility: HashMap<String, Visibility>,
962 item_dependencies: Vec<ItemDependency>,
964}
965
966pub fn analyze_project(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
968 analyze_project_parallel(path)
969}
970
971pub fn analyze_project_parallel(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
976 if !path.exists() {
977 return Err(AnalyzerError::InvalidPath(path.display().to_string()));
978 }
979
980 let file_paths: Vec<PathBuf> = WalkDir::new(path)
982 .follow_links(true)
983 .into_iter()
984 .filter_map(|e| e.ok())
985 .filter(|entry| {
986 let file_path = entry.path();
987 !file_path.components().any(|c| {
989 let s = c.as_os_str().to_string_lossy();
990 s == "target" || s.starts_with('.')
991 }) && file_path.extension() == Some(OsStr::new("rs"))
992 })
993 .map(|e| e.path().to_path_buf())
994 .collect();
995
996 let num_threads = rayon::current_num_threads();
1000 let file_count = file_paths.len();
1001
1002 let chunk_size = if file_count < num_threads * 2 {
1005 1 } else {
1007 (file_count / (num_threads * 4)).max(1)
1010 };
1011
1012 let analyzed_results: Vec<_> = file_paths
1014 .par_chunks(chunk_size)
1015 .flat_map(|chunk| {
1016 chunk
1017 .iter()
1018 .filter_map(|file_path| match analyze_rust_file_full(file_path) {
1019 Ok(result) => Some(AnalyzedFile {
1020 module_name: result.metrics.name.clone(),
1021 file_path: file_path.clone(),
1022 metrics: result.metrics,
1023 dependencies: result.dependencies,
1024 type_visibility: result.type_visibility,
1025 item_dependencies: result.item_dependencies,
1026 }),
1027 Err(e) => {
1028 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1029 None
1030 }
1031 })
1032 .collect::<Vec<_>>()
1033 })
1034 .collect();
1035
1036 let module_names: HashSet<String> = analyzed_results
1038 .iter()
1039 .map(|a| a.module_name.clone())
1040 .collect();
1041
1042 let mut project = ProjectMetrics::new();
1044 project.total_files = analyzed_results.len();
1045
1046 for analyzed in &analyzed_results {
1048 for (type_name, visibility) in &analyzed.type_visibility {
1049 project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
1050 }
1051 }
1052
1053 for analyzed in &analyzed_results {
1055 let mut metrics = analyzed.metrics.clone();
1057 metrics.item_dependencies = analyzed.item_dependencies.clone();
1058 project.add_module(metrics);
1059
1060 for dep in &analyzed.dependencies {
1061 if !is_valid_dependency_path(&dep.path) {
1063 continue;
1064 }
1065
1066 let target_module = extract_target_module(&dep.path);
1068
1069 if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1071 continue;
1072 }
1073
1074 let distance = calculate_distance(&dep.path, &module_names);
1076
1077 let strength = dep.usage.to_strength();
1079
1080 let volatility = Volatility::Low;
1082
1083 let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
1085 let visibility = project
1086 .get_type_visibility(target_type)
1087 .unwrap_or(Visibility::Public); let coupling = CouplingMetrics::with_location(
1091 analyzed.module_name.clone(),
1092 target_module.clone(),
1093 strength,
1094 distance,
1095 volatility,
1096 visibility,
1097 analyzed.file_path.clone(),
1098 dep.line,
1099 );
1100
1101 project.add_coupling(coupling);
1102 }
1103 }
1104
1105 project.update_coupling_visibility();
1107
1108 Ok(project)
1109}
1110
1111pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1113 let workspace = match WorkspaceInfo::from_path(path) {
1115 Ok(ws) => Some(ws),
1116 Err(e) => {
1117 eprintln!("Note: Could not load workspace metadata: {}", e);
1118 eprintln!("Falling back to basic analysis...");
1119 None
1120 }
1121 };
1122
1123 if let Some(ws) = workspace {
1124 analyze_with_workspace(path, &ws)
1125 } else {
1126 analyze_project(path)
1128 }
1129}
1130
1131fn analyze_with_workspace(
1133 _path: &Path,
1134 workspace: &WorkspaceInfo,
1135) -> Result<ProjectMetrics, AnalyzerError> {
1136 let mut project = ProjectMetrics::new();
1137
1138 project.workspace_name = Some(
1140 workspace
1141 .root
1142 .file_name()
1143 .and_then(|n| n.to_str())
1144 .unwrap_or("workspace")
1145 .to_string(),
1146 );
1147 project.workspace_members = workspace.members.clone();
1148
1149 let mut file_crate_pairs: Vec<(PathBuf, String)> = Vec::new();
1151
1152 for member_name in &workspace.members {
1153 if let Some(crate_info) = workspace.get_crate(member_name) {
1154 if !crate_info.src_path.exists() {
1155 continue;
1156 }
1157
1158 for entry in WalkDir::new(&crate_info.src_path)
1159 .follow_links(true)
1160 .into_iter()
1161 .filter_map(|e| e.ok())
1162 {
1163 let file_path = entry.path();
1164
1165 if file_path.components().any(|c| {
1167 let s = c.as_os_str().to_string_lossy();
1168 s == "target" || s.starts_with('.')
1169 }) {
1170 continue;
1171 }
1172
1173 if file_path.extension() == Some(OsStr::new("rs")) {
1175 file_crate_pairs.push((file_path.to_path_buf(), member_name.clone()));
1176 }
1177 }
1178 }
1179 }
1180
1181 let num_threads = rayon::current_num_threads();
1183 let file_count = file_crate_pairs.len();
1184 let chunk_size = if file_count < num_threads * 2 {
1185 1
1186 } else {
1187 (file_count / (num_threads * 4)).max(1)
1188 };
1189
1190 let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1192 .par_chunks(chunk_size)
1193 .flat_map(|chunk| {
1194 chunk
1195 .iter()
1196 .filter_map(
1197 |(file_path, crate_name)| match analyze_rust_file_full(file_path) {
1198 Ok(result) => Some(AnalyzedFileWithCrate {
1199 module_name: result.metrics.name.clone(),
1200 crate_name: crate_name.clone(),
1201 file_path: file_path.clone(),
1202 metrics: result.metrics,
1203 dependencies: result.dependencies,
1204 item_dependencies: result.item_dependencies,
1205 }),
1206 Err(e) => {
1207 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1208 None
1209 }
1210 },
1211 )
1212 .collect::<Vec<_>>()
1213 })
1214 .collect();
1215
1216 project.total_files = analyzed_files.len();
1217
1218 let module_names: HashSet<String> = analyzed_files
1220 .iter()
1221 .map(|a| a.module_name.clone())
1222 .collect();
1223
1224 for analyzed in &analyzed_files {
1226 let mut metrics = analyzed.metrics.clone();
1228 metrics.item_dependencies = analyzed.item_dependencies.clone();
1229 project.add_module(metrics);
1230
1231 for dep in &analyzed.dependencies {
1232 if !is_valid_dependency_path(&dep.path) {
1234 continue;
1235 }
1236
1237 let resolved_crate =
1239 resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1240
1241 let target_module = extract_target_module(&dep.path);
1242
1243 if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1245 continue;
1246 }
1247
1248 let distance =
1250 calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1251
1252 let strength = dep.usage.to_strength();
1254
1255 let volatility = Volatility::Low;
1257
1258 let mut coupling = CouplingMetrics::with_location(
1260 format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1261 if let Some(ref crate_name) = resolved_crate {
1262 format!("{}::{}", crate_name, target_module)
1263 } else {
1264 target_module.clone()
1265 },
1266 strength,
1267 distance,
1268 volatility,
1269 Visibility::Public, analyzed.file_path.clone(),
1271 dep.line,
1272 );
1273
1274 coupling.source_crate = Some(analyzed.crate_name.clone());
1276 coupling.target_crate = resolved_crate;
1277
1278 project.add_coupling(coupling);
1279 }
1280 }
1281
1282 for (crate_name, deps) in &workspace.dependency_graph {
1284 if workspace.is_workspace_member(crate_name) {
1285 for dep in deps {
1286 project
1288 .crate_dependencies
1289 .entry(crate_name.clone())
1290 .or_default()
1291 .push(dep.clone());
1292 }
1293 }
1294 }
1295
1296 Ok(project)
1297}
1298
1299fn calculate_distance_with_workspace(
1301 dep_path: &str,
1302 current_crate: &str,
1303 workspace: &WorkspaceInfo,
1304) -> Distance {
1305 if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1306 Distance::SameModule
1308 } else if dep_path.starts_with("super::") {
1309 Distance::DifferentModule
1311 } else {
1312 if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1314 if target_crate == current_crate {
1315 Distance::SameModule
1316 } else if workspace.is_workspace_member(&target_crate) {
1317 Distance::DifferentModule
1319 } else {
1320 Distance::DifferentCrate
1322 }
1323 } else {
1324 Distance::DifferentCrate
1325 }
1326 }
1327}
1328
1329#[derive(Debug, Clone)]
1331struct AnalyzedFileWithCrate {
1332 module_name: String,
1333 crate_name: String,
1334 #[allow(dead_code)]
1335 file_path: PathBuf,
1336 metrics: ModuleMetrics,
1337 dependencies: Vec<Dependency>,
1338 item_dependencies: Vec<ItemDependency>,
1340}
1341
1342fn extract_target_module(path: &str) -> String {
1344 let cleaned = path
1346 .trim_start_matches("crate::")
1347 .trim_start_matches("super::")
1348 .trim_start_matches("::");
1349
1350 cleaned.split("::").next().unwrap_or(path).to_string()
1352}
1353
1354fn is_valid_dependency_path(path: &str) -> bool {
1356 if path.is_empty() {
1358 return false;
1359 }
1360
1361 if path == "Self" || path.starts_with("Self::") {
1363 return false;
1364 }
1365
1366 let segments: Vec<&str> = path.split("::").collect();
1367
1368 if segments.len() == 1 {
1370 let name = segments[0];
1371 if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1372 return false;
1373 }
1374 }
1375
1376 if segments.len() >= 2 {
1378 let last = segments.last().unwrap();
1379 let second_last = segments.get(segments.len() - 2).unwrap();
1380 if last == second_last {
1381 return false;
1382 }
1383 }
1384
1385 let last_segment = segments.last().unwrap_or(&path);
1387 let common_locals = [
1388 "request",
1389 "response",
1390 "result",
1391 "content",
1392 "config",
1393 "proto",
1394 "domain",
1395 "info",
1396 "data",
1397 "item",
1398 "value",
1399 "error",
1400 "message",
1401 "expected",
1402 "actual",
1403 "status",
1404 "state",
1405 "context",
1406 "params",
1407 "args",
1408 "options",
1409 "settings",
1410 "violation",
1411 "page_token",
1412 ];
1413 if common_locals.contains(last_segment) && segments.len() <= 2 {
1414 return false;
1415 }
1416
1417 true
1418}
1419
1420fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1422 if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1423 Distance::DifferentModule
1425 } else if dep_path.starts_with("self::") {
1426 Distance::SameModule
1427 } else {
1428 Distance::DifferentCrate
1430 }
1431}
1432
1433pub struct AnalyzedFileResult {
1436 pub metrics: ModuleMetrics,
1437 pub dependencies: Vec<Dependency>,
1438 pub type_visibility: HashMap<String, Visibility>,
1439 pub item_dependencies: Vec<ItemDependency>,
1440}
1441
1442pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1443 let result = analyze_rust_file_full(path)?;
1444 Ok((result.metrics, result.dependencies))
1445}
1446
1447pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1449 let content = fs::read_to_string(path)?;
1450
1451 let module_name = path
1452 .file_stem()
1453 .and_then(|s| s.to_str())
1454 .unwrap_or("unknown")
1455 .to_string();
1456
1457 let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1458 analyzer.analyze_file(&content)?;
1459
1460 Ok(AnalyzedFileResult {
1461 metrics: analyzer.metrics,
1462 dependencies: analyzer.dependencies,
1463 type_visibility: analyzer.type_visibility,
1464 item_dependencies: analyzer.item_dependencies,
1465 })
1466}
1467
1468#[cfg(test)]
1469mod tests {
1470 use super::*;
1471
1472 #[test]
1473 fn test_analyzer_creation() {
1474 let analyzer = CouplingAnalyzer::new(
1475 "test_module".to_string(),
1476 std::path::PathBuf::from("test.rs"),
1477 );
1478 assert_eq!(analyzer.current_module, "test_module");
1479 }
1480
1481 #[test]
1482 fn test_analyze_simple_file() {
1483 let mut analyzer =
1484 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1485
1486 let code = r#"
1487 pub struct User {
1488 name: String,
1489 email: String,
1490 }
1491
1492 impl User {
1493 pub fn new(name: String, email: String) -> Self {
1494 Self { name, email }
1495 }
1496 }
1497 "#;
1498
1499 let result = analyzer.analyze_file(code);
1500 assert!(result.is_ok());
1501 assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1502 }
1503
1504 #[test]
1505 fn test_item_dependencies() {
1506 let mut analyzer =
1507 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1508
1509 let code = r#"
1510 pub struct Config {
1511 pub value: i32,
1512 }
1513
1514 pub fn process(config: Config) -> i32 {
1515 let x = config.value;
1516 helper(x)
1517 }
1518
1519 fn helper(n: i32) -> i32 {
1520 n * 2
1521 }
1522 "#;
1523
1524 let result = analyzer.analyze_file(code);
1525 assert!(result.is_ok());
1526
1527 assert!(analyzer.defined_functions.contains_key("process"));
1529 assert!(analyzer.defined_functions.contains_key("helper"));
1530
1531 println!(
1533 "Item dependencies count: {}",
1534 analyzer.item_dependencies.len()
1535 );
1536 for dep in &analyzer.item_dependencies {
1537 println!(
1538 " {} -> {} ({:?})",
1539 dep.source_item, dep.target, dep.dep_type
1540 );
1541 }
1542
1543 let process_deps: Vec<_> = analyzer
1545 .item_dependencies
1546 .iter()
1547 .filter(|d| d.source_item == "process")
1548 .collect();
1549
1550 assert!(
1551 !process_deps.is_empty(),
1552 "process function should have item dependencies"
1553 );
1554 }
1555
1556 #[test]
1557 fn test_analyze_trait_impl() {
1558 let mut analyzer =
1559 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1560
1561 let code = r#"
1562 trait Printable {
1563 fn print(&self);
1564 }
1565
1566 struct Document;
1567
1568 impl Printable for Document {
1569 fn print(&self) {}
1570 }
1571 "#;
1572
1573 let result = analyzer.analyze_file(code);
1574 assert!(result.is_ok());
1575 assert!(analyzer.metrics.trait_impl_count >= 1);
1576 }
1577
1578 #[test]
1579 fn test_analyze_use_statements() {
1580 let mut analyzer =
1581 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1582
1583 let code = r#"
1584 use std::collections::HashMap;
1585 use serde::Serialize;
1586 use crate::utils;
1587 use crate::models::{User, Post};
1588 "#;
1589
1590 let result = analyzer.analyze_file(code);
1591 assert!(result.is_ok());
1592 assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1593 assert!(
1594 analyzer
1595 .metrics
1596 .external_deps
1597 .contains(&"serde".to_string())
1598 );
1599 assert!(!analyzer.dependencies.is_empty());
1600
1601 let internal_deps: Vec<_> = analyzer
1603 .dependencies
1604 .iter()
1605 .filter(|d| d.kind == DependencyKind::InternalUse)
1606 .collect();
1607 assert!(!internal_deps.is_empty());
1608 }
1609
1610 #[test]
1611 fn test_extract_use_paths() {
1612 let analyzer =
1613 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1614
1615 let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1617 let paths = analyzer.extract_use_paths(&tree, "");
1618 assert_eq!(paths.len(), 1);
1619 assert_eq!(paths[0].0, "std::collections::HashMap");
1620
1621 let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1623 let paths = analyzer.extract_use_paths(&tree, "");
1624 assert_eq!(paths.len(), 2);
1625 }
1626
1627 #[test]
1628 fn test_extract_target_module() {
1629 assert_eq!(extract_target_module("crate::models::user"), "models");
1630 assert_eq!(extract_target_module("super::utils"), "utils");
1631 assert_eq!(extract_target_module("std::collections"), "std");
1632 }
1633
1634 #[test]
1635 fn test_field_access_detection() {
1636 let mut analyzer =
1637 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1638
1639 let code = r#"
1640 use crate::models::User;
1641
1642 fn get_name(user: &User) -> String {
1643 user.name.clone()
1644 }
1645 "#;
1646
1647 let result = analyzer.analyze_file(code);
1648 assert!(result.is_ok());
1649
1650 let _field_deps: Vec<_> = analyzer
1652 .dependencies
1653 .iter()
1654 .filter(|d| d.usage == UsageContext::FieldAccess)
1655 .collect();
1656 }
1659
1660 #[test]
1661 fn test_method_call_detection() {
1662 let mut analyzer =
1663 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1664
1665 let code = r#"
1666 fn process() {
1667 let data = String::new();
1668 data.push_str("hello");
1669 }
1670 "#;
1671
1672 let result = analyzer.analyze_file(code);
1673 assert!(result.is_ok());
1674 }
1676
1677 #[test]
1678 fn test_struct_construction_detection() {
1679 let mut analyzer =
1680 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1681
1682 let code = r#"
1683 use crate::config::Config;
1684
1685 fn create_config() {
1686 let c = Config { value: 42 };
1687 }
1688 "#;
1689
1690 let result = analyzer.analyze_file(code);
1691 assert!(result.is_ok());
1692
1693 let struct_deps: Vec<_> = analyzer
1695 .dependencies
1696 .iter()
1697 .filter(|d| d.usage == UsageContext::StructConstruction)
1698 .collect();
1699 assert!(!struct_deps.is_empty());
1700 }
1701
1702 #[test]
1703 fn test_usage_context_to_strength() {
1704 assert_eq!(
1705 UsageContext::FieldAccess.to_strength(),
1706 IntegrationStrength::Intrusive
1707 );
1708 assert_eq!(
1709 UsageContext::MethodCall.to_strength(),
1710 IntegrationStrength::Functional
1711 );
1712 assert_eq!(
1713 UsageContext::TypeParameter.to_strength(),
1714 IntegrationStrength::Model
1715 );
1716 assert_eq!(
1717 UsageContext::TraitBound.to_strength(),
1718 IntegrationStrength::Contract
1719 );
1720 }
1721}