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
52fn has_test_attribute(attrs: &[syn::Attribute]) -> bool {
54 attrs.iter().any(|attr| attr.path().is_ident("test"))
55}
56
57fn has_cfg_test_attribute(attrs: &[syn::Attribute]) -> bool {
59 attrs.iter().any(|attr| {
60 if attr.path().is_ident("cfg") {
61 if let Ok(meta) = attr.meta.require_list() {
63 let tokens = meta.tokens.to_string();
64 return tokens.contains("test");
65 }
66 }
67 false
68 })
69}
70
71fn is_test_module(item: &ItemMod) -> bool {
73 item.ident == "tests" || has_cfg_test_attribute(&item.attrs)
74}
75
76fn file_path_to_module_path(file_path: &Path, src_root: &Path) -> String {
87 let relative = file_path.strip_prefix(src_root).unwrap_or(file_path);
89
90 let mut parts: Vec<String> = Vec::new();
91
92 for component in relative.components() {
93 if let Some(s) = component.as_os_str().to_str() {
94 parts.push(s.to_string());
95 }
96 }
97
98 if let Some(last) = parts.last().cloned() {
100 parts.pop();
101 match last.as_str() {
102 "lib.rs" | "main.rs" => {
103 }
105 "mod.rs" => {
106 }
108 _ => {
109 if let Some(stem) = last.strip_suffix(".rs") {
111 parts.push(stem.to_string());
112 } else {
113 parts.push(last);
114 }
115 }
116 }
117 }
118
119 parts.join("::")
120}
121
122#[derive(Error, Debug)]
124pub enum AnalyzerError {
125 #[error("Failed to read file: {0}")]
126 IoError(#[from] std::io::Error),
127
128 #[error("Failed to parse Rust file: {0}")]
129 ParseError(String),
130
131 #[error("Invalid path: {0}")]
132 InvalidPath(String),
133
134 #[error("Workspace error: {0}")]
135 WorkspaceError(#[from] WorkspaceError),
136}
137
138#[derive(Debug, Clone)]
140pub struct Dependency {
141 pub path: String,
143 pub kind: DependencyKind,
145 pub line: usize,
147 pub usage: UsageContext,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum DependencyKind {
154 InternalUse,
156 ExternalUse,
158 TraitImpl,
160 InherentImpl,
162 TypeRef,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168pub enum UsageContext {
169 Import,
171 TraitBound,
173 FieldAccess,
175 MethodCall,
177 FunctionCall,
179 StructConstruction,
181 TypeParameter,
183 FunctionParameter,
185 ReturnType,
187 InherentImplBlock,
189}
190
191impl UsageContext {
192 pub fn to_strength(&self) -> IntegrationStrength {
194 match self {
195 UsageContext::FieldAccess => IntegrationStrength::Intrusive,
197 UsageContext::StructConstruction => IntegrationStrength::Intrusive,
198 UsageContext::InherentImplBlock => IntegrationStrength::Intrusive,
199
200 UsageContext::MethodCall => IntegrationStrength::Functional,
202 UsageContext::FunctionCall => IntegrationStrength::Functional,
203 UsageContext::FunctionParameter => IntegrationStrength::Functional,
204 UsageContext::ReturnType => IntegrationStrength::Functional,
205
206 UsageContext::TypeParameter => IntegrationStrength::Model,
208 UsageContext::Import => IntegrationStrength::Model,
209
210 UsageContext::TraitBound => IntegrationStrength::Contract,
212 }
213 }
214}
215
216impl DependencyKind {
217 pub fn to_strength(&self) -> IntegrationStrength {
218 match self {
219 DependencyKind::TraitImpl => IntegrationStrength::Contract,
220 DependencyKind::InternalUse => IntegrationStrength::Model,
221 DependencyKind::ExternalUse => IntegrationStrength::Model,
222 DependencyKind::TypeRef => IntegrationStrength::Model,
223 DependencyKind::InherentImpl => IntegrationStrength::Intrusive,
224 }
225 }
226}
227
228#[derive(Debug)]
230pub struct CouplingAnalyzer {
231 pub current_module: String,
233 pub file_path: std::path::PathBuf,
235 pub metrics: ModuleMetrics,
237 pub dependencies: Vec<Dependency>,
239 pub defined_types: HashSet<String>,
241 pub defined_traits: HashSet<String>,
243 pub defined_functions: HashMap<String, Visibility>,
245 imported_types: HashMap<String, String>,
247 seen_dependencies: HashSet<(String, UsageContext)>,
249 pub usage_counts: UsageCounts,
251 pub type_visibility: HashMap<String, Visibility>,
253 current_item: Option<(String, ItemKind)>,
255 pub item_dependencies: Vec<ItemDependency>,
257}
258
259#[derive(Debug, Default, Clone)]
261pub struct UsageCounts {
262 pub field_accesses: usize,
263 pub method_calls: usize,
264 pub function_calls: usize,
265 pub struct_constructions: usize,
266 pub trait_bounds: usize,
267 pub type_parameters: usize,
268}
269
270#[derive(Debug, Clone)]
272pub struct ItemDependency {
273 pub source_item: String,
275 pub source_kind: ItemKind,
277 pub target: String,
279 pub target_module: Option<String>,
281 pub dep_type: ItemDepType,
283 pub line: usize,
285 pub expression: Option<String>,
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum ItemKind {
292 Function,
293 Method,
294 Struct,
295 Enum,
296 Trait,
297 Impl,
298 Module,
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum ItemDepType {
304 FunctionCall,
306 MethodCall,
308 TypeUsage,
310 FieldAccess,
312 StructConstruction,
314 TraitImpl,
316 TraitBound,
318 Import,
320}
321
322impl CouplingAnalyzer {
323 pub fn new(module_name: String, path: std::path::PathBuf) -> Self {
325 Self {
326 current_module: module_name.clone(),
327 file_path: path.clone(),
328 metrics: ModuleMetrics::new(path, module_name),
329 dependencies: Vec::new(),
330 defined_types: HashSet::new(),
331 defined_traits: HashSet::new(),
332 defined_functions: HashMap::new(),
333 imported_types: HashMap::new(),
334 seen_dependencies: HashSet::new(),
335 usage_counts: UsageCounts::default(),
336 type_visibility: HashMap::new(),
337 current_item: None,
338 item_dependencies: Vec::new(),
339 }
340 }
341
342 pub fn analyze_file(&mut self, content: &str) -> Result<(), AnalyzerError> {
344 let syntax: File =
345 syn::parse_file(content).map_err(|e| AnalyzerError::ParseError(e.to_string()))?;
346
347 self.visit_file(&syntax);
348
349 Ok(())
350 }
351
352 fn add_dependency(&mut self, path: String, kind: DependencyKind, usage: UsageContext) {
354 let key = (path.clone(), usage);
355 if self.seen_dependencies.contains(&key) {
356 return;
357 }
358 self.seen_dependencies.insert(key);
359
360 self.dependencies.push(Dependency {
361 path,
362 kind,
363 line: 0,
364 usage,
365 });
366 }
367
368 fn add_item_dependency(
370 &mut self,
371 target: String,
372 dep_type: ItemDepType,
373 line: usize,
374 expression: Option<String>,
375 ) {
376 if let Some((ref source_item, source_kind)) = self.current_item {
377 let target_module = self.imported_types.get(&target).cloned().or_else(|| {
379 if self.defined_types.contains(&target)
380 || self.defined_functions.contains_key(&target)
381 {
382 Some(self.current_module.clone())
383 } else {
384 None
385 }
386 });
387
388 self.item_dependencies.push(ItemDependency {
389 source_item: source_item.clone(),
390 source_kind,
391 target,
392 target_module,
393 dep_type,
394 line,
395 expression,
396 });
397 }
398 }
399
400 fn extract_use_paths(&self, tree: &UseTree, prefix: &str) -> Vec<(String, DependencyKind)> {
402 let mut paths = Vec::new();
403
404 match tree {
405 UseTree::Path(path) => {
406 let new_prefix = if prefix.is_empty() {
407 path.ident.to_string()
408 } else {
409 format!("{}::{}", prefix, path.ident)
410 };
411 paths.extend(self.extract_use_paths(&path.tree, &new_prefix));
412 }
413 UseTree::Name(name) => {
414 let full_path = if prefix.is_empty() {
415 name.ident.to_string()
416 } else {
417 format!("{}::{}", prefix, name.ident)
418 };
419 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
420 DependencyKind::InternalUse
421 } else {
422 DependencyKind::ExternalUse
423 };
424 paths.push((full_path, kind));
425 }
426 UseTree::Rename(rename) => {
427 let full_path = if prefix.is_empty() {
428 rename.ident.to_string()
429 } else {
430 format!("{}::{}", prefix, rename.ident)
431 };
432 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
433 DependencyKind::InternalUse
434 } else {
435 DependencyKind::ExternalUse
436 };
437 paths.push((full_path, kind));
438 }
439 UseTree::Glob(_) => {
440 let full_path = format!("{}::*", prefix);
441 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
442 DependencyKind::InternalUse
443 } else {
444 DependencyKind::ExternalUse
445 };
446 paths.push((full_path, kind));
447 }
448 UseTree::Group(group) => {
449 for item in &group.items {
450 paths.extend(self.extract_use_paths(item, prefix));
451 }
452 }
453 }
454
455 paths
456 }
457
458 fn extract_type_name(&self, ty: &Type) -> Option<String> {
460 match ty {
461 Type::Path(type_path) => {
462 let segments: Vec<_> = type_path
463 .path
464 .segments
465 .iter()
466 .map(|s| s.ident.to_string())
467 .collect();
468 Some(segments.join("::"))
469 }
470 Type::Reference(ref_type) => self.extract_type_name(&ref_type.elem),
471 Type::Slice(slice_type) => self.extract_type_name(&slice_type.elem),
472 Type::Array(array_type) => self.extract_type_name(&array_type.elem),
473 Type::Ptr(ptr_type) => self.extract_type_name(&ptr_type.elem),
474 Type::Paren(paren_type) => self.extract_type_name(&paren_type.elem),
475 Type::Group(group_type) => self.extract_type_name(&group_type.elem),
476 _ => None,
477 }
478 }
479
480 fn analyze_signature(&mut self, sig: &Signature) {
482 for arg in &sig.inputs {
484 if let FnArg::Typed(pat_type) = arg
485 && let Some(type_name) = self.extract_type_name(&pat_type.ty)
486 && !self.is_primitive_type(&type_name)
487 {
488 self.add_dependency(
489 type_name,
490 DependencyKind::TypeRef,
491 UsageContext::FunctionParameter,
492 );
493 }
494 }
495
496 if let ReturnType::Type(_, ty) = &sig.output
498 && let Some(type_name) = self.extract_type_name(ty)
499 && !self.is_primitive_type(&type_name)
500 {
501 self.add_dependency(type_name, DependencyKind::TypeRef, UsageContext::ReturnType);
502 }
503 }
504
505 fn is_primitive_type(&self, type_name: &str) -> bool {
507 if matches!(
509 type_name,
510 "bool"
511 | "char"
512 | "str"
513 | "u8"
514 | "u16"
515 | "u32"
516 | "u64"
517 | "u128"
518 | "usize"
519 | "i8"
520 | "i16"
521 | "i32"
522 | "i64"
523 | "i128"
524 | "isize"
525 | "f32"
526 | "f64"
527 | "String"
528 | "Self"
529 | "()"
530 | "Option"
531 | "Result"
532 | "Vec"
533 | "Box"
534 | "Rc"
535 | "Arc"
536 | "RefCell"
537 | "Cell"
538 | "Mutex"
539 | "RwLock"
540 ) {
541 return true;
542 }
543
544 if type_name.len() <= 3 && type_name.chars().all(|c| c.is_lowercase()) {
547 return true;
548 }
549
550 if type_name.starts_with("self") || type_name == "self" {
552 return true;
553 }
554
555 false
556 }
557}
558
559impl<'ast> Visit<'ast> for CouplingAnalyzer {
560 fn visit_item_use(&mut self, node: &'ast ItemUse) {
561 let paths = self.extract_use_paths(&node.tree, "");
562
563 for (path, kind) in paths {
564 if path == "self" || path.starts_with("self::") {
566 continue;
567 }
568
569 if let Some(type_name) = path.split("::").last() {
571 self.imported_types
572 .insert(type_name.to_string(), path.clone());
573 }
574
575 self.add_dependency(path.clone(), kind, UsageContext::Import);
576
577 if kind == DependencyKind::InternalUse {
579 if !self.metrics.internal_deps.contains(&path) {
580 self.metrics.internal_deps.push(path.clone());
581 }
582 } else if kind == DependencyKind::ExternalUse {
583 let crate_name = path.split("::").next().unwrap_or(&path).to_string();
585 if !self.metrics.external_deps.contains(&crate_name) {
586 self.metrics.external_deps.push(crate_name);
587 }
588 }
589 }
590
591 syn::visit::visit_item_use(self, node);
592 }
593
594 fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
595 if let Some((_, trait_path, _)) = &node.trait_ {
596 self.metrics.trait_impl_count += 1;
598
599 let trait_name: String = trait_path
601 .segments
602 .iter()
603 .map(|s| s.ident.to_string())
604 .collect::<Vec<_>>()
605 .join("::");
606
607 self.add_dependency(
608 trait_name,
609 DependencyKind::TraitImpl,
610 UsageContext::TraitBound,
611 );
612 self.usage_counts.trait_bounds += 1;
613 } else {
614 self.metrics.inherent_impl_count += 1;
616
617 if let Some(type_name) = self.extract_type_name(&node.self_ty)
619 && !self.defined_types.contains(&type_name)
620 {
621 self.add_dependency(
622 type_name,
623 DependencyKind::InherentImpl,
624 UsageContext::InherentImplBlock,
625 );
626 }
627 }
628 syn::visit::visit_item_impl(self, node);
629 }
630
631 fn visit_item_fn(&mut self, node: &'ast ItemFn) {
632 let fn_name = node.sig.ident.to_string();
634 let visibility = convert_visibility(&node.vis);
635 self.defined_functions.insert(fn_name.clone(), visibility);
636
637 if has_test_attribute(&node.attrs) {
639 self.metrics.test_function_count += 1;
640 }
641
642 let mut param_count = 0;
644 let mut primitive_param_count = 0;
645 let mut param_types = Vec::new();
646
647 for arg in &node.sig.inputs {
648 if let FnArg::Typed(pat_type) = arg {
649 param_count += 1;
650 if let Some(type_name) = self.extract_type_name(&pat_type.ty) {
651 param_types.push(type_name.clone());
652 if self.is_primitive_type(&type_name) {
653 primitive_param_count += 1;
654 }
655 }
656 }
657 }
658
659 self.metrics.add_function_definition_full(
661 fn_name.clone(),
662 visibility,
663 param_count,
664 primitive_param_count,
665 param_types,
666 );
667
668 let previous_item = self.current_item.take();
670 self.current_item = Some((fn_name, ItemKind::Function));
671
672 self.analyze_signature(&node.sig);
674 syn::visit::visit_item_fn(self, node);
675
676 self.current_item = previous_item;
678 }
679
680 fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
681 let name = node.ident.to_string();
682 let visibility = convert_visibility(&node.vis);
683
684 self.defined_types.insert(name.clone());
685 self.type_visibility.insert(name.clone(), visibility);
686
687 let (is_newtype, inner_type) = match &node.fields {
689 syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
690 let inner = fields
691 .unnamed
692 .first()
693 .and_then(|f| self.extract_type_name(&f.ty));
694 (true, inner)
695 }
696 _ => (false, None),
697 };
698
699 let has_serde_derive = node.attrs.iter().any(|attr| {
701 if attr.path().is_ident("derive")
702 && let Ok(nested) = attr.parse_args_with(
703 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
704 )
705 {
706 return nested.iter().any(|path| {
707 let path_str = path
708 .segments
709 .iter()
710 .map(|s| s.ident.to_string())
711 .collect::<Vec<_>>()
712 .join("::");
713 path_str == "Serialize"
714 || path_str == "Deserialize"
715 || path_str == "serde::Serialize"
716 || path_str == "serde::Deserialize"
717 });
718 }
719 false
720 });
721
722 let (total_field_count, public_field_count) = match &node.fields {
724 syn::Fields::Named(fields) => {
725 let total = fields.named.len();
726 let public = fields
727 .named
728 .iter()
729 .filter(|f| matches!(f.vis, syn::Visibility::Public(_)))
730 .count();
731 (total, public)
732 }
733 syn::Fields::Unnamed(fields) => {
734 let total = fields.unnamed.len();
735 let public = fields
736 .unnamed
737 .iter()
738 .filter(|f| matches!(f.vis, syn::Visibility::Public(_)))
739 .count();
740 (total, public)
741 }
742 syn::Fields::Unit => (0, 0),
743 };
744
745 self.metrics.add_type_definition_full(
747 name,
748 visibility,
749 false, is_newtype,
751 inner_type,
752 has_serde_derive,
753 public_field_count,
754 total_field_count,
755 );
756
757 match &node.fields {
759 syn::Fields::Named(fields) => {
760 self.metrics.type_usage_count += fields.named.len();
761 for field in &fields.named {
762 if let Some(type_name) = self.extract_type_name(&field.ty)
763 && !self.is_primitive_type(&type_name)
764 {
765 self.add_dependency(
766 type_name,
767 DependencyKind::TypeRef,
768 UsageContext::TypeParameter,
769 );
770 self.usage_counts.type_parameters += 1;
771 }
772 }
773 }
774 syn::Fields::Unnamed(fields) => {
775 for field in &fields.unnamed {
776 if let Some(type_name) = self.extract_type_name(&field.ty)
777 && !self.is_primitive_type(&type_name)
778 {
779 self.add_dependency(
780 type_name,
781 DependencyKind::TypeRef,
782 UsageContext::TypeParameter,
783 );
784 }
785 }
786 }
787 syn::Fields::Unit => {}
788 }
789 syn::visit::visit_item_struct(self, node);
790 }
791
792 fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
793 let name = node.ident.to_string();
794 let visibility = convert_visibility(&node.vis);
795
796 self.defined_types.insert(name.clone());
797 self.type_visibility.insert(name.clone(), visibility);
798
799 self.metrics.add_type_definition(name, visibility, false);
801
802 for variant in &node.variants {
804 match &variant.fields {
805 syn::Fields::Named(fields) => {
806 for field in &fields.named {
807 if let Some(type_name) = self.extract_type_name(&field.ty)
808 && !self.is_primitive_type(&type_name)
809 {
810 self.add_dependency(
811 type_name,
812 DependencyKind::TypeRef,
813 UsageContext::TypeParameter,
814 );
815 }
816 }
817 }
818 syn::Fields::Unnamed(fields) => {
819 for field in &fields.unnamed {
820 if let Some(type_name) = self.extract_type_name(&field.ty)
821 && !self.is_primitive_type(&type_name)
822 {
823 self.add_dependency(
824 type_name,
825 DependencyKind::TypeRef,
826 UsageContext::TypeParameter,
827 );
828 }
829 }
830 }
831 syn::Fields::Unit => {}
832 }
833 }
834 syn::visit::visit_item_enum(self, node);
835 }
836
837 fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
838 let name = node.ident.to_string();
839 let visibility = convert_visibility(&node.vis);
840
841 self.defined_traits.insert(name.clone());
842 self.type_visibility.insert(name.clone(), visibility);
843
844 self.metrics.add_type_definition(name, visibility, true);
846
847 self.metrics.trait_impl_count += 1;
848 syn::visit::visit_item_trait(self, node);
849 }
850
851 fn visit_item_mod(&mut self, node: &'ast ItemMod) {
852 if is_test_module(node) {
854 self.metrics.is_test_module = true;
855 }
856
857 if node.content.is_some() {
858 self.metrics.internal_deps.push(node.ident.to_string());
859 }
860 syn::visit::visit_item_mod(self, node);
861 }
862
863 fn visit_expr_field(&mut self, node: &'ast ExprField) {
865 let field_name = match &node.member {
866 syn::Member::Named(ident) => ident.to_string(),
867 syn::Member::Unnamed(idx) => format!("{}", idx.index),
868 };
869
870 if let Expr::Path(path_expr) = &*node.base {
872 let base_name = path_expr
873 .path
874 .segments
875 .iter()
876 .map(|s| s.ident.to_string())
877 .collect::<Vec<_>>()
878 .join("::");
879
880 let full_path = self
882 .imported_types
883 .get(&base_name)
884 .cloned()
885 .unwrap_or(base_name.clone());
886
887 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
888 self.add_dependency(
889 full_path.clone(),
890 DependencyKind::TypeRef,
891 UsageContext::FieldAccess,
892 );
893 self.usage_counts.field_accesses += 1;
894 }
895
896 let expr = format!("{}.{}", base_name, field_name);
898 self.add_item_dependency(
899 format!("{}.{}", full_path, field_name),
900 ItemDepType::FieldAccess,
901 0,
902 Some(expr),
903 );
904 }
905 syn::visit::visit_expr_field(self, node);
906 }
907
908 fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
910 let method_name = node.method.to_string();
911
912 if let Expr::Path(path_expr) = &*node.receiver {
914 let receiver_name = path_expr
915 .path
916 .segments
917 .iter()
918 .map(|s| s.ident.to_string())
919 .collect::<Vec<_>>()
920 .join("::");
921
922 let full_path = self
923 .imported_types
924 .get(&receiver_name)
925 .cloned()
926 .unwrap_or(receiver_name.clone());
927
928 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
929 self.add_dependency(
930 full_path.clone(),
931 DependencyKind::TypeRef,
932 UsageContext::MethodCall,
933 );
934 self.usage_counts.method_calls += 1;
935 }
936
937 let expr = format!("{}.{}()", receiver_name, method_name);
939 self.add_item_dependency(
940 format!("{}::{}", full_path, method_name),
941 ItemDepType::MethodCall,
942 0, Some(expr),
944 );
945 }
946 syn::visit::visit_expr_method_call(self, node);
947 }
948
949 fn visit_expr_call(&mut self, node: &'ast ExprCall) {
951 if let Expr::Path(path_expr) = &*node.func {
952 let path_str = path_expr
953 .path
954 .segments
955 .iter()
956 .map(|s| s.ident.to_string())
957 .collect::<Vec<_>>()
958 .join("::");
959
960 if path_str.contains("::") || path_str.chars().next().is_some_and(|c| c.is_uppercase())
962 {
963 let full_path = self
964 .imported_types
965 .get(&path_str)
966 .cloned()
967 .unwrap_or(path_str.clone());
968
969 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
970 self.add_dependency(
971 full_path.clone(),
972 DependencyKind::TypeRef,
973 UsageContext::FunctionCall,
974 );
975 self.usage_counts.function_calls += 1;
976 }
977
978 self.add_item_dependency(
980 full_path,
981 ItemDepType::FunctionCall,
982 0,
983 Some(format!("{}()", path_str)),
984 );
985 } else {
986 self.add_item_dependency(
988 path_str.clone(),
989 ItemDepType::FunctionCall,
990 0,
991 Some(format!("{}()", path_str)),
992 );
993 }
994 }
995 syn::visit::visit_expr_call(self, node);
996 }
997
998 fn visit_expr_struct(&mut self, node: &'ast ExprStruct) {
1000 let struct_name = node
1001 .path
1002 .segments
1003 .iter()
1004 .map(|s| s.ident.to_string())
1005 .collect::<Vec<_>>()
1006 .join("::");
1007
1008 if struct_name == "Self" || struct_name.starts_with("Self::") {
1010 syn::visit::visit_expr_struct(self, node);
1011 return;
1012 }
1013
1014 let full_path = self
1015 .imported_types
1016 .get(&struct_name)
1017 .cloned()
1018 .unwrap_or(struct_name.clone());
1019
1020 if !self.defined_types.contains(&full_path) && !self.is_primitive_type(&struct_name) {
1021 self.add_dependency(
1022 full_path,
1023 DependencyKind::TypeRef,
1024 UsageContext::StructConstruction,
1025 );
1026 self.usage_counts.struct_constructions += 1;
1027 }
1028 syn::visit::visit_expr_struct(self, node);
1029 }
1030}
1031
1032#[derive(Debug, Clone)]
1034struct AnalyzedFile {
1035 module_name: String,
1036 #[allow(dead_code)]
1037 file_path: PathBuf,
1038 metrics: ModuleMetrics,
1039 dependencies: Vec<Dependency>,
1040 type_visibility: HashMap<String, Visibility>,
1042 item_dependencies: Vec<ItemDependency>,
1044}
1045
1046pub fn analyze_project(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1048 analyze_project_parallel(path)
1049}
1050
1051fn rs_files(dir: &Path) -> impl Iterator<Item = PathBuf> {
1057 WalkDir::new(dir)
1058 .follow_links(true)
1059 .into_iter()
1060 .filter_map(|e| e.ok())
1061 .filter(move |entry| {
1062 let file_path = entry.path();
1063 let file_path = file_path.strip_prefix(dir).unwrap_or(file_path);
1068
1069 !file_path.components().any(|c| {
1071 let s = c.as_os_str().to_string_lossy();
1072 s == "target" || s.starts_with('.')
1073 }) && file_path.extension() == Some(OsStr::new("rs"))
1074 })
1075 .map(|e| e.path().to_path_buf())
1076}
1077
1078pub fn analyze_project_parallel(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1083 if !path.exists() {
1084 return Err(AnalyzerError::InvalidPath(path.display().to_string()));
1085 }
1086
1087 let file_paths: Vec<PathBuf> = rs_files(path).collect();
1089
1090 let num_threads = rayon::current_num_threads();
1094 let file_count = file_paths.len();
1095
1096 let chunk_size = if file_count < num_threads * 2 {
1099 1 } else {
1101 (file_count / (num_threads * 4)).max(1)
1104 };
1105
1106 let analyzed_results: Vec<_> = file_paths
1108 .par_chunks(chunk_size)
1109 .flat_map(|chunk| {
1110 chunk
1111 .iter()
1112 .filter_map(|file_path| match analyze_rust_file_full(file_path) {
1113 Ok(result) => {
1114 let module_path = file_path_to_module_path(file_path, path);
1116 let old_name = result.metrics.name.clone();
1117 let module_name = if module_path.is_empty() {
1118 old_name.clone()
1120 } else {
1121 module_path
1122 };
1123
1124 let item_dependencies = result
1126 .item_dependencies
1127 .into_iter()
1128 .map(|mut dep| {
1129 if dep.target_module.as_ref() == Some(&old_name) {
1130 dep.target_module = Some(module_name.clone());
1131 }
1132 dep
1133 })
1134 .collect();
1135
1136 Some(AnalyzedFile {
1137 module_name: module_name.clone(),
1138 file_path: file_path.clone(),
1139 metrics: {
1140 let mut m = result.metrics;
1141 m.name = module_name;
1142 m
1143 },
1144 dependencies: result.dependencies,
1145 type_visibility: result.type_visibility,
1146 item_dependencies,
1147 })
1148 }
1149 Err(e) => {
1150 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1151 None
1152 }
1153 })
1154 .collect::<Vec<_>>()
1155 })
1156 .collect();
1157
1158 let module_names: HashSet<String> = analyzed_results
1160 .iter()
1161 .map(|a| a.module_name.clone())
1162 .collect();
1163
1164 let mut project = ProjectMetrics::new();
1166 project.total_files = analyzed_results.len();
1167
1168 for analyzed in &analyzed_results {
1170 for (type_name, visibility) in &analyzed.type_visibility {
1171 project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
1172 }
1173 }
1174
1175 for analyzed in &analyzed_results {
1177 let mut metrics = analyzed.metrics.clone();
1179 metrics.item_dependencies = analyzed.item_dependencies.clone();
1180 project.add_module(metrics);
1181
1182 for dep in &analyzed.dependencies {
1183 if !is_valid_dependency_path(&dep.path) {
1185 continue;
1186 }
1187
1188 let target_module = extract_target_module(&dep.path);
1190
1191 if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1193 continue;
1194 }
1195
1196 let distance = calculate_distance(&dep.path, &module_names);
1198
1199 let strength = dep.usage.to_strength();
1201
1202 let volatility = Volatility::Low;
1204
1205 let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
1207 let visibility = project
1208 .get_type_visibility(target_type)
1209 .unwrap_or(Visibility::Public); let coupling = CouplingMetrics::with_location(
1213 analyzed.module_name.clone(),
1214 target_module.clone(),
1215 strength,
1216 distance,
1217 volatility,
1218 visibility,
1219 analyzed.file_path.clone(),
1220 dep.line,
1221 );
1222
1223 project.add_coupling(coupling);
1224 }
1225 }
1226
1227 project.update_coupling_visibility();
1229
1230 Ok(project)
1231}
1232
1233pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1235 let workspace = match WorkspaceInfo::from_path(path) {
1237 Ok(ws) => Some(ws),
1238 Err(e) => {
1239 eprintln!("Note: Could not load workspace metadata: {}", e);
1240 eprintln!("Falling back to basic analysis...");
1241 None
1242 }
1243 };
1244
1245 if let Some(ws) = workspace {
1246 analyze_with_workspace(path, &ws)
1247 } else {
1248 analyze_project(path)
1250 }
1251}
1252
1253fn analyze_with_workspace(
1255 _path: &Path,
1256 workspace: &WorkspaceInfo,
1257) -> Result<ProjectMetrics, AnalyzerError> {
1258 let mut project = ProjectMetrics::new();
1259
1260 project.workspace_name = Some(
1262 workspace
1263 .root
1264 .file_name()
1265 .and_then(|n| n.to_str())
1266 .unwrap_or("workspace")
1267 .to_string(),
1268 );
1269 project.workspace_members = workspace.members.clone();
1270
1271 let mut file_crate_pairs: Vec<(PathBuf, String, PathBuf)> = Vec::new();
1274
1275 for member_name in &workspace.members {
1276 if let Some(crate_info) = workspace.get_crate(member_name) {
1277 if !crate_info.src_path.exists() {
1278 continue;
1279 }
1280
1281 let src_root = crate_info.src_path.clone();
1282 for file_path in rs_files(&crate_info.src_path) {
1283 file_crate_pairs.push((
1284 file_path.to_path_buf(),
1285 member_name.clone(),
1286 src_root.clone(),
1287 ));
1288 }
1289 }
1290 }
1291
1292 let num_threads = rayon::current_num_threads();
1294 let file_count = file_crate_pairs.len();
1295 let chunk_size = if file_count < num_threads * 2 {
1296 1
1297 } else {
1298 (file_count / (num_threads * 4)).max(1)
1299 };
1300
1301 let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1303 .par_chunks(chunk_size)
1304 .flat_map(|chunk| {
1305 chunk
1306 .iter()
1307 .filter_map(|(file_path, crate_name, src_root)| {
1308 match analyze_rust_file_full(file_path) {
1309 Ok(result) => {
1310 let module_path = file_path_to_module_path(file_path, src_root);
1312 let old_name = result.metrics.name.clone();
1313 let module_name = if module_path.is_empty() {
1314 old_name.clone()
1316 } else {
1317 module_path
1318 };
1319
1320 let item_dependencies = result
1322 .item_dependencies
1323 .into_iter()
1324 .map(|mut dep| {
1325 if dep.target_module.as_ref() == Some(&old_name) {
1326 dep.target_module = Some(module_name.clone());
1327 }
1328 dep
1329 })
1330 .collect();
1331
1332 Some(AnalyzedFileWithCrate {
1333 module_name: module_name.clone(),
1334 crate_name: crate_name.clone(),
1335 file_path: file_path.clone(),
1336 metrics: {
1337 let mut m = result.metrics;
1338 m.name = module_name;
1339 m
1340 },
1341 dependencies: result.dependencies,
1342 item_dependencies,
1343 })
1344 }
1345 Err(e) => {
1346 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1347 None
1348 }
1349 }
1350 })
1351 .collect::<Vec<_>>()
1352 })
1353 .collect();
1354
1355 project.total_files = analyzed_files.len();
1356
1357 let module_names: HashSet<String> = analyzed_files
1359 .iter()
1360 .map(|a| a.module_name.clone())
1361 .collect();
1362
1363 for analyzed in &analyzed_files {
1365 let mut metrics = analyzed.metrics.clone();
1367 metrics.item_dependencies = analyzed.item_dependencies.clone();
1368 project.add_module(metrics);
1369
1370 for dep in &analyzed.dependencies {
1371 if !is_valid_dependency_path(&dep.path) {
1373 continue;
1374 }
1375
1376 let resolved_crate =
1378 resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1379
1380 let target_module = extract_target_module(&dep.path);
1381
1382 if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1384 continue;
1385 }
1386
1387 let distance =
1389 calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1390
1391 let strength = dep.usage.to_strength();
1393
1394 let volatility = Volatility::Low;
1396
1397 let mut coupling = CouplingMetrics::with_location(
1399 format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1400 if let Some(ref crate_name) = resolved_crate {
1401 format!("{}::{}", crate_name, target_module)
1402 } else {
1403 target_module.clone()
1404 },
1405 strength,
1406 distance,
1407 volatility,
1408 Visibility::Public, analyzed.file_path.clone(),
1410 dep.line,
1411 );
1412
1413 coupling.source_crate = Some(analyzed.crate_name.clone());
1415 coupling.target_crate = resolved_crate;
1416
1417 project.add_coupling(coupling);
1418 }
1419 }
1420
1421 for (crate_name, deps) in &workspace.dependency_graph {
1423 if workspace.is_workspace_member(crate_name) {
1424 for dep in deps {
1425 project
1427 .crate_dependencies
1428 .entry(crate_name.clone())
1429 .or_default()
1430 .push(dep.clone());
1431 }
1432 }
1433 }
1434
1435 Ok(project)
1436}
1437
1438fn calculate_distance_with_workspace(
1440 dep_path: &str,
1441 current_crate: &str,
1442 workspace: &WorkspaceInfo,
1443) -> Distance {
1444 if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1445 Distance::SameModule
1447 } else if dep_path.starts_with("super::") {
1448 Distance::DifferentModule
1450 } else {
1451 if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1453 if target_crate == current_crate {
1454 Distance::SameModule
1455 } else if workspace.is_workspace_member(&target_crate) {
1456 Distance::DifferentModule
1458 } else {
1459 Distance::DifferentCrate
1461 }
1462 } else {
1463 Distance::DifferentCrate
1464 }
1465 }
1466}
1467
1468#[derive(Debug, Clone)]
1470struct AnalyzedFileWithCrate {
1471 module_name: String,
1472 crate_name: String,
1473 #[allow(dead_code)]
1474 file_path: PathBuf,
1475 metrics: ModuleMetrics,
1476 dependencies: Vec<Dependency>,
1477 item_dependencies: Vec<ItemDependency>,
1479}
1480
1481fn extract_target_module(path: &str) -> String {
1483 let cleaned = path
1485 .trim_start_matches("crate::")
1486 .trim_start_matches("super::")
1487 .trim_start_matches("::");
1488
1489 cleaned.split("::").next().unwrap_or(path).to_string()
1491}
1492
1493fn is_valid_dependency_path(path: &str) -> bool {
1495 if path.is_empty() {
1497 return false;
1498 }
1499
1500 if path == "Self" || path.starts_with("Self::") {
1502 return false;
1503 }
1504
1505 let segments: Vec<&str> = path.split("::").collect();
1506
1507 if segments.len() == 1 {
1509 let name = segments[0];
1510 if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1511 return false;
1512 }
1513 }
1514
1515 if segments.len() >= 2 {
1517 let last = segments.last().unwrap();
1518 let second_last = segments.get(segments.len() - 2).unwrap();
1519 if last == second_last {
1520 return false;
1521 }
1522 }
1523
1524 let last_segment = segments.last().unwrap_or(&path);
1526 let common_locals = [
1527 "request",
1528 "response",
1529 "result",
1530 "content",
1531 "config",
1532 "proto",
1533 "domain",
1534 "info",
1535 "data",
1536 "item",
1537 "value",
1538 "error",
1539 "message",
1540 "expected",
1541 "actual",
1542 "status",
1543 "state",
1544 "context",
1545 "params",
1546 "args",
1547 "options",
1548 "settings",
1549 "violation",
1550 "page_token",
1551 ];
1552 if common_locals.contains(last_segment) && segments.len() <= 2 {
1553 return false;
1554 }
1555
1556 true
1557}
1558
1559fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1561 if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1562 Distance::DifferentModule
1564 } else if dep_path.starts_with("self::") {
1565 Distance::SameModule
1566 } else {
1567 Distance::DifferentCrate
1569 }
1570}
1571
1572pub struct AnalyzedFileResult {
1575 pub metrics: ModuleMetrics,
1576 pub dependencies: Vec<Dependency>,
1577 pub type_visibility: HashMap<String, Visibility>,
1578 pub item_dependencies: Vec<ItemDependency>,
1579}
1580
1581pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1582 let result = analyze_rust_file_full(path)?;
1583 Ok((result.metrics, result.dependencies))
1584}
1585
1586pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1588 let content = fs::read_to_string(path)?;
1589
1590 let module_name = path
1591 .file_stem()
1592 .and_then(|s| s.to_str())
1593 .unwrap_or("unknown")
1594 .to_string();
1595
1596 let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1597 analyzer.analyze_file(&content)?;
1598
1599 Ok(AnalyzedFileResult {
1600 metrics: analyzer.metrics,
1601 dependencies: analyzer.dependencies,
1602 type_visibility: analyzer.type_visibility,
1603 item_dependencies: analyzer.item_dependencies,
1604 })
1605}
1606
1607#[cfg(test)]
1608mod tests {
1609 use super::*;
1610
1611 #[test]
1612 fn test_analyzer_creation() {
1613 let analyzer = CouplingAnalyzer::new(
1614 "test_module".to_string(),
1615 std::path::PathBuf::from("test.rs"),
1616 );
1617 assert_eq!(analyzer.current_module, "test_module");
1618 }
1619
1620 #[test]
1621 fn test_analyze_simple_file() {
1622 let mut analyzer =
1623 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1624
1625 let code = r#"
1626 pub struct User {
1627 name: String,
1628 email: String,
1629 }
1630
1631 impl User {
1632 pub fn new(name: String, email: String) -> Self {
1633 Self { name, email }
1634 }
1635 }
1636 "#;
1637
1638 let result = analyzer.analyze_file(code);
1639 assert!(result.is_ok());
1640 assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1641 }
1642
1643 #[test]
1644 fn test_item_dependencies() {
1645 let mut analyzer =
1646 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1647
1648 let code = r#"
1649 pub struct Config {
1650 pub value: i32,
1651 }
1652
1653 pub fn process(config: Config) -> i32 {
1654 let x = config.value;
1655 helper(x)
1656 }
1657
1658 fn helper(n: i32) -> i32 {
1659 n * 2
1660 }
1661 "#;
1662
1663 let result = analyzer.analyze_file(code);
1664 assert!(result.is_ok());
1665
1666 assert!(analyzer.defined_functions.contains_key("process"));
1668 assert!(analyzer.defined_functions.contains_key("helper"));
1669
1670 println!(
1672 "Item dependencies count: {}",
1673 analyzer.item_dependencies.len()
1674 );
1675 for dep in &analyzer.item_dependencies {
1676 println!(
1677 " {} -> {} ({:?})",
1678 dep.source_item, dep.target, dep.dep_type
1679 );
1680 }
1681
1682 let process_deps: Vec<_> = analyzer
1684 .item_dependencies
1685 .iter()
1686 .filter(|d| d.source_item == "process")
1687 .collect();
1688
1689 assert!(
1690 !process_deps.is_empty(),
1691 "process function should have item dependencies"
1692 );
1693 }
1694
1695 #[test]
1696 fn test_analyze_trait_impl() {
1697 let mut analyzer =
1698 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1699
1700 let code = r#"
1701 trait Printable {
1702 fn print(&self);
1703 }
1704
1705 struct Document;
1706
1707 impl Printable for Document {
1708 fn print(&self) {}
1709 }
1710 "#;
1711
1712 let result = analyzer.analyze_file(code);
1713 assert!(result.is_ok());
1714 assert!(analyzer.metrics.trait_impl_count >= 1);
1715 }
1716
1717 #[test]
1718 fn test_analyze_use_statements() {
1719 let mut analyzer =
1720 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1721
1722 let code = r#"
1723 use std::collections::HashMap;
1724 use serde::Serialize;
1725 use crate::utils;
1726 use crate::models::{User, Post};
1727 "#;
1728
1729 let result = analyzer.analyze_file(code);
1730 assert!(result.is_ok());
1731 assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1732 assert!(
1733 analyzer
1734 .metrics
1735 .external_deps
1736 .contains(&"serde".to_string())
1737 );
1738 assert!(!analyzer.dependencies.is_empty());
1739
1740 let internal_deps: Vec<_> = analyzer
1742 .dependencies
1743 .iter()
1744 .filter(|d| d.kind == DependencyKind::InternalUse)
1745 .collect();
1746 assert!(!internal_deps.is_empty());
1747 }
1748
1749 #[test]
1750 fn test_extract_use_paths() {
1751 let analyzer =
1752 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1753
1754 let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1756 let paths = analyzer.extract_use_paths(&tree, "");
1757 assert_eq!(paths.len(), 1);
1758 assert_eq!(paths[0].0, "std::collections::HashMap");
1759
1760 let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1762 let paths = analyzer.extract_use_paths(&tree, "");
1763 assert_eq!(paths.len(), 2);
1764 }
1765
1766 #[test]
1767 fn test_extract_target_module() {
1768 assert_eq!(extract_target_module("crate::models::user"), "models");
1769 assert_eq!(extract_target_module("super::utils"), "utils");
1770 assert_eq!(extract_target_module("std::collections"), "std");
1771 }
1772
1773 #[test]
1774 fn test_field_access_detection() {
1775 let mut analyzer =
1776 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1777
1778 let code = r#"
1779 use crate::models::User;
1780
1781 fn get_name(user: &User) -> String {
1782 user.name.clone()
1783 }
1784 "#;
1785
1786 let result = analyzer.analyze_file(code);
1787 assert!(result.is_ok());
1788
1789 let _field_deps: Vec<_> = analyzer
1791 .dependencies
1792 .iter()
1793 .filter(|d| d.usage == UsageContext::FieldAccess)
1794 .collect();
1795 }
1798
1799 #[test]
1800 fn test_method_call_detection() {
1801 let mut analyzer =
1802 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1803
1804 let code = r#"
1805 fn process() {
1806 let data = String::new();
1807 data.push_str("hello");
1808 }
1809 "#;
1810
1811 let result = analyzer.analyze_file(code);
1812 assert!(result.is_ok());
1813 }
1815
1816 #[test]
1817 fn test_struct_construction_detection() {
1818 let mut analyzer =
1819 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1820
1821 let code = r#"
1822 use crate::config::Config;
1823
1824 fn create_config() {
1825 let c = Config { value: 42 };
1826 }
1827 "#;
1828
1829 let result = analyzer.analyze_file(code);
1830 assert!(result.is_ok());
1831
1832 let struct_deps: Vec<_> = analyzer
1834 .dependencies
1835 .iter()
1836 .filter(|d| d.usage == UsageContext::StructConstruction)
1837 .collect();
1838 assert!(!struct_deps.is_empty());
1839 }
1840
1841 #[test]
1842 fn test_usage_context_to_strength() {
1843 assert_eq!(
1844 UsageContext::FieldAccess.to_strength(),
1845 IntegrationStrength::Intrusive
1846 );
1847 assert_eq!(
1848 UsageContext::MethodCall.to_strength(),
1849 IntegrationStrength::Functional
1850 );
1851 assert_eq!(
1852 UsageContext::TypeParameter.to_strength(),
1853 IntegrationStrength::Model
1854 );
1855 assert_eq!(
1856 UsageContext::TraitBound.to_strength(),
1857 IntegrationStrength::Contract
1858 );
1859 }
1860
1861 #[test]
1864 fn test_rs_files_with_hidden_parent_directory() {
1865 use std::fs;
1866 use tempfile::TempDir;
1867
1868 let temp = TempDir::new().unwrap();
1871 let hidden_parent = temp.path().join(".hidden-parent");
1872 let project_dir = hidden_parent.join("myproject").join("src");
1873 fs::create_dir_all(&project_dir).unwrap();
1874
1875 fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1877 fs::write(project_dir.join("main.rs"), "fn main() {}").unwrap();
1878
1879 let files: Vec<_> = rs_files(&project_dir).collect();
1881 assert_eq!(
1882 files.len(),
1883 2,
1884 "Should find 2 .rs files in hidden parent path"
1885 );
1886
1887 let file_names: Vec<_> = files
1889 .iter()
1890 .filter_map(|p| p.file_name())
1891 .filter_map(|n| n.to_str())
1892 .collect();
1893 assert!(file_names.contains(&"lib.rs"));
1894 assert!(file_names.contains(&"main.rs"));
1895 }
1896
1897 #[test]
1899 fn test_rs_files_excludes_hidden_dirs_in_project() {
1900 use std::fs;
1901 use tempfile::TempDir;
1902
1903 let temp = TempDir::new().unwrap();
1904 let project_dir = temp.path().join("myproject").join("src");
1905 let hidden_dir = project_dir.join(".hidden");
1906 fs::create_dir_all(&hidden_dir).unwrap();
1907
1908 fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1910 fs::write(hidden_dir.join("secret.rs"), "fn secret() {}").unwrap();
1911
1912 let files: Vec<_> = rs_files(&project_dir).collect();
1914 assert_eq!(
1915 files.len(),
1916 1,
1917 "Should find only 1 .rs file (excluding .hidden/)"
1918 );
1919
1920 let file_names: Vec<_> = files
1921 .iter()
1922 .filter_map(|p| p.file_name())
1923 .filter_map(|n| n.to_str())
1924 .collect();
1925 assert!(file_names.contains(&"lib.rs"));
1926 assert!(!file_names.contains(&"secret.rs"));
1927 }
1928
1929 #[test]
1931 fn test_rs_files_excludes_target_directory() {
1932 use std::fs;
1933 use tempfile::TempDir;
1934
1935 let temp = TempDir::new().unwrap();
1936 let project_dir = temp.path().join("myproject");
1937 let src_dir = project_dir.join("src");
1938 let target_dir = project_dir.join("target").join("debug");
1939 fs::create_dir_all(&src_dir).unwrap();
1940 fs::create_dir_all(&target_dir).unwrap();
1941
1942 fs::write(src_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1944 fs::write(target_dir.join("generated.rs"), "// generated").unwrap();
1945
1946 let files: Vec<_> = rs_files(&project_dir).collect();
1948 assert_eq!(
1949 files.len(),
1950 1,
1951 "Should find only 1 .rs file (excluding target/)"
1952 );
1953
1954 let file_names: Vec<_> = files
1955 .iter()
1956 .filter_map(|p| p.file_name())
1957 .filter_map(|n| n.to_str())
1958 .collect();
1959 assert!(file_names.contains(&"lib.rs"));
1960 assert!(!file_names.contains(&"generated.rs"));
1961 }
1962
1963 #[test]
1964 fn test_file_path_to_module_path_nested() {
1965 let src_root = Path::new("/project/src");
1967 let file_path = Path::new("/project/src/level/enemy/spawner.rs");
1968 assert_eq!(
1969 file_path_to_module_path(file_path, src_root),
1970 "level::enemy::spawner"
1971 );
1972 }
1973
1974 #[test]
1975 fn test_file_path_to_module_path_lib() {
1976 let src_root = Path::new("/project/src");
1978 let file_path = Path::new("/project/src/lib.rs");
1979 assert_eq!(file_path_to_module_path(file_path, src_root), "");
1980 }
1981
1982 #[test]
1983 fn test_file_path_to_module_path_main() {
1984 let src_root = Path::new("/project/src");
1986 let file_path = Path::new("/project/src/main.rs");
1987 assert_eq!(file_path_to_module_path(file_path, src_root), "");
1988 }
1989
1990 #[test]
1991 fn test_file_path_to_module_path_mod() {
1992 let src_root = Path::new("/project/src");
1994 let file_path = Path::new("/project/src/level/mod.rs");
1995 assert_eq!(file_path_to_module_path(file_path, src_root), "level");
1996 }
1997
1998 #[test]
1999 fn test_file_path_to_module_path_deeply_nested_mod() {
2000 let src_root = Path::new("/project/src");
2002 let file_path = Path::new("/project/src/a/b/c/mod.rs");
2003 assert_eq!(file_path_to_module_path(file_path, src_root), "a::b::c");
2004 }
2005
2006 #[test]
2007 fn test_file_path_to_module_path_simple() {
2008 let src_root = Path::new("/project/src");
2010 let file_path = Path::new("/project/src/utils.rs");
2011 assert_eq!(file_path_to_module_path(file_path, src_root), "utils");
2012 }
2013
2014 #[test]
2015 fn test_file_path_to_module_path_two_levels() {
2016 let src_root = Path::new("/project/src");
2018 let file_path = Path::new("/project/src/foo/bar.rs");
2019 assert_eq!(file_path_to_module_path(file_path, src_root), "foo::bar");
2020 }
2021
2022 #[test]
2023 fn test_file_path_to_module_path_bin() {
2024 let src_root = Path::new("/project/src");
2026 let file_path = Path::new("/project/src/bin/cli.rs");
2027 assert_eq!(file_path_to_module_path(file_path, src_root), "bin::cli");
2028 }
2029
2030 #[test]
2031 fn test_file_path_to_module_path_mismatched_root() {
2032 let src_root = Path::new("/other/src");
2035 let file_path = Path::new("/project/src/utils.rs");
2036 let result = file_path_to_module_path(file_path, src_root);
2038 assert!(result.contains("utils"));
2040 }
2041
2042 #[test]
2043 fn test_has_test_attribute_with_test() {
2044 let code = r#"
2045 #[test]
2046 fn my_test() {}
2047 "#;
2048 let syntax: syn::File = syn::parse_str(code).unwrap();
2049 if let syn::Item::Fn(func) = &syntax.items[0] {
2050 assert!(has_test_attribute(&func.attrs));
2051 } else {
2052 panic!("Expected function");
2053 }
2054 }
2055
2056 #[test]
2057 fn test_has_test_attribute_without_test() {
2058 let code = r#"
2059 fn regular_fn() {}
2060 "#;
2061 let syntax: syn::File = syn::parse_str(code).unwrap();
2062 if let syn::Item::Fn(func) = &syntax.items[0] {
2063 assert!(!has_test_attribute(&func.attrs));
2064 } else {
2065 panic!("Expected function");
2066 }
2067 }
2068
2069 #[test]
2070 fn test_has_cfg_test_attribute_with_cfg_test() {
2071 let code = r#"
2072 #[cfg(test)]
2073 mod tests {}
2074 "#;
2075 let syntax: syn::File = syn::parse_str(code).unwrap();
2076 if let syn::Item::Mod(module) = &syntax.items[0] {
2077 assert!(has_cfg_test_attribute(&module.attrs));
2078 } else {
2079 panic!("Expected module");
2080 }
2081 }
2082
2083 #[test]
2084 fn test_has_cfg_test_attribute_without_cfg_test() {
2085 let code = r#"
2086 mod regular_mod {}
2087 "#;
2088 let syntax: syn::File = syn::parse_str(code).unwrap();
2089 if let syn::Item::Mod(module) = &syntax.items[0] {
2090 assert!(!has_cfg_test_attribute(&module.attrs));
2091 } else {
2092 panic!("Expected module");
2093 }
2094 }
2095
2096 #[test]
2097 fn test_has_cfg_test_attribute_with_other_cfg() {
2098 let code = r#"
2099 #[cfg(feature = "foo")]
2100 mod feature_mod {}
2101 "#;
2102 let syntax: syn::File = syn::parse_str(code).unwrap();
2103 if let syn::Item::Mod(module) = &syntax.items[0] {
2104 assert!(!has_cfg_test_attribute(&module.attrs));
2105 } else {
2106 panic!("Expected module");
2107 }
2108 }
2109
2110 #[test]
2111 fn test_is_test_module_named_tests() {
2112 let code = r#"
2113 mod tests {}
2114 "#;
2115 let syntax: syn::File = syn::parse_str(code).unwrap();
2116 if let syn::Item::Mod(module) = &syntax.items[0] {
2117 assert!(is_test_module(module));
2118 } else {
2119 panic!("Expected module");
2120 }
2121 }
2122
2123 #[test]
2124 fn test_is_test_module_with_cfg_test() {
2125 let code = r#"
2126 #[cfg(test)]
2127 mod my_tests {}
2128 "#;
2129 let syntax: syn::File = syn::parse_str(code).unwrap();
2130 if let syn::Item::Mod(module) = &syntax.items[0] {
2131 assert!(is_test_module(module));
2132 } else {
2133 panic!("Expected module");
2134 }
2135 }
2136
2137 #[test]
2138 fn test_is_test_module_regular_module() {
2139 let code = r#"
2140 mod utils {}
2141 "#;
2142 let syntax: syn::File = syn::parse_str(code).unwrap();
2143 if let syn::Item::Mod(module) = &syntax.items[0] {
2144 assert!(!is_test_module(module));
2145 } else {
2146 panic!("Expected module");
2147 }
2148 }
2149}