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
971fn rs_files(dir: &Path) -> impl Iterator<Item = PathBuf> {
977 WalkDir::new(dir)
978 .follow_links(true)
979 .into_iter()
980 .filter_map(|e| e.ok())
981 .filter(move |entry| {
982 let file_path = entry.path();
983 let file_path = file_path.strip_prefix(dir).unwrap_or(file_path);
988
989 !file_path.components().any(|c| {
991 let s = c.as_os_str().to_string_lossy();
992 s == "target" || s.starts_with('.')
993 }) && file_path.extension() == Some(OsStr::new("rs"))
994 })
995 .map(|e| e.path().to_path_buf())
996}
997
998pub fn analyze_project_parallel(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1003 if !path.exists() {
1004 return Err(AnalyzerError::InvalidPath(path.display().to_string()));
1005 }
1006
1007 let file_paths: Vec<PathBuf> = rs_files(path).collect();
1009
1010 let num_threads = rayon::current_num_threads();
1014 let file_count = file_paths.len();
1015
1016 let chunk_size = if file_count < num_threads * 2 {
1019 1 } else {
1021 (file_count / (num_threads * 4)).max(1)
1024 };
1025
1026 let analyzed_results: Vec<_> = file_paths
1028 .par_chunks(chunk_size)
1029 .flat_map(|chunk| {
1030 chunk
1031 .iter()
1032 .filter_map(|file_path| match analyze_rust_file_full(file_path) {
1033 Ok(result) => Some(AnalyzedFile {
1034 module_name: result.metrics.name.clone(),
1035 file_path: file_path.clone(),
1036 metrics: result.metrics,
1037 dependencies: result.dependencies,
1038 type_visibility: result.type_visibility,
1039 item_dependencies: result.item_dependencies,
1040 }),
1041 Err(e) => {
1042 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1043 None
1044 }
1045 })
1046 .collect::<Vec<_>>()
1047 })
1048 .collect();
1049
1050 let module_names: HashSet<String> = analyzed_results
1052 .iter()
1053 .map(|a| a.module_name.clone())
1054 .collect();
1055
1056 let mut project = ProjectMetrics::new();
1058 project.total_files = analyzed_results.len();
1059
1060 for analyzed in &analyzed_results {
1062 for (type_name, visibility) in &analyzed.type_visibility {
1063 project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
1064 }
1065 }
1066
1067 for analyzed in &analyzed_results {
1069 let mut metrics = analyzed.metrics.clone();
1071 metrics.item_dependencies = analyzed.item_dependencies.clone();
1072 project.add_module(metrics);
1073
1074 for dep in &analyzed.dependencies {
1075 if !is_valid_dependency_path(&dep.path) {
1077 continue;
1078 }
1079
1080 let target_module = extract_target_module(&dep.path);
1082
1083 if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1085 continue;
1086 }
1087
1088 let distance = calculate_distance(&dep.path, &module_names);
1090
1091 let strength = dep.usage.to_strength();
1093
1094 let volatility = Volatility::Low;
1096
1097 let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
1099 let visibility = project
1100 .get_type_visibility(target_type)
1101 .unwrap_or(Visibility::Public); let coupling = CouplingMetrics::with_location(
1105 analyzed.module_name.clone(),
1106 target_module.clone(),
1107 strength,
1108 distance,
1109 volatility,
1110 visibility,
1111 analyzed.file_path.clone(),
1112 dep.line,
1113 );
1114
1115 project.add_coupling(coupling);
1116 }
1117 }
1118
1119 project.update_coupling_visibility();
1121
1122 Ok(project)
1123}
1124
1125pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1127 let workspace = match WorkspaceInfo::from_path(path) {
1129 Ok(ws) => Some(ws),
1130 Err(e) => {
1131 eprintln!("Note: Could not load workspace metadata: {}", e);
1132 eprintln!("Falling back to basic analysis...");
1133 None
1134 }
1135 };
1136
1137 if let Some(ws) = workspace {
1138 analyze_with_workspace(path, &ws)
1139 } else {
1140 analyze_project(path)
1142 }
1143}
1144
1145fn analyze_with_workspace(
1147 _path: &Path,
1148 workspace: &WorkspaceInfo,
1149) -> Result<ProjectMetrics, AnalyzerError> {
1150 let mut project = ProjectMetrics::new();
1151
1152 project.workspace_name = Some(
1154 workspace
1155 .root
1156 .file_name()
1157 .and_then(|n| n.to_str())
1158 .unwrap_or("workspace")
1159 .to_string(),
1160 );
1161 project.workspace_members = workspace.members.clone();
1162
1163 let mut file_crate_pairs: Vec<(PathBuf, String)> = Vec::new();
1165
1166 for member_name in &workspace.members {
1167 if let Some(crate_info) = workspace.get_crate(member_name) {
1168 if !crate_info.src_path.exists() {
1169 continue;
1170 }
1171
1172 for file_path in rs_files(&crate_info.src_path) {
1173 file_crate_pairs.push((file_path.to_path_buf(), member_name.clone()));
1174 }
1175 }
1176 }
1177
1178 let num_threads = rayon::current_num_threads();
1180 let file_count = file_crate_pairs.len();
1181 let chunk_size = if file_count < num_threads * 2 {
1182 1
1183 } else {
1184 (file_count / (num_threads * 4)).max(1)
1185 };
1186
1187 let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1189 .par_chunks(chunk_size)
1190 .flat_map(|chunk| {
1191 chunk
1192 .iter()
1193 .filter_map(
1194 |(file_path, crate_name)| match analyze_rust_file_full(file_path) {
1195 Ok(result) => Some(AnalyzedFileWithCrate {
1196 module_name: result.metrics.name.clone(),
1197 crate_name: crate_name.clone(),
1198 file_path: file_path.clone(),
1199 metrics: result.metrics,
1200 dependencies: result.dependencies,
1201 item_dependencies: result.item_dependencies,
1202 }),
1203 Err(e) => {
1204 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1205 None
1206 }
1207 },
1208 )
1209 .collect::<Vec<_>>()
1210 })
1211 .collect();
1212
1213 project.total_files = analyzed_files.len();
1214
1215 let module_names: HashSet<String> = analyzed_files
1217 .iter()
1218 .map(|a| a.module_name.clone())
1219 .collect();
1220
1221 for analyzed in &analyzed_files {
1223 let mut metrics = analyzed.metrics.clone();
1225 metrics.item_dependencies = analyzed.item_dependencies.clone();
1226 project.add_module(metrics);
1227
1228 for dep in &analyzed.dependencies {
1229 if !is_valid_dependency_path(&dep.path) {
1231 continue;
1232 }
1233
1234 let resolved_crate =
1236 resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1237
1238 let target_module = extract_target_module(&dep.path);
1239
1240 if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1242 continue;
1243 }
1244
1245 let distance =
1247 calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1248
1249 let strength = dep.usage.to_strength();
1251
1252 let volatility = Volatility::Low;
1254
1255 let mut coupling = CouplingMetrics::with_location(
1257 format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1258 if let Some(ref crate_name) = resolved_crate {
1259 format!("{}::{}", crate_name, target_module)
1260 } else {
1261 target_module.clone()
1262 },
1263 strength,
1264 distance,
1265 volatility,
1266 Visibility::Public, analyzed.file_path.clone(),
1268 dep.line,
1269 );
1270
1271 coupling.source_crate = Some(analyzed.crate_name.clone());
1273 coupling.target_crate = resolved_crate;
1274
1275 project.add_coupling(coupling);
1276 }
1277 }
1278
1279 for (crate_name, deps) in &workspace.dependency_graph {
1281 if workspace.is_workspace_member(crate_name) {
1282 for dep in deps {
1283 project
1285 .crate_dependencies
1286 .entry(crate_name.clone())
1287 .or_default()
1288 .push(dep.clone());
1289 }
1290 }
1291 }
1292
1293 Ok(project)
1294}
1295
1296fn calculate_distance_with_workspace(
1298 dep_path: &str,
1299 current_crate: &str,
1300 workspace: &WorkspaceInfo,
1301) -> Distance {
1302 if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1303 Distance::SameModule
1305 } else if dep_path.starts_with("super::") {
1306 Distance::DifferentModule
1308 } else {
1309 if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1311 if target_crate == current_crate {
1312 Distance::SameModule
1313 } else if workspace.is_workspace_member(&target_crate) {
1314 Distance::DifferentModule
1316 } else {
1317 Distance::DifferentCrate
1319 }
1320 } else {
1321 Distance::DifferentCrate
1322 }
1323 }
1324}
1325
1326#[derive(Debug, Clone)]
1328struct AnalyzedFileWithCrate {
1329 module_name: String,
1330 crate_name: String,
1331 #[allow(dead_code)]
1332 file_path: PathBuf,
1333 metrics: ModuleMetrics,
1334 dependencies: Vec<Dependency>,
1335 item_dependencies: Vec<ItemDependency>,
1337}
1338
1339fn extract_target_module(path: &str) -> String {
1341 let cleaned = path
1343 .trim_start_matches("crate::")
1344 .trim_start_matches("super::")
1345 .trim_start_matches("::");
1346
1347 cleaned.split("::").next().unwrap_or(path).to_string()
1349}
1350
1351fn is_valid_dependency_path(path: &str) -> bool {
1353 if path.is_empty() {
1355 return false;
1356 }
1357
1358 if path == "Self" || path.starts_with("Self::") {
1360 return false;
1361 }
1362
1363 let segments: Vec<&str> = path.split("::").collect();
1364
1365 if segments.len() == 1 {
1367 let name = segments[0];
1368 if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1369 return false;
1370 }
1371 }
1372
1373 if segments.len() >= 2 {
1375 let last = segments.last().unwrap();
1376 let second_last = segments.get(segments.len() - 2).unwrap();
1377 if last == second_last {
1378 return false;
1379 }
1380 }
1381
1382 let last_segment = segments.last().unwrap_or(&path);
1384 let common_locals = [
1385 "request",
1386 "response",
1387 "result",
1388 "content",
1389 "config",
1390 "proto",
1391 "domain",
1392 "info",
1393 "data",
1394 "item",
1395 "value",
1396 "error",
1397 "message",
1398 "expected",
1399 "actual",
1400 "status",
1401 "state",
1402 "context",
1403 "params",
1404 "args",
1405 "options",
1406 "settings",
1407 "violation",
1408 "page_token",
1409 ];
1410 if common_locals.contains(last_segment) && segments.len() <= 2 {
1411 return false;
1412 }
1413
1414 true
1415}
1416
1417fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1419 if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1420 Distance::DifferentModule
1422 } else if dep_path.starts_with("self::") {
1423 Distance::SameModule
1424 } else {
1425 Distance::DifferentCrate
1427 }
1428}
1429
1430pub struct AnalyzedFileResult {
1433 pub metrics: ModuleMetrics,
1434 pub dependencies: Vec<Dependency>,
1435 pub type_visibility: HashMap<String, Visibility>,
1436 pub item_dependencies: Vec<ItemDependency>,
1437}
1438
1439pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1440 let result = analyze_rust_file_full(path)?;
1441 Ok((result.metrics, result.dependencies))
1442}
1443
1444pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1446 let content = fs::read_to_string(path)?;
1447
1448 let module_name = path
1449 .file_stem()
1450 .and_then(|s| s.to_str())
1451 .unwrap_or("unknown")
1452 .to_string();
1453
1454 let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1455 analyzer.analyze_file(&content)?;
1456
1457 Ok(AnalyzedFileResult {
1458 metrics: analyzer.metrics,
1459 dependencies: analyzer.dependencies,
1460 type_visibility: analyzer.type_visibility,
1461 item_dependencies: analyzer.item_dependencies,
1462 })
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use super::*;
1468
1469 #[test]
1470 fn test_analyzer_creation() {
1471 let analyzer = CouplingAnalyzer::new(
1472 "test_module".to_string(),
1473 std::path::PathBuf::from("test.rs"),
1474 );
1475 assert_eq!(analyzer.current_module, "test_module");
1476 }
1477
1478 #[test]
1479 fn test_analyze_simple_file() {
1480 let mut analyzer =
1481 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1482
1483 let code = r#"
1484 pub struct User {
1485 name: String,
1486 email: String,
1487 }
1488
1489 impl User {
1490 pub fn new(name: String, email: String) -> Self {
1491 Self { name, email }
1492 }
1493 }
1494 "#;
1495
1496 let result = analyzer.analyze_file(code);
1497 assert!(result.is_ok());
1498 assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1499 }
1500
1501 #[test]
1502 fn test_item_dependencies() {
1503 let mut analyzer =
1504 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1505
1506 let code = r#"
1507 pub struct Config {
1508 pub value: i32,
1509 }
1510
1511 pub fn process(config: Config) -> i32 {
1512 let x = config.value;
1513 helper(x)
1514 }
1515
1516 fn helper(n: i32) -> i32 {
1517 n * 2
1518 }
1519 "#;
1520
1521 let result = analyzer.analyze_file(code);
1522 assert!(result.is_ok());
1523
1524 assert!(analyzer.defined_functions.contains_key("process"));
1526 assert!(analyzer.defined_functions.contains_key("helper"));
1527
1528 println!(
1530 "Item dependencies count: {}",
1531 analyzer.item_dependencies.len()
1532 );
1533 for dep in &analyzer.item_dependencies {
1534 println!(
1535 " {} -> {} ({:?})",
1536 dep.source_item, dep.target, dep.dep_type
1537 );
1538 }
1539
1540 let process_deps: Vec<_> = analyzer
1542 .item_dependencies
1543 .iter()
1544 .filter(|d| d.source_item == "process")
1545 .collect();
1546
1547 assert!(
1548 !process_deps.is_empty(),
1549 "process function should have item dependencies"
1550 );
1551 }
1552
1553 #[test]
1554 fn test_analyze_trait_impl() {
1555 let mut analyzer =
1556 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1557
1558 let code = r#"
1559 trait Printable {
1560 fn print(&self);
1561 }
1562
1563 struct Document;
1564
1565 impl Printable for Document {
1566 fn print(&self) {}
1567 }
1568 "#;
1569
1570 let result = analyzer.analyze_file(code);
1571 assert!(result.is_ok());
1572 assert!(analyzer.metrics.trait_impl_count >= 1);
1573 }
1574
1575 #[test]
1576 fn test_analyze_use_statements() {
1577 let mut analyzer =
1578 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1579
1580 let code = r#"
1581 use std::collections::HashMap;
1582 use serde::Serialize;
1583 use crate::utils;
1584 use crate::models::{User, Post};
1585 "#;
1586
1587 let result = analyzer.analyze_file(code);
1588 assert!(result.is_ok());
1589 assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1590 assert!(
1591 analyzer
1592 .metrics
1593 .external_deps
1594 .contains(&"serde".to_string())
1595 );
1596 assert!(!analyzer.dependencies.is_empty());
1597
1598 let internal_deps: Vec<_> = analyzer
1600 .dependencies
1601 .iter()
1602 .filter(|d| d.kind == DependencyKind::InternalUse)
1603 .collect();
1604 assert!(!internal_deps.is_empty());
1605 }
1606
1607 #[test]
1608 fn test_extract_use_paths() {
1609 let analyzer =
1610 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1611
1612 let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1614 let paths = analyzer.extract_use_paths(&tree, "");
1615 assert_eq!(paths.len(), 1);
1616 assert_eq!(paths[0].0, "std::collections::HashMap");
1617
1618 let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1620 let paths = analyzer.extract_use_paths(&tree, "");
1621 assert_eq!(paths.len(), 2);
1622 }
1623
1624 #[test]
1625 fn test_extract_target_module() {
1626 assert_eq!(extract_target_module("crate::models::user"), "models");
1627 assert_eq!(extract_target_module("super::utils"), "utils");
1628 assert_eq!(extract_target_module("std::collections"), "std");
1629 }
1630
1631 #[test]
1632 fn test_field_access_detection() {
1633 let mut analyzer =
1634 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1635
1636 let code = r#"
1637 use crate::models::User;
1638
1639 fn get_name(user: &User) -> String {
1640 user.name.clone()
1641 }
1642 "#;
1643
1644 let result = analyzer.analyze_file(code);
1645 assert!(result.is_ok());
1646
1647 let _field_deps: Vec<_> = analyzer
1649 .dependencies
1650 .iter()
1651 .filter(|d| d.usage == UsageContext::FieldAccess)
1652 .collect();
1653 }
1656
1657 #[test]
1658 fn test_method_call_detection() {
1659 let mut analyzer =
1660 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1661
1662 let code = r#"
1663 fn process() {
1664 let data = String::new();
1665 data.push_str("hello");
1666 }
1667 "#;
1668
1669 let result = analyzer.analyze_file(code);
1670 assert!(result.is_ok());
1671 }
1673
1674 #[test]
1675 fn test_struct_construction_detection() {
1676 let mut analyzer =
1677 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1678
1679 let code = r#"
1680 use crate::config::Config;
1681
1682 fn create_config() {
1683 let c = Config { value: 42 };
1684 }
1685 "#;
1686
1687 let result = analyzer.analyze_file(code);
1688 assert!(result.is_ok());
1689
1690 let struct_deps: Vec<_> = analyzer
1692 .dependencies
1693 .iter()
1694 .filter(|d| d.usage == UsageContext::StructConstruction)
1695 .collect();
1696 assert!(!struct_deps.is_empty());
1697 }
1698
1699 #[test]
1700 fn test_usage_context_to_strength() {
1701 assert_eq!(
1702 UsageContext::FieldAccess.to_strength(),
1703 IntegrationStrength::Intrusive
1704 );
1705 assert_eq!(
1706 UsageContext::MethodCall.to_strength(),
1707 IntegrationStrength::Functional
1708 );
1709 assert_eq!(
1710 UsageContext::TypeParameter.to_strength(),
1711 IntegrationStrength::Model
1712 );
1713 assert_eq!(
1714 UsageContext::TraitBound.to_strength(),
1715 IntegrationStrength::Contract
1716 );
1717 }
1718
1719 #[test]
1722 fn test_rs_files_with_hidden_parent_directory() {
1723 use std::fs;
1724 use tempfile::TempDir;
1725
1726 let temp = TempDir::new().unwrap();
1729 let hidden_parent = temp.path().join(".hidden-parent");
1730 let project_dir = hidden_parent.join("myproject").join("src");
1731 fs::create_dir_all(&project_dir).unwrap();
1732
1733 fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1735 fs::write(project_dir.join("main.rs"), "fn main() {}").unwrap();
1736
1737 let files: Vec<_> = rs_files(&project_dir).collect();
1739 assert_eq!(
1740 files.len(),
1741 2,
1742 "Should find 2 .rs files in hidden parent path"
1743 );
1744
1745 let file_names: Vec<_> = files
1747 .iter()
1748 .filter_map(|p| p.file_name())
1749 .filter_map(|n| n.to_str())
1750 .collect();
1751 assert!(file_names.contains(&"lib.rs"));
1752 assert!(file_names.contains(&"main.rs"));
1753 }
1754
1755 #[test]
1757 fn test_rs_files_excludes_hidden_dirs_in_project() {
1758 use std::fs;
1759 use tempfile::TempDir;
1760
1761 let temp = TempDir::new().unwrap();
1762 let project_dir = temp.path().join("myproject").join("src");
1763 let hidden_dir = project_dir.join(".hidden");
1764 fs::create_dir_all(&hidden_dir).unwrap();
1765
1766 fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1768 fs::write(hidden_dir.join("secret.rs"), "fn secret() {}").unwrap();
1769
1770 let files: Vec<_> = rs_files(&project_dir).collect();
1772 assert_eq!(
1773 files.len(),
1774 1,
1775 "Should find only 1 .rs file (excluding .hidden/)"
1776 );
1777
1778 let file_names: Vec<_> = files
1779 .iter()
1780 .filter_map(|p| p.file_name())
1781 .filter_map(|n| n.to_str())
1782 .collect();
1783 assert!(file_names.contains(&"lib.rs"));
1784 assert!(!file_names.contains(&"secret.rs"));
1785 }
1786
1787 #[test]
1789 fn test_rs_files_excludes_target_directory() {
1790 use std::fs;
1791 use tempfile::TempDir;
1792
1793 let temp = TempDir::new().unwrap();
1794 let project_dir = temp.path().join("myproject");
1795 let src_dir = project_dir.join("src");
1796 let target_dir = project_dir.join("target").join("debug");
1797 fs::create_dir_all(&src_dir).unwrap();
1798 fs::create_dir_all(&target_dir).unwrap();
1799
1800 fs::write(src_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1802 fs::write(target_dir.join("generated.rs"), "// generated").unwrap();
1803
1804 let files: Vec<_> = rs_files(&project_dir).collect();
1806 assert_eq!(
1807 files.len(),
1808 1,
1809 "Should find only 1 .rs file (excluding target/)"
1810 );
1811
1812 let file_names: Vec<_> = files
1813 .iter()
1814 .filter_map(|p| p.file_name())
1815 .filter_map(|n| n.to_str())
1816 .collect();
1817 assert!(file_names.contains(&"lib.rs"));
1818 assert!(!file_names.contains(&"generated.rs"));
1819 }
1820}