1use std::collections::{HashMap, HashSet};
7use std::fmt;
8use std::path::PathBuf;
9
10use crate::connascence::ConnascenceStats;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub enum Visibility {
18 Public,
20 PubCrate,
22 PubSuper,
24 PubIn,
26 #[default]
28 Private,
29}
30
31impl Visibility {
32 pub fn allows_external_access(&self) -> bool {
34 matches!(self, Visibility::Public | Visibility::PubCrate)
35 }
36
37 pub fn is_intrusive_from(&self, same_crate: bool, same_module: bool) -> bool {
42 if same_module {
43 return false;
45 }
46
47 match self {
48 Visibility::Public => false, Visibility::PubCrate => !same_crate, Visibility::PubSuper | Visibility::PubIn => true, Visibility::Private => true, }
53 }
54
55 pub fn intrusive_penalty(&self) -> f64 {
59 match self {
60 Visibility::Public => 0.0, Visibility::PubCrate => 0.25, Visibility::PubSuper => 0.5, Visibility::PubIn => 0.5, Visibility::Private => 1.0, }
66 }
67}
68
69impl fmt::Display for Visibility {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 Visibility::Public => write!(f, "pub"),
73 Visibility::PubCrate => write!(f, "pub(crate)"),
74 Visibility::PubSuper => write!(f, "pub(super)"),
75 Visibility::PubIn => write!(f, "pub(in ...)"),
76 Visibility::Private => write!(f, "private"),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq)]
83pub enum IntegrationStrength {
84 Intrusive,
86 Functional,
88 Model,
90 Contract,
92}
93
94impl IntegrationStrength {
95 pub fn value(&self) -> f64 {
97 match self {
98 IntegrationStrength::Intrusive => 1.0,
99 IntegrationStrength::Functional => 0.75,
100 IntegrationStrength::Model => 0.5,
101 IntegrationStrength::Contract => 0.25,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq)]
108pub enum Distance {
109 SameFunction,
111 SameModule,
113 DifferentModule,
115 DifferentCrate,
117}
118
119impl Distance {
120 pub fn value(&self) -> f64 {
122 match self {
123 Distance::SameFunction => 0.0,
124 Distance::SameModule => 0.25,
125 Distance::DifferentModule => 0.5,
126 Distance::DifferentCrate => 1.0,
127 }
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq)]
133pub enum Volatility {
134 Low,
136 Medium,
138 High,
140}
141
142impl Volatility {
143 pub fn value(&self) -> f64 {
145 match self {
146 Volatility::Low => 0.0,
147 Volatility::Medium => 0.5,
148 Volatility::High => 1.0,
149 }
150 }
151
152 pub fn from_count(count: usize) -> Self {
154 match count {
155 0..=2 => Volatility::Low,
156 3..=10 => Volatility::Medium,
157 _ => Volatility::High,
158 }
159 }
160}
161
162#[derive(Debug, Clone)]
164pub struct CouplingMetrics {
165 pub source: String,
167 pub target: String,
169 pub strength: IntegrationStrength,
171 pub distance: Distance,
173 pub volatility: Volatility,
175 pub source_crate: Option<String>,
177 pub target_crate: Option<String>,
179 pub target_visibility: Visibility,
181}
182
183impl CouplingMetrics {
184 pub fn new(
186 source: String,
187 target: String,
188 strength: IntegrationStrength,
189 distance: Distance,
190 volatility: Volatility,
191 ) -> Self {
192 Self {
193 source,
194 target,
195 strength,
196 distance,
197 volatility,
198 source_crate: None,
199 target_crate: None,
200 target_visibility: Visibility::default(),
201 }
202 }
203
204 pub fn with_visibility(
206 source: String,
207 target: String,
208 strength: IntegrationStrength,
209 distance: Distance,
210 volatility: Volatility,
211 visibility: Visibility,
212 ) -> Self {
213 Self {
214 source,
215 target,
216 strength,
217 distance,
218 volatility,
219 source_crate: None,
220 target_crate: None,
221 target_visibility: visibility,
222 }
223 }
224
225 pub fn is_visibility_intrusive(&self) -> bool {
230 let same_crate = self.source_crate == self.target_crate;
231 let same_module =
232 self.distance == Distance::SameModule || self.distance == Distance::SameFunction;
233 self.target_visibility
234 .is_intrusive_from(same_crate, same_module)
235 }
236
237 pub fn effective_strength(&self) -> IntegrationStrength {
242 if self.is_visibility_intrusive() && self.strength != IntegrationStrength::Intrusive {
243 match self.strength {
245 IntegrationStrength::Contract => IntegrationStrength::Model,
246 IntegrationStrength::Model => IntegrationStrength::Functional,
247 IntegrationStrength::Functional => IntegrationStrength::Intrusive,
248 IntegrationStrength::Intrusive => IntegrationStrength::Intrusive,
249 }
250 } else {
251 self.strength
252 }
253 }
254
255 pub fn effective_strength_value(&self) -> f64 {
257 self.effective_strength().value()
258 }
259
260 pub fn strength_value(&self) -> f64 {
262 self.strength.value()
263 }
264
265 pub fn distance_value(&self) -> f64 {
267 self.distance.value()
268 }
269
270 pub fn volatility_value(&self) -> f64 {
272 self.volatility.value()
273 }
274}
275
276#[derive(Debug, Clone)]
278pub struct TypeDefinition {
279 pub name: String,
281 pub visibility: Visibility,
283 pub is_trait: bool,
285}
286
287#[derive(Debug, Clone, Default)]
289pub struct ModuleMetrics {
290 pub path: PathBuf,
292 pub name: String,
294 pub trait_impl_count: usize,
296 pub inherent_impl_count: usize,
298 pub function_call_count: usize,
300 pub type_usage_count: usize,
302 pub external_deps: Vec<String>,
304 pub internal_deps: Vec<String>,
306 pub type_definitions: HashMap<String, TypeDefinition>,
308}
309
310impl ModuleMetrics {
311 pub fn new(path: PathBuf, name: String) -> Self {
312 Self {
313 path,
314 name,
315 ..Default::default()
316 }
317 }
318
319 pub fn add_type_definition(&mut self, name: String, visibility: Visibility, is_trait: bool) {
321 self.type_definitions.insert(
322 name.clone(),
323 TypeDefinition {
324 name,
325 visibility,
326 is_trait,
327 },
328 );
329 }
330
331 pub fn get_type_visibility(&self, name: &str) -> Option<Visibility> {
333 self.type_definitions.get(name).map(|t| t.visibility)
334 }
335
336 pub fn public_type_count(&self) -> usize {
338 self.type_definitions
339 .values()
340 .filter(|t| t.visibility == Visibility::Public)
341 .count()
342 }
343
344 pub fn private_type_count(&self) -> usize {
346 self.type_definitions
347 .values()
348 .filter(|t| t.visibility != Visibility::Public)
349 .count()
350 }
351
352 pub fn average_strength(&self) -> f64 {
354 let total = self.trait_impl_count + self.inherent_impl_count;
355 if total == 0 {
356 return 0.0;
357 }
358
359 let contract_weight = self.trait_impl_count as f64 * IntegrationStrength::Contract.value();
360 let intrusive_weight =
361 self.inherent_impl_count as f64 * IntegrationStrength::Intrusive.value();
362
363 (contract_weight + intrusive_weight) / total as f64
364 }
365}
366
367#[derive(Debug, Default)]
369pub struct ProjectMetrics {
370 pub modules: HashMap<String, ModuleMetrics>,
372 pub couplings: Vec<CouplingMetrics>,
374 pub file_changes: HashMap<String, usize>,
376 pub total_files: usize,
378 pub workspace_name: Option<String>,
380 pub workspace_members: Vec<String>,
382 pub crate_dependencies: HashMap<String, Vec<String>>,
384 pub type_registry: HashMap<String, (String, Visibility)>,
386 pub connascence_stats: ConnascenceStats,
388}
389
390impl ProjectMetrics {
391 pub fn new() -> Self {
392 Self::default()
393 }
394
395 pub fn add_module(&mut self, metrics: ModuleMetrics) {
397 self.modules.insert(metrics.name.clone(), metrics);
398 }
399
400 pub fn add_coupling(&mut self, coupling: CouplingMetrics) {
402 self.couplings.push(coupling);
403 }
404
405 pub fn register_type(
407 &mut self,
408 type_name: String,
409 module_name: String,
410 visibility: Visibility,
411 ) {
412 self.type_registry
413 .insert(type_name, (module_name, visibility));
414 }
415
416 pub fn get_type_visibility(&self, type_name: &str) -> Option<Visibility> {
418 self.type_registry.get(type_name).map(|(_, vis)| *vis)
419 }
420
421 pub fn get_type_module(&self, type_name: &str) -> Option<&str> {
423 self.type_registry
424 .get(type_name)
425 .map(|(module, _)| module.as_str())
426 }
427
428 pub fn update_coupling_visibility(&mut self) {
433 let visibility_updates: Vec<(usize, Visibility)> = self
435 .couplings
436 .iter()
437 .enumerate()
438 .filter_map(|(idx, coupling)| {
439 let target_type = coupling
440 .target
441 .split("::")
442 .last()
443 .unwrap_or(&coupling.target);
444 self.type_registry
445 .get(target_type)
446 .map(|(_, vis)| (idx, *vis))
447 })
448 .collect();
449
450 for (idx, visibility) in visibility_updates {
452 self.couplings[idx].target_visibility = visibility;
453 }
454 }
455
456 pub fn module_count(&self) -> usize {
458 self.modules.len()
459 }
460
461 pub fn coupling_count(&self) -> usize {
463 self.couplings.len()
464 }
465
466 pub fn average_strength(&self) -> Option<f64> {
468 if self.couplings.is_empty() {
469 return None;
470 }
471 let sum: f64 = self.couplings.iter().map(|c| c.strength_value()).sum();
472 Some(sum / self.couplings.len() as f64)
473 }
474
475 pub fn average_distance(&self) -> Option<f64> {
477 if self.couplings.is_empty() {
478 return None;
479 }
480 let sum: f64 = self.couplings.iter().map(|c| c.distance_value()).sum();
481 Some(sum / self.couplings.len() as f64)
482 }
483
484 pub fn update_volatility_from_git(&mut self) {
490 if self.file_changes.is_empty() {
491 return;
492 }
493
494 for coupling in &mut self.couplings {
495 let target_module = coupling
500 .target
501 .split("::")
502 .last()
503 .unwrap_or(&coupling.target);
504
505 let mut max_changes = 0usize;
507 for (file_path, &changes) in &self.file_changes {
508 let file_name = file_path
510 .rsplit('/')
511 .next()
512 .unwrap_or(file_path)
513 .trim_end_matches(".rs");
514
515 if file_name == target_module || file_path.contains(target_module) {
516 max_changes = max_changes.max(changes);
517 }
518 }
519
520 coupling.volatility = Volatility::from_count(max_changes);
521 }
522 }
523
524 fn build_dependency_graph(&self) -> HashMap<String, HashSet<String>> {
526 let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
527
528 for coupling in &self.couplings {
529 if coupling.distance == Distance::DifferentCrate {
531 continue;
532 }
533
534 let source = coupling.source.clone();
536 let target = coupling.target.clone();
537
538 graph.entry(source).or_default().insert(target);
539 }
540
541 graph
542 }
543
544 pub fn detect_circular_dependencies(&self) -> Vec<Vec<String>> {
549 let graph = self.build_dependency_graph();
550 let mut cycles: Vec<Vec<String>> = Vec::new();
551 let mut visited: HashSet<String> = HashSet::new();
552 let mut rec_stack: HashSet<String> = HashSet::new();
553
554 for node in graph.keys() {
555 if !visited.contains(node) {
556 let mut path = Vec::new();
557 self.dfs_find_cycles(
558 node,
559 &graph,
560 &mut visited,
561 &mut rec_stack,
562 &mut path,
563 &mut cycles,
564 );
565 }
566 }
567
568 let mut unique_cycles: Vec<Vec<String>> = Vec::new();
570 for cycle in cycles {
571 let normalized = Self::normalize_cycle(&cycle);
572 if !unique_cycles
573 .iter()
574 .any(|c| Self::normalize_cycle(c) == normalized)
575 {
576 unique_cycles.push(cycle);
577 }
578 }
579
580 unique_cycles
581 }
582
583 fn dfs_find_cycles(
585 &self,
586 node: &str,
587 graph: &HashMap<String, HashSet<String>>,
588 visited: &mut HashSet<String>,
589 rec_stack: &mut HashSet<String>,
590 path: &mut Vec<String>,
591 cycles: &mut Vec<Vec<String>>,
592 ) {
593 visited.insert(node.to_string());
594 rec_stack.insert(node.to_string());
595 path.push(node.to_string());
596
597 if let Some(neighbors) = graph.get(node) {
598 for neighbor in neighbors {
599 if !visited.contains(neighbor) {
600 self.dfs_find_cycles(neighbor, graph, visited, rec_stack, path, cycles);
601 } else if rec_stack.contains(neighbor) {
602 if let Some(start_idx) = path.iter().position(|n| n == neighbor) {
604 let cycle: Vec<String> = path[start_idx..].to_vec();
605 if cycle.len() >= 2 {
606 cycles.push(cycle);
607 }
608 }
609 }
610 }
611 }
612
613 path.pop();
614 rec_stack.remove(node);
615 }
616
617 fn normalize_cycle(cycle: &[String]) -> Vec<String> {
620 if cycle.is_empty() {
621 return Vec::new();
622 }
623
624 let min_pos = cycle
626 .iter()
627 .enumerate()
628 .min_by_key(|(_, s)| s.as_str())
629 .map(|(i, _)| i)
630 .unwrap_or(0);
631
632 let mut normalized: Vec<String> = cycle[min_pos..].to_vec();
634 normalized.extend_from_slice(&cycle[..min_pos]);
635 normalized
636 }
637
638 pub fn circular_dependency_summary(&self) -> CircularDependencySummary {
640 let cycles = self.detect_circular_dependencies();
641 let affected_modules: HashSet<String> = cycles.iter().flatten().cloned().collect();
642
643 CircularDependencySummary {
644 total_cycles: cycles.len(),
645 affected_modules: affected_modules.len(),
646 cycles,
647 }
648 }
649}
650
651#[derive(Debug, Clone)]
653pub struct CircularDependencySummary {
654 pub total_cycles: usize,
656 pub affected_modules: usize,
658 pub cycles: Vec<Vec<String>>,
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn test_integration_strength_values() {
668 assert_eq!(IntegrationStrength::Intrusive.value(), 1.0);
669 assert_eq!(IntegrationStrength::Contract.value(), 0.25);
670 }
671
672 #[test]
673 fn test_distance_values() {
674 assert_eq!(Distance::SameFunction.value(), 0.0);
675 assert_eq!(Distance::DifferentCrate.value(), 1.0);
676 }
677
678 #[test]
679 fn test_volatility_from_count() {
680 assert_eq!(Volatility::from_count(0), Volatility::Low);
681 assert_eq!(Volatility::from_count(5), Volatility::Medium);
682 assert_eq!(Volatility::from_count(15), Volatility::High);
683 }
684
685 #[test]
686 fn test_module_metrics_average_strength() {
687 let mut metrics = ModuleMetrics::new(PathBuf::from("test.rs"), "test".to_string());
688 metrics.trait_impl_count = 3;
689 metrics.inherent_impl_count = 1;
690
691 let avg = metrics.average_strength();
692 assert!(avg > 0.0 && avg < 1.0);
693 }
694
695 #[test]
696 fn test_project_metrics() {
697 let mut project = ProjectMetrics::new();
698
699 let module = ModuleMetrics::new(PathBuf::from("lib.rs"), "lib".to_string());
700 project.add_module(module);
701
702 assert_eq!(project.module_count(), 1);
703 assert_eq!(project.coupling_count(), 0);
704 }
705
706 #[test]
707 fn test_circular_dependency_detection() {
708 let mut project = ProjectMetrics::new();
709
710 project.add_coupling(CouplingMetrics::new(
712 "module_a".to_string(),
713 "module_b".to_string(),
714 IntegrationStrength::Model,
715 Distance::DifferentModule,
716 Volatility::Low,
717 ));
718 project.add_coupling(CouplingMetrics::new(
719 "module_b".to_string(),
720 "module_c".to_string(),
721 IntegrationStrength::Model,
722 Distance::DifferentModule,
723 Volatility::Low,
724 ));
725 project.add_coupling(CouplingMetrics::new(
726 "module_c".to_string(),
727 "module_a".to_string(),
728 IntegrationStrength::Model,
729 Distance::DifferentModule,
730 Volatility::Low,
731 ));
732
733 let cycles = project.detect_circular_dependencies();
734 assert_eq!(cycles.len(), 1);
735 assert_eq!(cycles[0].len(), 3);
736 }
737
738 #[test]
739 fn test_no_circular_dependencies() {
740 let mut project = ProjectMetrics::new();
741
742 project.add_coupling(CouplingMetrics::new(
744 "module_a".to_string(),
745 "module_b".to_string(),
746 IntegrationStrength::Model,
747 Distance::DifferentModule,
748 Volatility::Low,
749 ));
750 project.add_coupling(CouplingMetrics::new(
751 "module_b".to_string(),
752 "module_c".to_string(),
753 IntegrationStrength::Model,
754 Distance::DifferentModule,
755 Volatility::Low,
756 ));
757
758 let cycles = project.detect_circular_dependencies();
759 assert!(cycles.is_empty());
760 }
761
762 #[test]
763 fn test_external_crates_excluded_from_cycles() {
764 let mut project = ProjectMetrics::new();
765
766 project.add_coupling(CouplingMetrics::new(
768 "module_a".to_string(),
769 "serde::Serialize".to_string(),
770 IntegrationStrength::Contract,
771 Distance::DifferentCrate, Volatility::Low,
773 ));
774 project.add_coupling(CouplingMetrics::new(
775 "serde::Serialize".to_string(),
776 "module_a".to_string(),
777 IntegrationStrength::Contract,
778 Distance::DifferentCrate, Volatility::Low,
780 ));
781
782 let cycles = project.detect_circular_dependencies();
783 assert!(cycles.is_empty());
784 }
785
786 #[test]
787 fn test_circular_dependency_summary() {
788 let mut project = ProjectMetrics::new();
789
790 project.add_coupling(CouplingMetrics::new(
792 "module_a".to_string(),
793 "module_b".to_string(),
794 IntegrationStrength::Functional,
795 Distance::DifferentModule,
796 Volatility::Low,
797 ));
798 project.add_coupling(CouplingMetrics::new(
799 "module_b".to_string(),
800 "module_a".to_string(),
801 IntegrationStrength::Functional,
802 Distance::DifferentModule,
803 Volatility::Low,
804 ));
805
806 let summary = project.circular_dependency_summary();
807 assert!(summary.total_cycles > 0);
808 assert!(summary.affected_modules >= 2);
809 }
810
811 #[test]
812 fn test_visibility_intrusive_detection() {
813 assert!(!Visibility::Public.is_intrusive_from(true, false));
815 assert!(!Visibility::Public.is_intrusive_from(false, false));
816
817 assert!(!Visibility::PubCrate.is_intrusive_from(true, false));
819 assert!(Visibility::PubCrate.is_intrusive_from(false, false));
820
821 assert!(Visibility::Private.is_intrusive_from(true, false));
823 assert!(Visibility::Private.is_intrusive_from(false, false));
824
825 assert!(!Visibility::Private.is_intrusive_from(true, true));
827 assert!(!Visibility::Private.is_intrusive_from(false, true));
828 }
829
830 #[test]
831 fn test_visibility_penalty() {
832 assert_eq!(Visibility::Public.intrusive_penalty(), 0.0);
833 assert_eq!(Visibility::PubCrate.intrusive_penalty(), 0.25);
834 assert_eq!(Visibility::Private.intrusive_penalty(), 1.0);
835 }
836
837 #[test]
838 fn test_effective_strength() {
839 let coupling = CouplingMetrics::with_visibility(
841 "source".to_string(),
842 "target".to_string(),
843 IntegrationStrength::Model,
844 Distance::DifferentModule,
845 Volatility::Low,
846 Visibility::Public,
847 );
848 assert_eq!(coupling.effective_strength(), IntegrationStrength::Model);
849
850 let coupling = CouplingMetrics::with_visibility(
852 "source".to_string(),
853 "target".to_string(),
854 IntegrationStrength::Model,
855 Distance::DifferentModule,
856 Volatility::Low,
857 Visibility::Private,
858 );
859 assert_eq!(
860 coupling.effective_strength(),
861 IntegrationStrength::Functional
862 );
863 }
864
865 #[test]
866 fn test_type_registry() {
867 let mut project = ProjectMetrics::new();
868
869 project.register_type(
870 "MyStruct".to_string(),
871 "my_module".to_string(),
872 Visibility::Public,
873 );
874 project.register_type(
875 "InternalType".to_string(),
876 "my_module".to_string(),
877 Visibility::PubCrate,
878 );
879
880 assert_eq!(
881 project.get_type_visibility("MyStruct"),
882 Some(Visibility::Public)
883 );
884 assert_eq!(
885 project.get_type_visibility("InternalType"),
886 Some(Visibility::PubCrate)
887 );
888 assert_eq!(project.get_type_visibility("Unknown"), None);
889
890 assert_eq!(project.get_type_module("MyStruct"), Some("my_module"));
891 }
892
893 #[test]
894 fn test_module_type_definitions() {
895 let mut module = ModuleMetrics::new(PathBuf::from("test.rs"), "test".to_string());
896
897 module.add_type_definition("PublicStruct".to_string(), Visibility::Public, false);
898 module.add_type_definition("PrivateStruct".to_string(), Visibility::Private, false);
899 module.add_type_definition("PublicTrait".to_string(), Visibility::Public, true);
900
901 assert_eq!(module.public_type_count(), 2);
902 assert_eq!(module.private_type_count(), 1);
903 assert_eq!(
904 module.get_type_visibility("PublicStruct"),
905 Some(Visibility::Public)
906 );
907 }
908}