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 !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 for analyzed in &analyzed_files {
1220 let mut metrics = analyzed.metrics.clone();
1222 metrics.item_dependencies = analyzed.item_dependencies.clone();
1223 project.add_module(metrics);
1224
1225 for dep in &analyzed.dependencies {
1226 if !is_valid_dependency_path(&dep.path) {
1228 continue;
1229 }
1230
1231 let resolved_crate =
1233 resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1234
1235 let target_module = extract_target_module(&dep.path);
1236
1237 if !is_valid_dependency_path(&target_module) {
1239 continue;
1240 }
1241
1242 let distance =
1244 calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1245
1246 let strength = dep.usage.to_strength();
1248
1249 let volatility = Volatility::Low;
1251
1252 let mut coupling = CouplingMetrics::with_location(
1254 format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1255 if let Some(ref crate_name) = resolved_crate {
1256 format!("{}::{}", crate_name, target_module)
1257 } else {
1258 target_module.clone()
1259 },
1260 strength,
1261 distance,
1262 volatility,
1263 Visibility::Public, analyzed.file_path.clone(),
1265 dep.line,
1266 );
1267
1268 coupling.source_crate = Some(analyzed.crate_name.clone());
1270 coupling.target_crate = resolved_crate;
1271
1272 project.add_coupling(coupling);
1273 }
1274 }
1275
1276 for (crate_name, deps) in &workspace.dependency_graph {
1278 if workspace.is_workspace_member(crate_name) {
1279 for dep in deps {
1280 project
1282 .crate_dependencies
1283 .entry(crate_name.clone())
1284 .or_default()
1285 .push(dep.clone());
1286 }
1287 }
1288 }
1289
1290 Ok(project)
1291}
1292
1293fn calculate_distance_with_workspace(
1295 dep_path: &str,
1296 current_crate: &str,
1297 workspace: &WorkspaceInfo,
1298) -> Distance {
1299 if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1300 Distance::SameModule
1302 } else if dep_path.starts_with("super::") {
1303 Distance::DifferentModule
1305 } else {
1306 if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1308 if target_crate == current_crate {
1309 Distance::SameModule
1310 } else if workspace.is_workspace_member(&target_crate) {
1311 Distance::DifferentModule
1313 } else {
1314 Distance::DifferentCrate
1316 }
1317 } else {
1318 Distance::DifferentCrate
1319 }
1320 }
1321}
1322
1323#[derive(Debug, Clone)]
1325struct AnalyzedFileWithCrate {
1326 module_name: String,
1327 crate_name: String,
1328 #[allow(dead_code)]
1329 file_path: PathBuf,
1330 metrics: ModuleMetrics,
1331 dependencies: Vec<Dependency>,
1332 item_dependencies: Vec<ItemDependency>,
1334}
1335
1336fn extract_target_module(path: &str) -> String {
1338 let cleaned = path
1340 .trim_start_matches("crate::")
1341 .trim_start_matches("super::")
1342 .trim_start_matches("::");
1343
1344 cleaned.split("::").next().unwrap_or(path).to_string()
1346}
1347
1348fn is_valid_dependency_path(path: &str) -> bool {
1350 if path.is_empty() {
1352 return false;
1353 }
1354
1355 if path == "Self" || path.starts_with("Self::") {
1357 return false;
1358 }
1359
1360 let segments: Vec<&str> = path.split("::").collect();
1361
1362 if segments.len() == 1 {
1364 let name = segments[0];
1365 if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1366 return false;
1367 }
1368 }
1369
1370 if segments.len() >= 2 {
1372 let last = segments.last().unwrap();
1373 let second_last = segments.get(segments.len() - 2).unwrap();
1374 if last == second_last {
1375 return false;
1376 }
1377 }
1378
1379 let last_segment = segments.last().unwrap_or(&path);
1381 let common_locals = [
1382 "request",
1383 "response",
1384 "result",
1385 "content",
1386 "config",
1387 "proto",
1388 "domain",
1389 "info",
1390 "data",
1391 "item",
1392 "value",
1393 "error",
1394 "message",
1395 "expected",
1396 "actual",
1397 "status",
1398 "state",
1399 "context",
1400 "params",
1401 "args",
1402 "options",
1403 "settings",
1404 "violation",
1405 "page_token",
1406 ];
1407 if common_locals.contains(last_segment) && segments.len() <= 2 {
1408 return false;
1409 }
1410
1411 true
1412}
1413
1414fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1416 if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1417 Distance::DifferentModule
1419 } else if dep_path.starts_with("self::") {
1420 Distance::SameModule
1421 } else {
1422 Distance::DifferentCrate
1424 }
1425}
1426
1427pub struct AnalyzedFileResult {
1430 pub metrics: ModuleMetrics,
1431 pub dependencies: Vec<Dependency>,
1432 pub type_visibility: HashMap<String, Visibility>,
1433 pub item_dependencies: Vec<ItemDependency>,
1434}
1435
1436pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1437 let result = analyze_rust_file_full(path)?;
1438 Ok((result.metrics, result.dependencies))
1439}
1440
1441pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1443 let content = fs::read_to_string(path)?;
1444
1445 let module_name = path
1446 .file_stem()
1447 .and_then(|s| s.to_str())
1448 .unwrap_or("unknown")
1449 .to_string();
1450
1451 let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1452 analyzer.analyze_file(&content)?;
1453
1454 Ok(AnalyzedFileResult {
1455 metrics: analyzer.metrics,
1456 dependencies: analyzer.dependencies,
1457 type_visibility: analyzer.type_visibility,
1458 item_dependencies: analyzer.item_dependencies,
1459 })
1460}
1461
1462#[cfg(test)]
1463mod tests {
1464 use super::*;
1465
1466 #[test]
1467 fn test_analyzer_creation() {
1468 let analyzer = CouplingAnalyzer::new(
1469 "test_module".to_string(),
1470 std::path::PathBuf::from("test.rs"),
1471 );
1472 assert_eq!(analyzer.current_module, "test_module");
1473 }
1474
1475 #[test]
1476 fn test_analyze_simple_file() {
1477 let mut analyzer =
1478 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1479
1480 let code = r#"
1481 pub struct User {
1482 name: String,
1483 email: String,
1484 }
1485
1486 impl User {
1487 pub fn new(name: String, email: String) -> Self {
1488 Self { name, email }
1489 }
1490 }
1491 "#;
1492
1493 let result = analyzer.analyze_file(code);
1494 assert!(result.is_ok());
1495 assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1496 }
1497
1498 #[test]
1499 fn test_item_dependencies() {
1500 let mut analyzer =
1501 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1502
1503 let code = r#"
1504 pub struct Config {
1505 pub value: i32,
1506 }
1507
1508 pub fn process(config: Config) -> i32 {
1509 let x = config.value;
1510 helper(x)
1511 }
1512
1513 fn helper(n: i32) -> i32 {
1514 n * 2
1515 }
1516 "#;
1517
1518 let result = analyzer.analyze_file(code);
1519 assert!(result.is_ok());
1520
1521 assert!(analyzer.defined_functions.contains_key("process"));
1523 assert!(analyzer.defined_functions.contains_key("helper"));
1524
1525 println!(
1527 "Item dependencies count: {}",
1528 analyzer.item_dependencies.len()
1529 );
1530 for dep in &analyzer.item_dependencies {
1531 println!(
1532 " {} -> {} ({:?})",
1533 dep.source_item, dep.target, dep.dep_type
1534 );
1535 }
1536
1537 let process_deps: Vec<_> = analyzer
1539 .item_dependencies
1540 .iter()
1541 .filter(|d| d.source_item == "process")
1542 .collect();
1543
1544 assert!(
1545 !process_deps.is_empty(),
1546 "process function should have item dependencies"
1547 );
1548 }
1549
1550 #[test]
1551 fn test_analyze_trait_impl() {
1552 let mut analyzer =
1553 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1554
1555 let code = r#"
1556 trait Printable {
1557 fn print(&self);
1558 }
1559
1560 struct Document;
1561
1562 impl Printable for Document {
1563 fn print(&self) {}
1564 }
1565 "#;
1566
1567 let result = analyzer.analyze_file(code);
1568 assert!(result.is_ok());
1569 assert!(analyzer.metrics.trait_impl_count >= 1);
1570 }
1571
1572 #[test]
1573 fn test_analyze_use_statements() {
1574 let mut analyzer =
1575 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1576
1577 let code = r#"
1578 use std::collections::HashMap;
1579 use serde::Serialize;
1580 use crate::utils;
1581 use crate::models::{User, Post};
1582 "#;
1583
1584 let result = analyzer.analyze_file(code);
1585 assert!(result.is_ok());
1586 assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1587 assert!(
1588 analyzer
1589 .metrics
1590 .external_deps
1591 .contains(&"serde".to_string())
1592 );
1593 assert!(!analyzer.dependencies.is_empty());
1594
1595 let internal_deps: Vec<_> = analyzer
1597 .dependencies
1598 .iter()
1599 .filter(|d| d.kind == DependencyKind::InternalUse)
1600 .collect();
1601 assert!(!internal_deps.is_empty());
1602 }
1603
1604 #[test]
1605 fn test_extract_use_paths() {
1606 let analyzer =
1607 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1608
1609 let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1611 let paths = analyzer.extract_use_paths(&tree, "");
1612 assert_eq!(paths.len(), 1);
1613 assert_eq!(paths[0].0, "std::collections::HashMap");
1614
1615 let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1617 let paths = analyzer.extract_use_paths(&tree, "");
1618 assert_eq!(paths.len(), 2);
1619 }
1620
1621 #[test]
1622 fn test_extract_target_module() {
1623 assert_eq!(extract_target_module("crate::models::user"), "models");
1624 assert_eq!(extract_target_module("super::utils"), "utils");
1625 assert_eq!(extract_target_module("std::collections"), "std");
1626 }
1627
1628 #[test]
1629 fn test_field_access_detection() {
1630 let mut analyzer =
1631 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1632
1633 let code = r#"
1634 use crate::models::User;
1635
1636 fn get_name(user: &User) -> String {
1637 user.name.clone()
1638 }
1639 "#;
1640
1641 let result = analyzer.analyze_file(code);
1642 assert!(result.is_ok());
1643
1644 let _field_deps: Vec<_> = analyzer
1646 .dependencies
1647 .iter()
1648 .filter(|d| d.usage == UsageContext::FieldAccess)
1649 .collect();
1650 }
1653
1654 #[test]
1655 fn test_method_call_detection() {
1656 let mut analyzer =
1657 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1658
1659 let code = r#"
1660 fn process() {
1661 let data = String::new();
1662 data.push_str("hello");
1663 }
1664 "#;
1665
1666 let result = analyzer.analyze_file(code);
1667 assert!(result.is_ok());
1668 }
1670
1671 #[test]
1672 fn test_struct_construction_detection() {
1673 let mut analyzer =
1674 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1675
1676 let code = r#"
1677 use crate::config::Config;
1678
1679 fn create_config() {
1680 let c = Config { value: 42 };
1681 }
1682 "#;
1683
1684 let result = analyzer.analyze_file(code);
1685 assert!(result.is_ok());
1686
1687 let struct_deps: Vec<_> = analyzer
1689 .dependencies
1690 .iter()
1691 .filter(|d| d.usage == UsageContext::StructConstruction)
1692 .collect();
1693 assert!(!struct_deps.is_empty());
1694 }
1695
1696 #[test]
1697 fn test_usage_context_to_strength() {
1698 assert_eq!(
1699 UsageContext::FieldAccess.to_strength(),
1700 IntegrationStrength::Intrusive
1701 );
1702 assert_eq!(
1703 UsageContext::MethodCall.to_strength(),
1704 IntegrationStrength::Functional
1705 );
1706 assert_eq!(
1707 UsageContext::TypeParameter.to_strength(),
1708 IntegrationStrength::Model
1709 );
1710 assert_eq!(
1711 UsageContext::TraitBound.to_strength(),
1712 IntegrationStrength::Contract
1713 );
1714 }
1715}