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
76#[derive(Error, Debug)]
78pub enum AnalyzerError {
79 #[error("Failed to read file: {0}")]
80 IoError(#[from] std::io::Error),
81
82 #[error("Failed to parse Rust file: {0}")]
83 ParseError(String),
84
85 #[error("Invalid path: {0}")]
86 InvalidPath(String),
87
88 #[error("Workspace error: {0}")]
89 WorkspaceError(#[from] WorkspaceError),
90}
91
92#[derive(Debug, Clone)]
94pub struct Dependency {
95 pub path: String,
97 pub kind: DependencyKind,
99 pub line: usize,
101 pub usage: UsageContext,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum DependencyKind {
108 InternalUse,
110 ExternalUse,
112 TraitImpl,
114 InherentImpl,
116 TypeRef,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122pub enum UsageContext {
123 Import,
125 TraitBound,
127 FieldAccess,
129 MethodCall,
131 FunctionCall,
133 StructConstruction,
135 TypeParameter,
137 FunctionParameter,
139 ReturnType,
141 InherentImplBlock,
143}
144
145impl UsageContext {
146 pub fn to_strength(&self) -> IntegrationStrength {
148 match self {
149 UsageContext::FieldAccess => IntegrationStrength::Intrusive,
151 UsageContext::StructConstruction => IntegrationStrength::Intrusive,
152 UsageContext::InherentImplBlock => IntegrationStrength::Intrusive,
153
154 UsageContext::MethodCall => IntegrationStrength::Functional,
156 UsageContext::FunctionCall => IntegrationStrength::Functional,
157 UsageContext::FunctionParameter => IntegrationStrength::Functional,
158 UsageContext::ReturnType => IntegrationStrength::Functional,
159
160 UsageContext::TypeParameter => IntegrationStrength::Model,
162 UsageContext::Import => IntegrationStrength::Model,
163
164 UsageContext::TraitBound => IntegrationStrength::Contract,
166 }
167 }
168}
169
170impl DependencyKind {
171 pub fn to_strength(&self) -> IntegrationStrength {
172 match self {
173 DependencyKind::TraitImpl => IntegrationStrength::Contract,
174 DependencyKind::InternalUse => IntegrationStrength::Model,
175 DependencyKind::ExternalUse => IntegrationStrength::Model,
176 DependencyKind::TypeRef => IntegrationStrength::Model,
177 DependencyKind::InherentImpl => IntegrationStrength::Intrusive,
178 }
179 }
180}
181
182#[derive(Debug)]
184pub struct CouplingAnalyzer {
185 pub current_module: String,
187 pub file_path: std::path::PathBuf,
189 pub metrics: ModuleMetrics,
191 pub dependencies: Vec<Dependency>,
193 pub defined_types: HashSet<String>,
195 pub defined_traits: HashSet<String>,
197 pub defined_functions: HashMap<String, Visibility>,
199 imported_types: HashMap<String, String>,
201 seen_dependencies: HashSet<(String, UsageContext)>,
203 pub usage_counts: UsageCounts,
205 pub type_visibility: HashMap<String, Visibility>,
207 current_item: Option<(String, ItemKind)>,
209 pub item_dependencies: Vec<ItemDependency>,
211}
212
213#[derive(Debug, Default, Clone)]
215pub struct UsageCounts {
216 pub field_accesses: usize,
217 pub method_calls: usize,
218 pub function_calls: usize,
219 pub struct_constructions: usize,
220 pub trait_bounds: usize,
221 pub type_parameters: usize,
222}
223
224#[derive(Debug, Clone)]
226pub struct ItemDependency {
227 pub source_item: String,
229 pub source_kind: ItemKind,
231 pub target: String,
233 pub target_module: Option<String>,
235 pub dep_type: ItemDepType,
237 pub line: usize,
239 pub expression: Option<String>,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum ItemKind {
246 Function,
247 Method,
248 Struct,
249 Enum,
250 Trait,
251 Impl,
252 Module,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum ItemDepType {
258 FunctionCall,
260 MethodCall,
262 TypeUsage,
264 FieldAccess,
266 StructConstruction,
268 TraitImpl,
270 TraitBound,
272 Import,
274}
275
276impl CouplingAnalyzer {
277 pub fn new(module_name: String, path: std::path::PathBuf) -> Self {
279 Self {
280 current_module: module_name.clone(),
281 file_path: path.clone(),
282 metrics: ModuleMetrics::new(path, module_name),
283 dependencies: Vec::new(),
284 defined_types: HashSet::new(),
285 defined_traits: HashSet::new(),
286 defined_functions: HashMap::new(),
287 imported_types: HashMap::new(),
288 seen_dependencies: HashSet::new(),
289 usage_counts: UsageCounts::default(),
290 type_visibility: HashMap::new(),
291 current_item: None,
292 item_dependencies: Vec::new(),
293 }
294 }
295
296 pub fn analyze_file(&mut self, content: &str) -> Result<(), AnalyzerError> {
298 let syntax: File =
299 syn::parse_file(content).map_err(|e| AnalyzerError::ParseError(e.to_string()))?;
300
301 self.visit_file(&syntax);
302
303 Ok(())
304 }
305
306 fn add_dependency(&mut self, path: String, kind: DependencyKind, usage: UsageContext) {
308 let key = (path.clone(), usage);
309 if self.seen_dependencies.contains(&key) {
310 return;
311 }
312 self.seen_dependencies.insert(key);
313
314 self.dependencies.push(Dependency {
315 path,
316 kind,
317 line: 0,
318 usage,
319 });
320 }
321
322 fn add_item_dependency(
324 &mut self,
325 target: String,
326 dep_type: ItemDepType,
327 line: usize,
328 expression: Option<String>,
329 ) {
330 if let Some((ref source_item, source_kind)) = self.current_item {
331 let target_module = self.imported_types.get(&target).cloned().or_else(|| {
333 if self.defined_types.contains(&target)
334 || self.defined_functions.contains_key(&target)
335 {
336 Some(self.current_module.clone())
337 } else {
338 None
339 }
340 });
341
342 self.item_dependencies.push(ItemDependency {
343 source_item: source_item.clone(),
344 source_kind,
345 target,
346 target_module,
347 dep_type,
348 line,
349 expression,
350 });
351 }
352 }
353
354 fn extract_use_paths(&self, tree: &UseTree, prefix: &str) -> Vec<(String, DependencyKind)> {
356 let mut paths = Vec::new();
357
358 match tree {
359 UseTree::Path(path) => {
360 let new_prefix = if prefix.is_empty() {
361 path.ident.to_string()
362 } else {
363 format!("{}::{}", prefix, path.ident)
364 };
365 paths.extend(self.extract_use_paths(&path.tree, &new_prefix));
366 }
367 UseTree::Name(name) => {
368 let full_path = if prefix.is_empty() {
369 name.ident.to_string()
370 } else {
371 format!("{}::{}", prefix, name.ident)
372 };
373 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
374 DependencyKind::InternalUse
375 } else {
376 DependencyKind::ExternalUse
377 };
378 paths.push((full_path, kind));
379 }
380 UseTree::Rename(rename) => {
381 let full_path = if prefix.is_empty() {
382 rename.ident.to_string()
383 } else {
384 format!("{}::{}", prefix, rename.ident)
385 };
386 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
387 DependencyKind::InternalUse
388 } else {
389 DependencyKind::ExternalUse
390 };
391 paths.push((full_path, kind));
392 }
393 UseTree::Glob(_) => {
394 let full_path = format!("{}::*", prefix);
395 let kind = if prefix.starts_with("crate") || prefix.starts_with("super") {
396 DependencyKind::InternalUse
397 } else {
398 DependencyKind::ExternalUse
399 };
400 paths.push((full_path, kind));
401 }
402 UseTree::Group(group) => {
403 for item in &group.items {
404 paths.extend(self.extract_use_paths(item, prefix));
405 }
406 }
407 }
408
409 paths
410 }
411
412 fn extract_type_name(&self, ty: &Type) -> Option<String> {
414 match ty {
415 Type::Path(type_path) => {
416 let segments: Vec<_> = type_path
417 .path
418 .segments
419 .iter()
420 .map(|s| s.ident.to_string())
421 .collect();
422 Some(segments.join("::"))
423 }
424 Type::Reference(ref_type) => self.extract_type_name(&ref_type.elem),
425 Type::Slice(slice_type) => self.extract_type_name(&slice_type.elem),
426 Type::Array(array_type) => self.extract_type_name(&array_type.elem),
427 Type::Ptr(ptr_type) => self.extract_type_name(&ptr_type.elem),
428 Type::Paren(paren_type) => self.extract_type_name(&paren_type.elem),
429 Type::Group(group_type) => self.extract_type_name(&group_type.elem),
430 _ => None,
431 }
432 }
433
434 fn analyze_signature(&mut self, sig: &Signature) {
436 for arg in &sig.inputs {
438 if let FnArg::Typed(pat_type) = arg
439 && let Some(type_name) = self.extract_type_name(&pat_type.ty)
440 && !self.is_primitive_type(&type_name)
441 {
442 self.add_dependency(
443 type_name,
444 DependencyKind::TypeRef,
445 UsageContext::FunctionParameter,
446 );
447 }
448 }
449
450 if let ReturnType::Type(_, ty) = &sig.output
452 && let Some(type_name) = self.extract_type_name(ty)
453 && !self.is_primitive_type(&type_name)
454 {
455 self.add_dependency(type_name, DependencyKind::TypeRef, UsageContext::ReturnType);
456 }
457 }
458
459 fn is_primitive_type(&self, type_name: &str) -> bool {
461 if matches!(
463 type_name,
464 "bool"
465 | "char"
466 | "str"
467 | "u8"
468 | "u16"
469 | "u32"
470 | "u64"
471 | "u128"
472 | "usize"
473 | "i8"
474 | "i16"
475 | "i32"
476 | "i64"
477 | "i128"
478 | "isize"
479 | "f32"
480 | "f64"
481 | "String"
482 | "Self"
483 | "()"
484 | "Option"
485 | "Result"
486 | "Vec"
487 | "Box"
488 | "Rc"
489 | "Arc"
490 | "RefCell"
491 | "Cell"
492 | "Mutex"
493 | "RwLock"
494 ) {
495 return true;
496 }
497
498 if type_name.len() <= 3 && type_name.chars().all(|c| c.is_lowercase()) {
501 return true;
502 }
503
504 if type_name.starts_with("self") || type_name == "self" {
506 return true;
507 }
508
509 false
510 }
511}
512
513impl<'ast> Visit<'ast> for CouplingAnalyzer {
514 fn visit_item_use(&mut self, node: &'ast ItemUse) {
515 let paths = self.extract_use_paths(&node.tree, "");
516
517 for (path, kind) in paths {
518 if path == "self" || path.starts_with("self::") {
520 continue;
521 }
522
523 if let Some(type_name) = path.split("::").last() {
525 self.imported_types
526 .insert(type_name.to_string(), path.clone());
527 }
528
529 self.add_dependency(path.clone(), kind, UsageContext::Import);
530
531 if kind == DependencyKind::InternalUse {
533 if !self.metrics.internal_deps.contains(&path) {
534 self.metrics.internal_deps.push(path.clone());
535 }
536 } else if kind == DependencyKind::ExternalUse {
537 let crate_name = path.split("::").next().unwrap_or(&path).to_string();
539 if !self.metrics.external_deps.contains(&crate_name) {
540 self.metrics.external_deps.push(crate_name);
541 }
542 }
543 }
544
545 syn::visit::visit_item_use(self, node);
546 }
547
548 fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
549 if let Some((_, trait_path, _)) = &node.trait_ {
550 self.metrics.trait_impl_count += 1;
552
553 let trait_name: String = trait_path
555 .segments
556 .iter()
557 .map(|s| s.ident.to_string())
558 .collect::<Vec<_>>()
559 .join("::");
560
561 self.add_dependency(
562 trait_name,
563 DependencyKind::TraitImpl,
564 UsageContext::TraitBound,
565 );
566 self.usage_counts.trait_bounds += 1;
567 } else {
568 self.metrics.inherent_impl_count += 1;
570
571 if let Some(type_name) = self.extract_type_name(&node.self_ty)
573 && !self.defined_types.contains(&type_name)
574 {
575 self.add_dependency(
576 type_name,
577 DependencyKind::InherentImpl,
578 UsageContext::InherentImplBlock,
579 );
580 }
581 }
582 syn::visit::visit_item_impl(self, node);
583 }
584
585 fn visit_item_fn(&mut self, node: &'ast ItemFn) {
586 let fn_name = node.sig.ident.to_string();
588 let visibility = convert_visibility(&node.vis);
589 self.defined_functions.insert(fn_name.clone(), visibility);
590
591 if has_test_attribute(&node.attrs) {
593 self.metrics.test_function_count += 1;
594 }
595
596 let mut param_count = 0;
598 let mut primitive_param_count = 0;
599 let mut param_types = Vec::new();
600
601 for arg in &node.sig.inputs {
602 if let FnArg::Typed(pat_type) = arg {
603 param_count += 1;
604 if let Some(type_name) = self.extract_type_name(&pat_type.ty) {
605 param_types.push(type_name.clone());
606 if self.is_primitive_type(&type_name) {
607 primitive_param_count += 1;
608 }
609 }
610 }
611 }
612
613 self.metrics.add_function_definition_full(
615 fn_name.clone(),
616 visibility,
617 param_count,
618 primitive_param_count,
619 param_types,
620 );
621
622 let previous_item = self.current_item.take();
624 self.current_item = Some((fn_name, ItemKind::Function));
625
626 self.analyze_signature(&node.sig);
628 syn::visit::visit_item_fn(self, node);
629
630 self.current_item = previous_item;
632 }
633
634 fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
635 let name = node.ident.to_string();
636 let visibility = convert_visibility(&node.vis);
637
638 self.defined_types.insert(name.clone());
639 self.type_visibility.insert(name.clone(), visibility);
640
641 let (is_newtype, inner_type) = match &node.fields {
643 syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
644 let inner = fields
645 .unnamed
646 .first()
647 .and_then(|f| self.extract_type_name(&f.ty));
648 (true, inner)
649 }
650 _ => (false, None),
651 };
652
653 let has_serde_derive = node.attrs.iter().any(|attr| {
655 if attr.path().is_ident("derive")
656 && let Ok(nested) = attr.parse_args_with(
657 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
658 )
659 {
660 return nested.iter().any(|path| {
661 let path_str = path
662 .segments
663 .iter()
664 .map(|s| s.ident.to_string())
665 .collect::<Vec<_>>()
666 .join("::");
667 path_str == "Serialize"
668 || path_str == "Deserialize"
669 || path_str == "serde::Serialize"
670 || path_str == "serde::Deserialize"
671 });
672 }
673 false
674 });
675
676 let (total_field_count, public_field_count) = match &node.fields {
678 syn::Fields::Named(fields) => {
679 let total = fields.named.len();
680 let public = fields
681 .named
682 .iter()
683 .filter(|f| matches!(f.vis, syn::Visibility::Public(_)))
684 .count();
685 (total, public)
686 }
687 syn::Fields::Unnamed(fields) => {
688 let total = fields.unnamed.len();
689 let public = fields
690 .unnamed
691 .iter()
692 .filter(|f| matches!(f.vis, syn::Visibility::Public(_)))
693 .count();
694 (total, public)
695 }
696 syn::Fields::Unit => (0, 0),
697 };
698
699 self.metrics.add_type_definition_full(
701 name,
702 visibility,
703 false, is_newtype,
705 inner_type,
706 has_serde_derive,
707 public_field_count,
708 total_field_count,
709 );
710
711 match &node.fields {
713 syn::Fields::Named(fields) => {
714 self.metrics.type_usage_count += fields.named.len();
715 for field in &fields.named {
716 if let Some(type_name) = self.extract_type_name(&field.ty)
717 && !self.is_primitive_type(&type_name)
718 {
719 self.add_dependency(
720 type_name,
721 DependencyKind::TypeRef,
722 UsageContext::TypeParameter,
723 );
724 self.usage_counts.type_parameters += 1;
725 }
726 }
727 }
728 syn::Fields::Unnamed(fields) => {
729 for field in &fields.unnamed {
730 if let Some(type_name) = self.extract_type_name(&field.ty)
731 && !self.is_primitive_type(&type_name)
732 {
733 self.add_dependency(
734 type_name,
735 DependencyKind::TypeRef,
736 UsageContext::TypeParameter,
737 );
738 }
739 }
740 }
741 syn::Fields::Unit => {}
742 }
743 syn::visit::visit_item_struct(self, node);
744 }
745
746 fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
747 let name = node.ident.to_string();
748 let visibility = convert_visibility(&node.vis);
749
750 self.defined_types.insert(name.clone());
751 self.type_visibility.insert(name.clone(), visibility);
752
753 self.metrics.add_type_definition(name, visibility, false);
755
756 for variant in &node.variants {
758 match &variant.fields {
759 syn::Fields::Named(fields) => {
760 for field in &fields.named {
761 if let Some(type_name) = self.extract_type_name(&field.ty)
762 && !self.is_primitive_type(&type_name)
763 {
764 self.add_dependency(
765 type_name,
766 DependencyKind::TypeRef,
767 UsageContext::TypeParameter,
768 );
769 }
770 }
771 }
772 syn::Fields::Unnamed(fields) => {
773 for field in &fields.unnamed {
774 if let Some(type_name) = self.extract_type_name(&field.ty)
775 && !self.is_primitive_type(&type_name)
776 {
777 self.add_dependency(
778 type_name,
779 DependencyKind::TypeRef,
780 UsageContext::TypeParameter,
781 );
782 }
783 }
784 }
785 syn::Fields::Unit => {}
786 }
787 }
788 syn::visit::visit_item_enum(self, node);
789 }
790
791 fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
792 let name = node.ident.to_string();
793 let visibility = convert_visibility(&node.vis);
794
795 self.defined_traits.insert(name.clone());
796 self.type_visibility.insert(name.clone(), visibility);
797
798 self.metrics.add_type_definition(name, visibility, true);
800
801 self.metrics.trait_impl_count += 1;
802 syn::visit::visit_item_trait(self, node);
803 }
804
805 fn visit_item_mod(&mut self, node: &'ast ItemMod) {
806 if is_test_module(node) {
808 self.metrics.is_test_module = true;
809 }
810
811 if node.content.is_some() {
812 self.metrics.internal_deps.push(node.ident.to_string());
813 }
814 syn::visit::visit_item_mod(self, node);
815 }
816
817 fn visit_expr_field(&mut self, node: &'ast ExprField) {
819 let field_name = match &node.member {
820 syn::Member::Named(ident) => ident.to_string(),
821 syn::Member::Unnamed(idx) => format!("{}", idx.index),
822 };
823
824 if let Expr::Path(path_expr) = &*node.base {
826 let base_name = path_expr
827 .path
828 .segments
829 .iter()
830 .map(|s| s.ident.to_string())
831 .collect::<Vec<_>>()
832 .join("::");
833
834 let full_path = self
836 .imported_types
837 .get(&base_name)
838 .cloned()
839 .unwrap_or(base_name.clone());
840
841 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
842 self.add_dependency(
843 full_path.clone(),
844 DependencyKind::TypeRef,
845 UsageContext::FieldAccess,
846 );
847 self.usage_counts.field_accesses += 1;
848 }
849
850 let expr = format!("{}.{}", base_name, field_name);
852 self.add_item_dependency(
853 format!("{}.{}", full_path, field_name),
854 ItemDepType::FieldAccess,
855 0,
856 Some(expr),
857 );
858 }
859 syn::visit::visit_expr_field(self, node);
860 }
861
862 fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
864 let method_name = node.method.to_string();
865
866 if let Expr::Path(path_expr) = &*node.receiver {
868 let receiver_name = path_expr
869 .path
870 .segments
871 .iter()
872 .map(|s| s.ident.to_string())
873 .collect::<Vec<_>>()
874 .join("::");
875
876 let full_path = self
877 .imported_types
878 .get(&receiver_name)
879 .cloned()
880 .unwrap_or(receiver_name.clone());
881
882 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
883 self.add_dependency(
884 full_path.clone(),
885 DependencyKind::TypeRef,
886 UsageContext::MethodCall,
887 );
888 self.usage_counts.method_calls += 1;
889 }
890
891 let expr = format!("{}.{}()", receiver_name, method_name);
893 self.add_item_dependency(
894 format!("{}::{}", full_path, method_name),
895 ItemDepType::MethodCall,
896 0, Some(expr),
898 );
899 }
900 syn::visit::visit_expr_method_call(self, node);
901 }
902
903 fn visit_expr_call(&mut self, node: &'ast ExprCall) {
905 if let Expr::Path(path_expr) = &*node.func {
906 let path_str = path_expr
907 .path
908 .segments
909 .iter()
910 .map(|s| s.ident.to_string())
911 .collect::<Vec<_>>()
912 .join("::");
913
914 if path_str.contains("::") || path_str.chars().next().is_some_and(|c| c.is_uppercase())
916 {
917 let full_path = self
918 .imported_types
919 .get(&path_str)
920 .cloned()
921 .unwrap_or(path_str.clone());
922
923 if !self.is_primitive_type(&full_path) && !self.defined_types.contains(&full_path) {
924 self.add_dependency(
925 full_path.clone(),
926 DependencyKind::TypeRef,
927 UsageContext::FunctionCall,
928 );
929 self.usage_counts.function_calls += 1;
930 }
931
932 self.add_item_dependency(
934 full_path,
935 ItemDepType::FunctionCall,
936 0,
937 Some(format!("{}()", path_str)),
938 );
939 } else {
940 self.add_item_dependency(
942 path_str.clone(),
943 ItemDepType::FunctionCall,
944 0,
945 Some(format!("{}()", path_str)),
946 );
947 }
948 }
949 syn::visit::visit_expr_call(self, node);
950 }
951
952 fn visit_expr_struct(&mut self, node: &'ast ExprStruct) {
954 let struct_name = node
955 .path
956 .segments
957 .iter()
958 .map(|s| s.ident.to_string())
959 .collect::<Vec<_>>()
960 .join("::");
961
962 if struct_name == "Self" || struct_name.starts_with("Self::") {
964 syn::visit::visit_expr_struct(self, node);
965 return;
966 }
967
968 let full_path = self
969 .imported_types
970 .get(&struct_name)
971 .cloned()
972 .unwrap_or(struct_name.clone());
973
974 if !self.defined_types.contains(&full_path) && !self.is_primitive_type(&struct_name) {
975 self.add_dependency(
976 full_path,
977 DependencyKind::TypeRef,
978 UsageContext::StructConstruction,
979 );
980 self.usage_counts.struct_constructions += 1;
981 }
982 syn::visit::visit_expr_struct(self, node);
983 }
984}
985
986#[derive(Debug, Clone)]
988struct AnalyzedFile {
989 module_name: String,
990 #[allow(dead_code)]
991 file_path: PathBuf,
992 metrics: ModuleMetrics,
993 dependencies: Vec<Dependency>,
994 type_visibility: HashMap<String, Visibility>,
996 item_dependencies: Vec<ItemDependency>,
998}
999
1000pub fn analyze_project(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1002 analyze_project_parallel(path)
1003}
1004
1005fn rs_files(dir: &Path) -> impl Iterator<Item = PathBuf> {
1011 WalkDir::new(dir)
1012 .follow_links(true)
1013 .into_iter()
1014 .filter_map(|e| e.ok())
1015 .filter(move |entry| {
1016 let file_path = entry.path();
1017 let file_path = file_path.strip_prefix(dir).unwrap_or(file_path);
1022
1023 !file_path.components().any(|c| {
1025 let s = c.as_os_str().to_string_lossy();
1026 s == "target" || s.starts_with('.')
1027 }) && file_path.extension() == Some(OsStr::new("rs"))
1028 })
1029 .map(|e| e.path().to_path_buf())
1030}
1031
1032pub fn analyze_project_parallel(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1037 if !path.exists() {
1038 return Err(AnalyzerError::InvalidPath(path.display().to_string()));
1039 }
1040
1041 let file_paths: Vec<PathBuf> = rs_files(path).collect();
1043
1044 let num_threads = rayon::current_num_threads();
1048 let file_count = file_paths.len();
1049
1050 let chunk_size = if file_count < num_threads * 2 {
1053 1 } else {
1055 (file_count / (num_threads * 4)).max(1)
1058 };
1059
1060 let analyzed_results: Vec<_> = file_paths
1062 .par_chunks(chunk_size)
1063 .flat_map(|chunk| {
1064 chunk
1065 .iter()
1066 .filter_map(|file_path| match analyze_rust_file_full(file_path) {
1067 Ok(result) => Some(AnalyzedFile {
1068 module_name: result.metrics.name.clone(),
1069 file_path: file_path.clone(),
1070 metrics: result.metrics,
1071 dependencies: result.dependencies,
1072 type_visibility: result.type_visibility,
1073 item_dependencies: result.item_dependencies,
1074 }),
1075 Err(e) => {
1076 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1077 None
1078 }
1079 })
1080 .collect::<Vec<_>>()
1081 })
1082 .collect();
1083
1084 let module_names: HashSet<String> = analyzed_results
1086 .iter()
1087 .map(|a| a.module_name.clone())
1088 .collect();
1089
1090 let mut project = ProjectMetrics::new();
1092 project.total_files = analyzed_results.len();
1093
1094 for analyzed in &analyzed_results {
1096 for (type_name, visibility) in &analyzed.type_visibility {
1097 project.register_type(type_name.clone(), analyzed.module_name.clone(), *visibility);
1098 }
1099 }
1100
1101 for analyzed in &analyzed_results {
1103 let mut metrics = analyzed.metrics.clone();
1105 metrics.item_dependencies = analyzed.item_dependencies.clone();
1106 project.add_module(metrics);
1107
1108 for dep in &analyzed.dependencies {
1109 if !is_valid_dependency_path(&dep.path) {
1111 continue;
1112 }
1113
1114 let target_module = extract_target_module(&dep.path);
1116
1117 if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1119 continue;
1120 }
1121
1122 let distance = calculate_distance(&dep.path, &module_names);
1124
1125 let strength = dep.usage.to_strength();
1127
1128 let volatility = Volatility::Low;
1130
1131 let target_type = dep.path.split("::").last().unwrap_or(&dep.path);
1133 let visibility = project
1134 .get_type_visibility(target_type)
1135 .unwrap_or(Visibility::Public); let coupling = CouplingMetrics::with_location(
1139 analyzed.module_name.clone(),
1140 target_module.clone(),
1141 strength,
1142 distance,
1143 volatility,
1144 visibility,
1145 analyzed.file_path.clone(),
1146 dep.line,
1147 );
1148
1149 project.add_coupling(coupling);
1150 }
1151 }
1152
1153 project.update_coupling_visibility();
1155
1156 Ok(project)
1157}
1158
1159pub fn analyze_workspace(path: &Path) -> Result<ProjectMetrics, AnalyzerError> {
1161 let workspace = match WorkspaceInfo::from_path(path) {
1163 Ok(ws) => Some(ws),
1164 Err(e) => {
1165 eprintln!("Note: Could not load workspace metadata: {}", e);
1166 eprintln!("Falling back to basic analysis...");
1167 None
1168 }
1169 };
1170
1171 if let Some(ws) = workspace {
1172 analyze_with_workspace(path, &ws)
1173 } else {
1174 analyze_project(path)
1176 }
1177}
1178
1179fn analyze_with_workspace(
1181 _path: &Path,
1182 workspace: &WorkspaceInfo,
1183) -> Result<ProjectMetrics, AnalyzerError> {
1184 let mut project = ProjectMetrics::new();
1185
1186 project.workspace_name = Some(
1188 workspace
1189 .root
1190 .file_name()
1191 .and_then(|n| n.to_str())
1192 .unwrap_or("workspace")
1193 .to_string(),
1194 );
1195 project.workspace_members = workspace.members.clone();
1196
1197 let mut file_crate_pairs: Vec<(PathBuf, String)> = Vec::new();
1199
1200 for member_name in &workspace.members {
1201 if let Some(crate_info) = workspace.get_crate(member_name) {
1202 if !crate_info.src_path.exists() {
1203 continue;
1204 }
1205
1206 for file_path in rs_files(&crate_info.src_path) {
1207 file_crate_pairs.push((file_path.to_path_buf(), member_name.clone()));
1208 }
1209 }
1210 }
1211
1212 let num_threads = rayon::current_num_threads();
1214 let file_count = file_crate_pairs.len();
1215 let chunk_size = if file_count < num_threads * 2 {
1216 1
1217 } else {
1218 (file_count / (num_threads * 4)).max(1)
1219 };
1220
1221 let analyzed_files: Vec<AnalyzedFileWithCrate> = file_crate_pairs
1223 .par_chunks(chunk_size)
1224 .flat_map(|chunk| {
1225 chunk
1226 .iter()
1227 .filter_map(
1228 |(file_path, crate_name)| match analyze_rust_file_full(file_path) {
1229 Ok(result) => Some(AnalyzedFileWithCrate {
1230 module_name: result.metrics.name.clone(),
1231 crate_name: crate_name.clone(),
1232 file_path: file_path.clone(),
1233 metrics: result.metrics,
1234 dependencies: result.dependencies,
1235 item_dependencies: result.item_dependencies,
1236 }),
1237 Err(e) => {
1238 eprintln!("Warning: Failed to analyze {}: {}", file_path.display(), e);
1239 None
1240 }
1241 },
1242 )
1243 .collect::<Vec<_>>()
1244 })
1245 .collect();
1246
1247 project.total_files = analyzed_files.len();
1248
1249 let module_names: HashSet<String> = analyzed_files
1251 .iter()
1252 .map(|a| a.module_name.clone())
1253 .collect();
1254
1255 for analyzed in &analyzed_files {
1257 let mut metrics = analyzed.metrics.clone();
1259 metrics.item_dependencies = analyzed.item_dependencies.clone();
1260 project.add_module(metrics);
1261
1262 for dep in &analyzed.dependencies {
1263 if !is_valid_dependency_path(&dep.path) {
1265 continue;
1266 }
1267
1268 let resolved_crate =
1270 resolve_crate_from_path(&dep.path, &analyzed.crate_name, workspace);
1271
1272 let target_module = extract_target_module(&dep.path);
1273
1274 if !module_names.contains(&target_module) && !is_valid_dependency_path(&target_module) {
1276 continue;
1277 }
1278
1279 let distance =
1281 calculate_distance_with_workspace(&dep.path, &analyzed.crate_name, workspace);
1282
1283 let strength = dep.usage.to_strength();
1285
1286 let volatility = Volatility::Low;
1288
1289 let mut coupling = CouplingMetrics::with_location(
1291 format!("{}::{}", analyzed.crate_name, analyzed.module_name),
1292 if let Some(ref crate_name) = resolved_crate {
1293 format!("{}::{}", crate_name, target_module)
1294 } else {
1295 target_module.clone()
1296 },
1297 strength,
1298 distance,
1299 volatility,
1300 Visibility::Public, analyzed.file_path.clone(),
1302 dep.line,
1303 );
1304
1305 coupling.source_crate = Some(analyzed.crate_name.clone());
1307 coupling.target_crate = resolved_crate;
1308
1309 project.add_coupling(coupling);
1310 }
1311 }
1312
1313 for (crate_name, deps) in &workspace.dependency_graph {
1315 if workspace.is_workspace_member(crate_name) {
1316 for dep in deps {
1317 project
1319 .crate_dependencies
1320 .entry(crate_name.clone())
1321 .or_default()
1322 .push(dep.clone());
1323 }
1324 }
1325 }
1326
1327 Ok(project)
1328}
1329
1330fn calculate_distance_with_workspace(
1332 dep_path: &str,
1333 current_crate: &str,
1334 workspace: &WorkspaceInfo,
1335) -> Distance {
1336 if dep_path.starts_with("crate::") || dep_path.starts_with("self::") {
1337 Distance::SameModule
1339 } else if dep_path.starts_with("super::") {
1340 Distance::DifferentModule
1342 } else {
1343 if let Some(target_crate) = resolve_crate_from_path(dep_path, current_crate, workspace) {
1345 if target_crate == current_crate {
1346 Distance::SameModule
1347 } else if workspace.is_workspace_member(&target_crate) {
1348 Distance::DifferentModule
1350 } else {
1351 Distance::DifferentCrate
1353 }
1354 } else {
1355 Distance::DifferentCrate
1356 }
1357 }
1358}
1359
1360#[derive(Debug, Clone)]
1362struct AnalyzedFileWithCrate {
1363 module_name: String,
1364 crate_name: String,
1365 #[allow(dead_code)]
1366 file_path: PathBuf,
1367 metrics: ModuleMetrics,
1368 dependencies: Vec<Dependency>,
1369 item_dependencies: Vec<ItemDependency>,
1371}
1372
1373fn extract_target_module(path: &str) -> String {
1375 let cleaned = path
1377 .trim_start_matches("crate::")
1378 .trim_start_matches("super::")
1379 .trim_start_matches("::");
1380
1381 cleaned.split("::").next().unwrap_or(path).to_string()
1383}
1384
1385fn is_valid_dependency_path(path: &str) -> bool {
1387 if path.is_empty() {
1389 return false;
1390 }
1391
1392 if path == "Self" || path.starts_with("Self::") {
1394 return false;
1395 }
1396
1397 let segments: Vec<&str> = path.split("::").collect();
1398
1399 if segments.len() == 1 {
1401 let name = segments[0];
1402 if name.len() <= 8 && name.chars().all(|c| c.is_lowercase() || c == '_') {
1403 return false;
1404 }
1405 }
1406
1407 if segments.len() >= 2 {
1409 let last = segments.last().unwrap();
1410 let second_last = segments.get(segments.len() - 2).unwrap();
1411 if last == second_last {
1412 return false;
1413 }
1414 }
1415
1416 let last_segment = segments.last().unwrap_or(&path);
1418 let common_locals = [
1419 "request",
1420 "response",
1421 "result",
1422 "content",
1423 "config",
1424 "proto",
1425 "domain",
1426 "info",
1427 "data",
1428 "item",
1429 "value",
1430 "error",
1431 "message",
1432 "expected",
1433 "actual",
1434 "status",
1435 "state",
1436 "context",
1437 "params",
1438 "args",
1439 "options",
1440 "settings",
1441 "violation",
1442 "page_token",
1443 ];
1444 if common_locals.contains(last_segment) && segments.len() <= 2 {
1445 return false;
1446 }
1447
1448 true
1449}
1450
1451fn calculate_distance(dep_path: &str, _known_modules: &HashSet<String>) -> Distance {
1453 if dep_path.starts_with("crate::") || dep_path.starts_with("super::") {
1454 Distance::DifferentModule
1456 } else if dep_path.starts_with("self::") {
1457 Distance::SameModule
1458 } else {
1459 Distance::DifferentCrate
1461 }
1462}
1463
1464pub struct AnalyzedFileResult {
1467 pub metrics: ModuleMetrics,
1468 pub dependencies: Vec<Dependency>,
1469 pub type_visibility: HashMap<String, Visibility>,
1470 pub item_dependencies: Vec<ItemDependency>,
1471}
1472
1473pub fn analyze_rust_file(path: &Path) -> Result<(ModuleMetrics, Vec<Dependency>), AnalyzerError> {
1474 let result = analyze_rust_file_full(path)?;
1475 Ok((result.metrics, result.dependencies))
1476}
1477
1478pub fn analyze_rust_file_full(path: &Path) -> Result<AnalyzedFileResult, AnalyzerError> {
1480 let content = fs::read_to_string(path)?;
1481
1482 let module_name = path
1483 .file_stem()
1484 .and_then(|s| s.to_str())
1485 .unwrap_or("unknown")
1486 .to_string();
1487
1488 let mut analyzer = CouplingAnalyzer::new(module_name, path.to_path_buf());
1489 analyzer.analyze_file(&content)?;
1490
1491 Ok(AnalyzedFileResult {
1492 metrics: analyzer.metrics,
1493 dependencies: analyzer.dependencies,
1494 type_visibility: analyzer.type_visibility,
1495 item_dependencies: analyzer.item_dependencies,
1496 })
1497}
1498
1499#[cfg(test)]
1500mod tests {
1501 use super::*;
1502
1503 #[test]
1504 fn test_analyzer_creation() {
1505 let analyzer = CouplingAnalyzer::new(
1506 "test_module".to_string(),
1507 std::path::PathBuf::from("test.rs"),
1508 );
1509 assert_eq!(analyzer.current_module, "test_module");
1510 }
1511
1512 #[test]
1513 fn test_analyze_simple_file() {
1514 let mut analyzer =
1515 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1516
1517 let code = r#"
1518 pub struct User {
1519 name: String,
1520 email: String,
1521 }
1522
1523 impl User {
1524 pub fn new(name: String, email: String) -> Self {
1525 Self { name, email }
1526 }
1527 }
1528 "#;
1529
1530 let result = analyzer.analyze_file(code);
1531 assert!(result.is_ok());
1532 assert_eq!(analyzer.metrics.inherent_impl_count, 1);
1533 }
1534
1535 #[test]
1536 fn test_item_dependencies() {
1537 let mut analyzer =
1538 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1539
1540 let code = r#"
1541 pub struct Config {
1542 pub value: i32,
1543 }
1544
1545 pub fn process(config: Config) -> i32 {
1546 let x = config.value;
1547 helper(x)
1548 }
1549
1550 fn helper(n: i32) -> i32 {
1551 n * 2
1552 }
1553 "#;
1554
1555 let result = analyzer.analyze_file(code);
1556 assert!(result.is_ok());
1557
1558 assert!(analyzer.defined_functions.contains_key("process"));
1560 assert!(analyzer.defined_functions.contains_key("helper"));
1561
1562 println!(
1564 "Item dependencies count: {}",
1565 analyzer.item_dependencies.len()
1566 );
1567 for dep in &analyzer.item_dependencies {
1568 println!(
1569 " {} -> {} ({:?})",
1570 dep.source_item, dep.target, dep.dep_type
1571 );
1572 }
1573
1574 let process_deps: Vec<_> = analyzer
1576 .item_dependencies
1577 .iter()
1578 .filter(|d| d.source_item == "process")
1579 .collect();
1580
1581 assert!(
1582 !process_deps.is_empty(),
1583 "process function should have item dependencies"
1584 );
1585 }
1586
1587 #[test]
1588 fn test_analyze_trait_impl() {
1589 let mut analyzer =
1590 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1591
1592 let code = r#"
1593 trait Printable {
1594 fn print(&self);
1595 }
1596
1597 struct Document;
1598
1599 impl Printable for Document {
1600 fn print(&self) {}
1601 }
1602 "#;
1603
1604 let result = analyzer.analyze_file(code);
1605 assert!(result.is_ok());
1606 assert!(analyzer.metrics.trait_impl_count >= 1);
1607 }
1608
1609 #[test]
1610 fn test_analyze_use_statements() {
1611 let mut analyzer =
1612 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1613
1614 let code = r#"
1615 use std::collections::HashMap;
1616 use serde::Serialize;
1617 use crate::utils;
1618 use crate::models::{User, Post};
1619 "#;
1620
1621 let result = analyzer.analyze_file(code);
1622 assert!(result.is_ok());
1623 assert!(analyzer.metrics.external_deps.contains(&"std".to_string()));
1624 assert!(
1625 analyzer
1626 .metrics
1627 .external_deps
1628 .contains(&"serde".to_string())
1629 );
1630 assert!(!analyzer.dependencies.is_empty());
1631
1632 let internal_deps: Vec<_> = analyzer
1634 .dependencies
1635 .iter()
1636 .filter(|d| d.kind == DependencyKind::InternalUse)
1637 .collect();
1638 assert!(!internal_deps.is_empty());
1639 }
1640
1641 #[test]
1642 fn test_extract_use_paths() {
1643 let analyzer =
1644 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1645
1646 let tree: UseTree = syn::parse_quote!(std::collections::HashMap);
1648 let paths = analyzer.extract_use_paths(&tree, "");
1649 assert_eq!(paths.len(), 1);
1650 assert_eq!(paths[0].0, "std::collections::HashMap");
1651
1652 let tree: UseTree = syn::parse_quote!(crate::models::{User, Post});
1654 let paths = analyzer.extract_use_paths(&tree, "");
1655 assert_eq!(paths.len(), 2);
1656 }
1657
1658 #[test]
1659 fn test_extract_target_module() {
1660 assert_eq!(extract_target_module("crate::models::user"), "models");
1661 assert_eq!(extract_target_module("super::utils"), "utils");
1662 assert_eq!(extract_target_module("std::collections"), "std");
1663 }
1664
1665 #[test]
1666 fn test_field_access_detection() {
1667 let mut analyzer =
1668 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1669
1670 let code = r#"
1671 use crate::models::User;
1672
1673 fn get_name(user: &User) -> String {
1674 user.name.clone()
1675 }
1676 "#;
1677
1678 let result = analyzer.analyze_file(code);
1679 assert!(result.is_ok());
1680
1681 let _field_deps: Vec<_> = analyzer
1683 .dependencies
1684 .iter()
1685 .filter(|d| d.usage == UsageContext::FieldAccess)
1686 .collect();
1687 }
1690
1691 #[test]
1692 fn test_method_call_detection() {
1693 let mut analyzer =
1694 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1695
1696 let code = r#"
1697 fn process() {
1698 let data = String::new();
1699 data.push_str("hello");
1700 }
1701 "#;
1702
1703 let result = analyzer.analyze_file(code);
1704 assert!(result.is_ok());
1705 }
1707
1708 #[test]
1709 fn test_struct_construction_detection() {
1710 let mut analyzer =
1711 CouplingAnalyzer::new("test".to_string(), std::path::PathBuf::from("test.rs"));
1712
1713 let code = r#"
1714 use crate::config::Config;
1715
1716 fn create_config() {
1717 let c = Config { value: 42 };
1718 }
1719 "#;
1720
1721 let result = analyzer.analyze_file(code);
1722 assert!(result.is_ok());
1723
1724 let struct_deps: Vec<_> = analyzer
1726 .dependencies
1727 .iter()
1728 .filter(|d| d.usage == UsageContext::StructConstruction)
1729 .collect();
1730 assert!(!struct_deps.is_empty());
1731 }
1732
1733 #[test]
1734 fn test_usage_context_to_strength() {
1735 assert_eq!(
1736 UsageContext::FieldAccess.to_strength(),
1737 IntegrationStrength::Intrusive
1738 );
1739 assert_eq!(
1740 UsageContext::MethodCall.to_strength(),
1741 IntegrationStrength::Functional
1742 );
1743 assert_eq!(
1744 UsageContext::TypeParameter.to_strength(),
1745 IntegrationStrength::Model
1746 );
1747 assert_eq!(
1748 UsageContext::TraitBound.to_strength(),
1749 IntegrationStrength::Contract
1750 );
1751 }
1752
1753 #[test]
1756 fn test_rs_files_with_hidden_parent_directory() {
1757 use std::fs;
1758 use tempfile::TempDir;
1759
1760 let temp = TempDir::new().unwrap();
1763 let hidden_parent = temp.path().join(".hidden-parent");
1764 let project_dir = hidden_parent.join("myproject").join("src");
1765 fs::create_dir_all(&project_dir).unwrap();
1766
1767 fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1769 fs::write(project_dir.join("main.rs"), "fn main() {}").unwrap();
1770
1771 let files: Vec<_> = rs_files(&project_dir).collect();
1773 assert_eq!(
1774 files.len(),
1775 2,
1776 "Should find 2 .rs files in hidden parent path"
1777 );
1778
1779 let file_names: Vec<_> = files
1781 .iter()
1782 .filter_map(|p| p.file_name())
1783 .filter_map(|n| n.to_str())
1784 .collect();
1785 assert!(file_names.contains(&"lib.rs"));
1786 assert!(file_names.contains(&"main.rs"));
1787 }
1788
1789 #[test]
1791 fn test_rs_files_excludes_hidden_dirs_in_project() {
1792 use std::fs;
1793 use tempfile::TempDir;
1794
1795 let temp = TempDir::new().unwrap();
1796 let project_dir = temp.path().join("myproject").join("src");
1797 let hidden_dir = project_dir.join(".hidden");
1798 fs::create_dir_all(&hidden_dir).unwrap();
1799
1800 fs::write(project_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1802 fs::write(hidden_dir.join("secret.rs"), "fn secret() {}").unwrap();
1803
1804 let files: Vec<_> = rs_files(&project_dir).collect();
1806 assert_eq!(
1807 files.len(),
1808 1,
1809 "Should find only 1 .rs file (excluding .hidden/)"
1810 );
1811
1812 let file_names: Vec<_> = files
1813 .iter()
1814 .filter_map(|p| p.file_name())
1815 .filter_map(|n| n.to_str())
1816 .collect();
1817 assert!(file_names.contains(&"lib.rs"));
1818 assert!(!file_names.contains(&"secret.rs"));
1819 }
1820
1821 #[test]
1823 fn test_rs_files_excludes_target_directory() {
1824 use std::fs;
1825 use tempfile::TempDir;
1826
1827 let temp = TempDir::new().unwrap();
1828 let project_dir = temp.path().join("myproject");
1829 let src_dir = project_dir.join("src");
1830 let target_dir = project_dir.join("target").join("debug");
1831 fs::create_dir_all(&src_dir).unwrap();
1832 fs::create_dir_all(&target_dir).unwrap();
1833
1834 fs::write(src_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
1836 fs::write(target_dir.join("generated.rs"), "// generated").unwrap();
1837
1838 let files: Vec<_> = rs_files(&project_dir).collect();
1840 assert_eq!(
1841 files.len(),
1842 1,
1843 "Should find only 1 .rs file (excluding target/)"
1844 );
1845
1846 let file_names: Vec<_> = files
1847 .iter()
1848 .filter_map(|p| p.file_name())
1849 .filter_map(|n| n.to_str())
1850 .collect();
1851 assert!(file_names.contains(&"lib.rs"));
1852 assert!(!file_names.contains(&"generated.rs"));
1853 }
1854}