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::connascence::{ConnascenceAnalyzer, ConnascenceType, detect_algorithm_patterns};
22use crate::metrics::{
23 CouplingMetrics, Distance, IntegrationStrength, ModuleMetrics, ProjectMetrics, Visibility,
24 Volatility,
25};
26use crate::workspace::{WorkspaceError, WorkspaceInfo, resolve_crate_from_path};
27
28fn convert_visibility(vis: &syn::Visibility) -> Visibility {
30 match vis {
31 syn::Visibility::Public(_) => Visibility::Public,
32 syn::Visibility::Restricted(restricted) => {
33 let path_str = restricted
35 .path
36 .segments
37 .iter()
38 .map(|s| s.ident.to_string())
39 .collect::<Vec<_>>()
40 .join("::");
41
42 match path_str.as_str() {
43 "crate" => Visibility::PubCrate,
44 "super" => Visibility::PubSuper,
45 "self" => Visibility::Private, _ => Visibility::PubIn, }
48 }
49 syn::Visibility::Inherited => Visibility::Private,
50 }
51}
52
53#[derive(Error, Debug)]
55pub enum AnalyzerError {
56 #[error("Failed to read file: {0}")]
57 IoError(#[from] std::io::Error),
58
59 #[error("Failed to parse Rust file: {0}")]
60 ParseError(String),
61
62 #[error("Invalid path: {0}")]
63 InvalidPath(String),
64
65 #[error("Workspace error: {0}")]
66 WorkspaceError(#[from] WorkspaceError),
67}
68
69#[derive(Debug, Clone)]
71pub struct Dependency {
72 pub path: String,
74 pub kind: DependencyKind,
76 pub line: usize,
78 pub usage: UsageContext,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum DependencyKind {
85 InternalUse,
87 ExternalUse,
89 TraitImpl,
91 InherentImpl,
93 TypeRef,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub enum UsageContext {
100 Import,
102 TraitBound,
104 FieldAccess,
106 MethodCall,
108 FunctionCall,
110 StructConstruction,
112 TypeParameter,
114 FunctionParameter,
116 ReturnType,
118 InherentImplBlock,
120}
121
122impl UsageContext {
123 pub fn to_strength(&self) -> IntegrationStrength {
125 match self {
126 UsageContext::FieldAccess => IntegrationStrength::Intrusive,
128 UsageContext::StructConstruction => IntegrationStrength::Intrusive,
129 UsageContext::InherentImplBlock => IntegrationStrength::Intrusive,
130
131 UsageContext::MethodCall => IntegrationStrength::Functional,
133 UsageContext::FunctionCall => IntegrationStrength::Functional,
134 UsageContext::FunctionParameter => IntegrationStrength::Functional,
135 UsageContext::ReturnType => IntegrationStrength::Functional,
136
137 UsageContext::TypeParameter => IntegrationStrength::Model,
139 UsageContext::Import => IntegrationStrength::Model,
140
141 UsageContext::TraitBound => IntegrationStrength::Contract,
143 }
144 }
145}
146
147impl DependencyKind {
148 pub fn to_strength(&self) -> IntegrationStrength {
149 match self {
150 DependencyKind::TraitImpl => IntegrationStrength::Contract,
151 DependencyKind::InternalUse => IntegrationStrength::Model,
152 DependencyKind::ExternalUse => IntegrationStrength::Model,
153 DependencyKind::TypeRef => IntegrationStrength::Model,
154 DependencyKind::InherentImpl => IntegrationStrength::Intrusive,
155 }
156 }
157}
158
159#[derive(Debug)]
161pub struct CouplingAnalyzer {
162 pub current_module: String,
164 pub file_path: std::path::PathBuf,
166 pub metrics: ModuleMetrics,
168 pub dependencies: Vec<Dependency>,
170 pub defined_types: HashSet<String>,
172 pub defined_traits: HashSet<String>,
174 imported_types: HashMap<String, String>,
176 seen_dependencies: HashSet<(String, UsageContext)>,
178 pub usage_counts: UsageCounts,
180 pub type_visibility: HashMap<String, Visibility>,
182 pub connascence: ConnascenceAnalyzer,
184}
185
186#[derive(Debug, Default, Clone)]
188pub struct UsageCounts {
189 pub field_accesses: usize,
190 pub method_calls: usize,
191 pub function_calls: usize,
192 pub struct_constructions: usize,
193 pub trait_bounds: usize,
194 pub type_parameters: usize,
195}
196
197impl CouplingAnalyzer {
198 pub fn new(module_name: String, path: std::path::PathBuf) -> Self {
200 let mut connascence = ConnascenceAnalyzer::new();
201 connascence.set_module(module_name.clone());
202
203 Self {
204 current_module: module_name.clone(),
205 file_path: path.clone(),
206 metrics: ModuleMetrics::new(path, module_name),
207 dependencies: Vec::new(),
208 defined_types: HashSet::new(),
209 defined_traits: HashSet::new(),
210 imported_types: HashMap::new(),
211 seen_dependencies: HashSet::new(),
212 usage_counts: UsageCounts::default(),
213 type_visibility: HashMap::new(),
214 connascence,
215 }
216 }
217
218 pub fn analyze_file(&mut self, content: &str) -> Result<(), AnalyzerError> {
220 let syntax: File =
221 syn::parse_file(content).map_err(|e| AnalyzerError::ParseError(e.to_string()))?;
222
223 self.visit_file(&syntax);
224
225 for (pattern, context) in detect_algorithm_patterns(content) {
227 self.connascence
228 .record_algorithm_dependency(pattern, &context);
229 }
230
231 Ok(())
232 }
233
234 fn add_dependency(&mut self, path: String, kind: DependencyKind, usage: UsageContext) {
236 let key = (path.clone(), usage);
237 if self.seen_dependencies.contains(&key) {
238 return;
239 }
240 self.seen_dependencies.insert(key);
241
242 let connascence_type = match usage {
244 UsageContext::Import => ConnascenceType::Name,
245 UsageContext::TraitBound => ConnascenceType::Type,
246 UsageContext::TypeParameter => ConnascenceType::Type,
247 UsageContext::FunctionParameter => ConnascenceType::Type,
248 UsageContext::ReturnType => ConnascenceType::Type,
249 UsageContext::MethodCall | UsageContext::FunctionCall => ConnascenceType::Name,
250 UsageContext::FieldAccess => ConnascenceType::Name,
251 UsageContext::StructConstruction => ConnascenceType::Position, UsageContext::InherentImplBlock => ConnascenceType::Type,
253 };
254
255 match connascence_type {
257 ConnascenceType::Name => {
258 self.connascence
259 .record_name_dependency(&path, &format!("{:?}", usage));
260 }
261 ConnascenceType::Type => {
262 self.connascence
263 .record_type_dependency(&path, &format!("{:?}", usage));
264 }
265 ConnascenceType::Position => {
266 self.connascence
268 .record_name_dependency(&path, &format!("{:?}", usage));
269 }
270 _ => {}
271 }
272
273 self.dependencies.push(Dependency {
274 path,
275 kind,
276 line: 0,
277 usage,
278 });
279 }
280
281 fn extract_use_paths(&self, tree: &UseTree, prefix: &str) -> Vec<(String, DependencyKind)> {
283 let mut paths = Vec::new();
284
285 match tree {
286 UseTree::Path(path) => {
287 let new_prefix = if prefix.is_empty() {
288 path.ident.to_string()
289 } else {
290 format!("{}::{}", prefix, path.ident)
291 };
292 paths.extend(self.extract_use_paths(&path.tree, &new_prefix));
293 }
294 UseTree::Name(name) => {
295 let full_path = if prefix.is_empty() {
296 name.ident.to_string()
297 } else {
298 format!("{}::{}", prefix, name.ident)
299 };
300 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
301 DependencyKind::InternalUse
302 } else {
303 DependencyKind::ExternalUse
304 };
305 paths.push((full_path, kind));
306 }
307 UseTree::Rename(rename) => {
308 let full_path = if prefix.is_empty() {
309 rename.ident.to_string()
310 } else {
311 format!("{}::{}", prefix, rename.ident)
312 };
313 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
314 DependencyKind::InternalUse
315 } else {
316 DependencyKind::ExternalUse
317 };
318 paths.push((full_path, kind));
319 }
320 UseTree::Glob(_) => {
321 let full_path = format!("{}::*", prefix);
322 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
323 DependencyKind::InternalUse
324 } else {
325 DependencyKind::ExternalUse
326 };
327 paths.push((full_path, kind));
328 }
329 UseTree::Group(group) => {
330 for item in &group.items {
331 paths.extend(self.extract_use_paths(item, prefix));
332 }
333 }
334 }
335
336 paths
337 }
338
339 fn extract_type_name(&self, ty: &Type) -> Option<String> {
341 match ty {
342 Type::Path(type_path) => {
343 let segments: Vec<_> = type_path
344 .path
345 .segments
346 .iter()
347 .map(|s| s.ident.to_string())
348 .collect();
349 Some(segments.join("::"))
350 }
351 Type::Reference(ref_type) => self.extract_type_name(&ref_type.elem),
352 Type::Slice(slice_type) => self.extract_type_name(&slice_type.elem),
353 Type::Array(array_type) => self.extract_type_name(&array_type.elem),
354 Type::Ptr(ptr_type) => self.extract_type_name(&ptr_type.elem),
355 Type::Paren(paren_type) => self.extract_type_name(&paren_type.elem),
356 Type::Group(group_type) => self.extract_type_name(&group_type.elem),
357 _ => None,
358 }
359 }
360
361 fn analyze_signature(&mut self, sig: &Signature) {
363 let fn_name = sig.ident.to_string();
364
365 let arg_count = sig
367 .inputs
368 .iter()
369 .filter(|arg| !matches!(arg, FnArg::Receiver(_)))
370 .count();
371
372 self.connascence
374 .record_position_dependency(&fn_name, arg_count);
375
376 for arg in &sig.inputs {
378 if let FnArg::Typed(pat_type) = arg
379 && let Some(type_name) = self.extract_type_name(&pat_type.ty)
380 && !self.is_primitive_type(&type_name)
381 {
382 self.add_dependency(
383 type_name,
384 DependencyKind::TypeRef,
385 UsageContext::FunctionParameter,
386 );
387 }
388 }
389
390 if let ReturnType::Type(_, ty) = &sig.output
392 && let Some(type_name) = self.extract_type_name(ty)
393 && !self.is_primitive_type(&type_name)
394 {
395 self.add_dependency(type_name, DependencyKind::TypeRef, UsageContext::ReturnType);
396 }
397 }
398
399 fn is_primitive_type(&self, type_name: &str) -> bool {
401 if matches!(
403 type_name,
404 "bool"
405 | "char"
406 | "str"
407 | "u8"
408 | "u16"
409 | "u32"
410 | "u64"
411 | "u128"
412 | "usize"
413 | "i8"
414 | "i16"
415 | "i32"
416 | "i64"
417 | "i128"
418 | "isize"
419 | "f32"
420 | "f64"
421 | "String"
422 | "Self"
423 | "()"
424 | "Option"
425 | "Result"
426 | "Vec"
427 | "Box"
428 | "Rc"
429 | "Arc"
430 | "RefCell"
431 | "Cell"
432 | "Mutex"
433 | "RwLock"
434 ) {
435 return true;
436 }
437
438 if type_name.len() <= 3 && type_name.chars().all(|c| c.is_lowercase()) {
441 return true;
442 }
443
444 if type_name.starts_with("self") || type_name == "self" {
446 return true;
447 }
448
449 false
450 }
451}
452
453impl<'ast> Visit<'ast> for CouplingAnalyzer {
454 fn visit_item_use(&mut self, node: &'ast ItemUse) {
455 let paths = self.extract_use_paths(&node.tree, "");
456
457 for (path, kind) in paths {
458 if path == "self" || path.starts_with("self::") {
460 continue;
461 }
462
463 if let Some(type_name) = path.split("::").last() {
465 self.imported_types
466 .insert(type_name.to_string(), path.clone());
467 }
468
469 self.add_dependency(path.clone(), kind, UsageContext::Import);
470
471 if kind == DependencyKind::InternalUse {
473 if !self.metrics.internal_deps.contains(&path) {
474 self.metrics.internal_deps.push(path.clone());
475 }
476 } else if kind == DependencyKind::ExternalUse {
477 let crate_name = path.split("::").next().unwrap_or(&path).to_string();
479 if !self.metrics.external_deps.contains(&crate_name) {
480 self.metrics.external_deps.push(crate_name);
481 }
482 }
483 }
484
485 syn::visit::visit_item_use(self, node);
486 }
487
488 fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
489 if let Some((_, trait_path, _)) = &node.trait_ {
490 self.metrics.trait_impl_count += 1;
492
493 let trait_name: String = trait_path
495 .segments
496 .iter()
497 .map(|s| s.ident.to_string())
498 .collect::<Vec<_>>()
499 .join("::");
500
501 self.add_dependency(
502 trait_name,
503 DependencyKind::TraitImpl,
504 UsageContext::TraitBound,
505 );
506 self.usage_counts.trait_bounds += 1;
507 } else {
508 self.metrics.inherent_impl_count += 1;
510
511 if let Some(type_name) = self.extract_type_name(&node.self_ty)
513 && !self.defined_types.contains(&type_name)
514 {
515 self.add_dependency(
516 type_name,
517 DependencyKind::InherentImpl,
518 UsageContext::InherentImplBlock,
519 );
520 }
521 }
522 syn::visit::visit_item_impl(self, node);
523 }
524
525 fn visit_item_fn(&mut self, node: &'ast ItemFn) {
526 self.analyze_signature(&node.sig);
528 syn::visit::visit_item_fn(self, node);
529 }
530
531 fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
532 let name = node.ident.to_string();
533 let visibility = convert_visibility(&node.vis);
534
535 self.defined_types.insert(name.clone());
536 self.type_visibility.insert(name.clone(), visibility);
537
538 self.metrics.add_type_definition(name, visibility, false);
540
541 match &node.fields {
543 syn::Fields::Named(fields) => {
544 self.metrics.type_usage_count += fields.named.len();
545 for field in &fields.named {
546 if let Some(type_name) = self.extract_type_name(&field.ty)
547 && !self.is_primitive_type(&type_name)
548 {
549 self.add_dependency(
550 type_name,
551 DependencyKind::TypeRef,
552 UsageContext::TypeParameter,
553 );
554 self.usage_counts.type_parameters += 1;
555 }
556 }
557 }
558 syn::Fields::Unnamed(fields) => {
559 for field in &fields.unnamed {
560 if let Some(type_name) = self.extract_type_name(&field.ty)
561 && !self.is_primitive_type(&type_name)
562 {
563 self.add_dependency(
564 type_name,
565 DependencyKind::TypeRef,
566 UsageContext::TypeParameter,
567 );
568 }
569 }
570 }
571 syn::Fields::Unit => {}
572 }
573 syn::visit::visit_item_struct(self, node);
574 }
575
576 fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
577 let name = node.ident.to_string();
578 let visibility = convert_visibility(&node.vis);
579
580 self.defined_types.insert(name.clone());
581 self.type_visibility.insert(name.clone(), visibility);
582
583 self.metrics.add_type_definition(name, visibility, false);
585
586 for variant in &node.variants {
588 match &variant.fields {
589 syn::Fields::Named(fields) => {
590 for field in &fields.named {
591 if let Some(type_name) = self.extract_type_name(&field.ty)
592 && !self.is_primitive_type(&type_name)
593 {
594 self.add_dependency(
595 type_name,
596 DependencyKind::TypeRef,
597 UsageContext::TypeParameter,
598 );
599 }
600 }
601 }
602 syn::Fields::Unnamed(fields) => {
603 for field in &fields.unnamed {
604 if let Some(type_name) = self.extract_type_name(&field.ty)
605 && !self.is_primitive_type(&type_name)
606 {
607 self.add_dependency(
608 type_name,
609 DependencyKind::TypeRef,
610 UsageContext::TypeParameter,
611 );
612 }
613 }
614 }
615 syn::Fields::Unit => {}
616 }
617 }
618 syn::visit::visit_item_enum(self, node);
619 }
620
621 fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
622 let name = node.ident.to_string();
623 let visibility = convert_visibility(&node.vis);
624
625 self.defined_traits.insert(name.clone());
626 self.type_visibility.insert(name.clone(), visibility);
627
628 self.metrics.add_type_definition(name, visibility, true);
630
631 self.metrics.trait_impl_count += 1;
632 syn::visit::visit_item_trait(self, node);
633 }
634
635 fn visit_item_mod(&mut self, node: &'ast ItemMod) {
636 if node.content.is_some() {
637 self.metrics.internal_deps.push(node.ident.to_string());
638 }
639 syn::visit::visit_item_mod(self, node);
640 }
641
642 fn visit_expr_field(&mut self, node: &'ast ExprField) {
644 if let Expr::Path(path_expr) = &*node.base {
646 let base_name = path_expr
647 .path
648 .segments
649 .iter()
650 .map(|s| s.ident.to_string())
651 .collect::<Vec<_>>()
652 .join("::");
653
654 let full_path = self
656 .imported_types
657 .get(&base_name)
658 .cloned()
659 .unwrap_or(base_name);
660
661 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
662 self.add_dependency(
663 full_path,
664 DependencyKind::TypeRef,
665 UsageContext::FieldAccess,
666 );
667 self.usage_counts.field_accesses += 1;
668 }
669 }
670 syn::visit::visit_expr_field(self, node);
671 }
672
673 fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
675 if let Expr::Path(path_expr) = &*node.receiver {
677 let receiver_name = path_expr
678 .path
679 .segments
680 .iter()
681 .map(|s| s.ident.to_string())
682 .collect::<Vec<_>>()
683 .join("::");
684
685 let full_path = self
686 .imported_types
687 .get(&receiver_name)
688 .cloned()
689 .unwrap_or(receiver_name);
690
691 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
692 self.add_dependency(full_path, DependencyKind::TypeRef, UsageContext::MethodCall);
693 self.usage_counts.method_calls += 1;
694 }
695 }
696 syn::visit::visit_expr_method_call(self, node);
697 }
698
699 fn visit_expr_call(&mut self, node: &'ast ExprCall) {
701 if let Expr::Path(path_expr) = &*node.func {
702 let path_str = path_expr
703 .path
704 .segments
705 .iter()
706 .map(|s| s.ident.to_string())
707 .collect::<Vec<_>>()
708 .join("::");
709
710 if path_str.contains("::") || path_str.chars().next().is_some_and(|c| c.is_uppercase())
712 {
713 let full_path = self
714 .imported_types
715 .get(&path_str)
716 .cloned()
717 .unwrap_or(path_str.clone());
718
719 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
720 self.add_dependency(
721 full_path,
722 DependencyKind::TypeRef,
723 UsageContext::FunctionCall,
724 );
725 self.usage_counts.function_calls += 1;
726 }
727 }
728 }
729 syn::visit::visit_expr_call(self, node);
730 }
731
732 fn visit_expr_struct(&mut self, node: &'ast ExprStruct) {
734 let struct_name = node
735 .path
736 .segments
737 .iter()
738 .map(|s| s.ident.to_string())
739 .collect::<Vec<_>>()
740 .join("::");
741
742 if struct_name == "Self" || struct_name.starts_with("Self::") {
744 syn::visit::visit_expr_struct(self, node);
745 return;
746 }
747
748 let full_path = self
749 .imported_types
750 .get(&struct_name)
751 .cloned()
752 .unwrap_or(struct_name.clone());
753
754 if !self.defined_types.contains(&full_path) && !self.is_primitive_type(&struct_name) {
755 self.add_dependency(
756 full_path,
757 DependencyKind::TypeRef,
758 UsageContext::StructConstruction,
759 );
760 self.usage_counts.struct_constructions += 1;
761 }
762 syn::visit::visit_expr_struct(self, node);
763 }
764}
765
766#[derive(Debug, Clone)]
768struct AnalyzedFile {
769 module_name: String,
770 #[allow(dead_code)]
771 file_path: PathBuf,
772 metrics: ModuleMetrics,
773 dependencies: Vec<Dependency>,
774 type_visibility: HashMap<String, Visibility>,
776 connascence: ConnascenceAnalyzer,
778}
779
780pub fn analyze_project(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
782 analyze_project_parallel(path)
783}
784
785pub fn analyze_project_parallel(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
790 if !path.exists() {
791 return Err(AnalyzerError::InvalidPath(path.display().to_string()));
792 }
793
794 let file_paths: Vec<PathBuf> = WalkDir::new(path)
796 .follow_links(true)
797 .into_iter()
798 .filter_map(|e| e.ok())
799 .filter(|entry| {
800 let file_path = entry.path();
801 !file_path.components().any(|c| {
803 let s = c.as_os_str().to_string_lossy();
804 s == "target" || s.starts_with('.')
805 }) && file_path.extension() == Some(OsStr::new("rs"))
806 })
807 .map(|e| e.path().to_path_buf())
808 .collect();
809
810 let num_threads = rayon::current_num_threads();
814 let file_count = file_paths.len();
815
816 let chunk_size = if file_count < num_threads * 2 {
819 1 } else {
821 (file_count / (num_threads * 4)).max(1)
824 };
825
826 let analyzed_results: Vec<_> = file_paths
828 .par_chunks(chunk_size)
829 .flat_map(|chunk| {
830 chunk
831 .iter()
832 .filter_map(|file_path| match analyze_rust_file_full(file_path) {
833 Ok(result) => Some(AnalyzedFile {
834 module_name: result.metrics.name.clone(),
835 file_path: file_path.clone(),
836 metrics: result.metrics,
837 dependencies: result.dependencies,
838 type_visibility: result.type_visibility,
839 connascence: result.connascence,
840 }),
841 Err(e) => {
842 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
843 None
844 }
845 })
846 .collect::<Vec<_>>()
847 })
848 .collect();
849
850 let module_names: HashSet<String> = analyzed_results
852 .iter()
853 .map(|a| a.module_name.clone())
854 .collect();
855
856 let mut project = ProjectMetrics::new();
858 project.total_files = analyzed_results.len();
859
860 for analyzed in &analyzed_results {
862 for (type_name, visibility) in &analyzed.type_visibility {
863 project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
864 }
865 }
866
867 for analyzed in &analyzed_results {
869 project.add_module(analyzed.metrics.clone());
870
871 for dep in &analyzed.dependencies {
872 if !is_valid_dependency_path(&dep.path) {
874 continue;
875 }
876
877 let target_module = extract_target_module(&dep.path);
879
880 if !is_valid_dependency_path(&target_module) {
882 continue;
883 }
884
885 let distance = calculate_distance(&dep.path, &module_names);
887
888 let strength = dep.usage.to_strength();
890
891 let volatility = Volatility::Low;
893
894 let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
896 let visibility = project
897 .get_type_visibility(target_type)
898 .unwrap_or(Visibility::Public); let coupling = CouplingMetrics::with_visibility(
902 analyzed.module_name.clone(),
903 target_module.clone(),
904 strength,
905 distance,
906 volatility,
907 visibility,
908 );
909
910 project.add_coupling(coupling);
911 }
912 }
913
914 project.update_coupling_visibility();
916
917 for analyzed in &analyzed_results {
919 for (conn_type, count) in &analyzed.connascence.stats.by_type {
920 for _ in 0..*count {
921 project.connascence_stats.add(*conn_type);
922 }
923 }
924 }
925
926 Ok(project)
927}
928
929pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
931 let workspace = match WorkspaceInfo::from_path(path) {
933 Ok(ws) => Some(ws),
934 Err(e) => {
935 eprintln!("Note: Could not load workspace metadata: {}", e);
936 eprintln!("Falling back to basic analysis...");
937 None
938 }
939 };
940
941 if let Some(ws) = workspace {
942 analyze_with_workspace(path, &ws)
943 } else {
944 analyze_project(path)
946 }
947}
948
949fn analyze_with_workspace(
951 _path: &Path,
952 workspace: &WorkspaceInfo,
953) -> Result<ProjectMetrics, AnalyzerError> {
954 let mut project = ProjectMetrics::new();
955
956 project.workspace_name = Some(
958 workspace
959 .root
960 .file_name()
961 .and_then(|n| n.to_str())
962 .unwrap_or("workspace")
963 .to_string(),
964 );
965 project.workspace_members = workspace.members.clone();
966
967 let mut file_crate_pairs: Vec<(PathBuf, String)> = Vec::new();
969
970 for member_name in &workspace.members {
971 if let Some(crate_info) = workspace.get_crate(member_name) {
972 if !crate_info.src_path.exists() {
973 continue;
974 }
975
976 for entry in WalkDir::new(&crate_info.src_path)
977 .follow_links(true)
978 .into_iter()
979 .filter_map(|e| e.ok())
980 {
981 let file_path = entry.path();
982
983 if file_path.components().any(|c| {
985 let s = c.as_os_str().to_string_lossy();
986 s == "target" || s.starts_with('.')
987 }) {
988 continue;
989 }
990
991 if file_path.extension() == Some(OsStr::new("rs")) {
993 file_crate_pairs.push((file_path.to_path_buf(), member_name.clone()));
994 }
995 }
996 }
997 }
998
999 let num_threads = rayon::current_num_threads();
1001 let file_count = file_crate_pairs.len();
1002 let chunk_size = if file_count < num_threads * 2 {
1003 1
1004 } else {
1005 (file_count / (num_threads * 4)).max(1)
1006 };
1007
1008 let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1010 .par_chunks(chunk_size)
1011 .flat_map(|chunk| {
1012 chunk
1013 .iter()
1014 .filter_map(
1015 |(file_path, crate_name)| match analyze_rust_file_full(file_path) {
1016 Ok(result) => Some(AnalyzedFileWithCrate {
1017 module_name: result.metrics.name.clone(),
1018 crate_name: crate_name.clone(),
1019 file_path: file_path.clone(),
1020 metrics: result.metrics,
1021 dependencies: result.dependencies,
1022 connascence: result.connascence,
1023 }),
1024 Err(e) => {
1025 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1026 None
1027 }
1028 },
1029 )
1030 .collect::<Vec<_>>()
1031 })
1032 .collect();
1033
1034 project.total_files = analyzed_files.len();
1035
1036 for analyzed in &analyzed_files {
1038 project.add_module(analyzed.metrics.clone());
1039
1040 for dep in &analyzed.dependencies {
1041 if !is_valid_dependency_path(&dep.path) {
1043 continue;
1044 }
1045
1046 let resolved_crate =
1048 resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1049
1050 let target_module = extract_target_module(&dep.path);
1051
1052 if !is_valid_dependency_path(&target_module) {
1054 continue;
1055 }
1056
1057 let distance =
1059 calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1060
1061 let strength = dep.usage.to_strength();
1063
1064 let volatility = Volatility::Low;
1066
1067 let mut coupling = CouplingMetrics::new(
1069 format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1070 if let Some(ref crate_name) = resolved_crate {
1071 format!("{}::{}", crate_name, target_module)
1072 } else {
1073 target_module.clone()
1074 },
1075 strength,
1076 distance,
1077 volatility,
1078 );
1079
1080 coupling.source_crate = Some(analyzed.crate_name.clone());
1082 coupling.target_crate = resolved_crate;
1083
1084 project.add_coupling(coupling);
1085 }
1086 }
1087
1088 for (crate_name, deps) in &workspace.dependency_graph {
1090 if workspace.is_workspace_member(crate_name) {
1091 for dep in deps {
1092 project
1094 .crate_dependencies
1095 .entry(crate_name.clone())
1096 .or_default()
1097 .push(dep.clone());
1098 }
1099 }
1100 }
1101
1102 for analyzed in &analyzed_files {
1104 for (conn_type, count) in &analyzed.connascence.stats.by_type {
1105 for _ in 0..*count {
1106 project.connascence_stats.add(*conn_type);
1107 }
1108 }
1109 }
1110
1111 Ok(project)
1112}
1113
1114fn calculate_distance_with_workspace(
1116 dep_path: &str,
1117 current_crate: &str,
1118 workspace: &WorkspaceInfo,
1119) -> Distance {
1120 if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1121 Distance::SameModule
1123 } else if dep_path.starts_with("super::") {
1124 Distance::DifferentModule
1126 } else {
1127 if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1129 if target_crate == current_crate {
1130 Distance::SameModule
1131 } else if workspace.is_workspace_member(&target_crate) {
1132 Distance::DifferentModule
1134 } else {
1135 Distance::DifferentCrate
1137 }
1138 } else {
1139 Distance::DifferentCrate
1140 }
1141 }
1142}
1143
1144#[derive(Debug, Clone)]
1146struct AnalyzedFileWithCrate {
1147 module_name: String,
1148 crate_name: String,
1149 #[allow(dead_code)]
1150 file_path: PathBuf,
1151 metrics: ModuleMetrics,
1152 dependencies: Vec<Dependency>,
1153 connascence: ConnascenceAnalyzer,
1154}
1155
1156fn extract_target_module(path: &str) -> String {
1158 let cleaned = path
1160 .trim_start_matches("crate::")
1161 .trim_start_matches("super::")
1162 .trim_start_matches("::");
1163
1164 cleaned.split("::").next().unwrap_or(path).to_string()
1166}
1167
1168fn is_valid_dependency_path(path: &str) -> bool {
1170 if path.is_empty() {
1172 return false;
1173 }
1174
1175 if path == "Self" || path.starts_with("Self::") {
1177 return false;
1178 }
1179
1180 let segments: Vec<&str> = path.split("::").collect();
1181
1182 if segments.len() == 1 {
1184 let name = segments[0];
1185 if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1186 return false;
1187 }
1188 }
1189
1190 if segments.len() >= 2 {
1192 let last = segments.last().unwrap();
1193 let second_last = segments.get(segments.len() - 2).unwrap();
1194 if last == second_last {
1195 return false;
1196 }
1197 }
1198
1199 let last_segment = segments.last().unwrap_or(&path);
1201 let common_locals = [
1202 "request",
1203 "response",
1204 "result",
1205 "content",
1206 "config",
1207 "proto",
1208 "domain",
1209 "info",
1210 "data",
1211 "item",
1212 "value",
1213 "error",
1214 "message",
1215 "expected",
1216 "actual",
1217 "status",
1218 "state",
1219 "context",
1220 "params",
1221 "args",
1222 "options",
1223 "settings",
1224 "violation",
1225 "page_token",
1226 ];
1227 if common_locals.contains(last_segment) && segments.len() <= 2 {
1228 return false;
1229 }
1230
1231 true
1232}
1233
1234fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1236 if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1237 Distance::DifferentModule
1239 } else if dep_path.starts_with("self::") {
1240 Distance::SameModule
1241 } else {
1242 Distance::DifferentCrate
1244 }
1245}
1246
1247pub struct AnalyzedFileResult {
1250 pub metrics: ModuleMetrics,
1251 pub dependencies: Vec<Dependency>,
1252 pub type_visibility: HashMap<String, Visibility>,
1253 pub connascence: ConnascenceAnalyzer,
1254}
1255
1256pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1257 let result = analyze_rust_file_full(path)?;
1258 Ok((result.metrics, result.dependencies))
1259}
1260
1261pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1263 let content = fs::read_to_string(path)?;
1264
1265 let module_name = path
1266 .file_stem()
1267 .and_then(|s| s.to_str())
1268 .unwrap_or("unknown")
1269 .to_string();
1270
1271 let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1272 analyzer.analyze_file(&content)?;
1273
1274 Ok(AnalyzedFileResult {
1275 metrics: analyzer.metrics,
1276 dependencies: analyzer.dependencies,
1277 type_visibility: analyzer.type_visibility,
1278 connascence: analyzer.connascence,
1279 })
1280}
1281
1282#[cfg(test)]
1283mod tests {
1284 use super::*;
1285
1286 #[test]
1287 fn test_analyzer_creation() {
1288 let analyzer = CouplingAnalyzer::new(
1289 "test_module".to_string(),
1290 std::path::PathBuf::from("test.rs"),
1291 );
1292 assert_eq!(analyzer.current_module, "test_module");
1293 }
1294
1295 #[test]
1296 fn test_analyze_simple_file() {
1297 let mut analyzer =
1298 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1299
1300 let code = r#"
1301 pub struct User {
1302 name: String,
1303 email: String,
1304 }
1305
1306 impl User {
1307 pub fn new(name: String, email: String) -> Self {
1308 Self { name, email }
1309 }
1310 }
1311 "#;
1312
1313 let result = analyzer.analyze_file(code);
1314 assert!(result.is_ok());
1315 assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1316 }
1317
1318 #[test]
1319 fn test_analyze_trait_impl() {
1320 let mut analyzer =
1321 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1322
1323 let code = r#"
1324 trait Printable {
1325 fn print(&self);
1326 }
1327
1328 struct Document;
1329
1330 impl Printable for Document {
1331 fn print(&self) {}
1332 }
1333 "#;
1334
1335 let result = analyzer.analyze_file(code);
1336 assert!(result.is_ok());
1337 assert!(analyzer.metrics.trait_impl_count >= 1);
1338 }
1339
1340 #[test]
1341 fn test_analyze_use_statements() {
1342 let mut analyzer =
1343 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1344
1345 let code = r#"
1346 use std::collections::HashMap;
1347 use serde::Serialize;
1348 use crate::utils;
1349 use crate::models::{User, Post};
1350 "#;
1351
1352 let result = analyzer.analyze_file(code);
1353 assert!(result.is_ok());
1354 assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1355 assert!(
1356 analyzer
1357 .metrics
1358 .external_deps
1359 .contains(&"serde".to_string())
1360 );
1361 assert!(!analyzer.dependencies.is_empty());
1362
1363 let internal_deps: Vec<_> = analyzer
1365 .dependencies
1366 .iter()
1367 .filter(|d| d.kind == DependencyKind::InternalUse)
1368 .collect();
1369 assert!(!internal_deps.is_empty());
1370 }
1371
1372 #[test]
1373 fn test_extract_use_paths() {
1374 let analyzer =
1375 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1376
1377 let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1379 let paths = analyzer.extract_use_paths(&tree, "");
1380 assert_eq!(paths.len(), 1);
1381 assert_eq!(paths[0].0, "std::collections::HashMap");
1382
1383 let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1385 let paths = analyzer.extract_use_paths(&tree, "");
1386 assert_eq!(paths.len(), 2);
1387 }
1388
1389 #[test]
1390 fn test_extract_target_module() {
1391 assert_eq!(extract_target_module("crate::models::user"), "models");
1392 assert_eq!(extract_target_module("super::utils"), "utils");
1393 assert_eq!(extract_target_module("std::collections"), "std");
1394 }
1395
1396 #[test]
1397 fn test_field_access_detection() {
1398 let mut analyzer =
1399 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1400
1401 let code = r#"
1402 use crate::models::User;
1403
1404 fn get_name(user: &User) -> String {
1405 user.name.clone()
1406 }
1407 "#;
1408
1409 let result = analyzer.analyze_file(code);
1410 assert!(result.is_ok());
1411
1412 let _field_deps: Vec<_> = analyzer
1414 .dependencies
1415 .iter()
1416 .filter(|d| d.usage == UsageContext::FieldAccess)
1417 .collect();
1418 }
1421
1422 #[test]
1423 fn test_method_call_detection() {
1424 let mut analyzer =
1425 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1426
1427 let code = r#"
1428 fn process() {
1429 let data = String::new();
1430 data.push_str("hello");
1431 }
1432 "#;
1433
1434 let result = analyzer.analyze_file(code);
1435 assert!(result.is_ok());
1436 }
1438
1439 #[test]
1440 fn test_struct_construction_detection() {
1441 let mut analyzer =
1442 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1443
1444 let code = r#"
1445 use crate::config::Config;
1446
1447 fn create_config() {
1448 let c = Config { value: 42 };
1449 }
1450 "#;
1451
1452 let result = analyzer.analyze_file(code);
1453 assert!(result.is_ok());
1454
1455 let struct_deps: Vec<_> = analyzer
1457 .dependencies
1458 .iter()
1459 .filter(|d| d.usage == UsageContext::StructConstruction)
1460 .collect();
1461 assert!(!struct_deps.is_empty());
1462 }
1463
1464 #[test]
1465 fn test_usage_context_to_strength() {
1466 assert_eq!(
1467 UsageContext::FieldAccess.to_strength(),
1468 IntegrationStrength::Intrusive
1469 );
1470 assert_eq!(
1471 UsageContext::MethodCall.to_strength(),
1472 IntegrationStrength::Functional
1473 );
1474 assert_eq!(
1475 UsageContext::TypeParameter.to_strength(),
1476 IntegrationStrength::Model
1477 );
1478 assert_eq!(
1479 UsageContext::TraitBound.to_strength(),
1480 IntegrationStrength::Contract
1481 );
1482 }
1483}