1use std::collections::{HashMap, HashSet};
7use std::fmt;
8use std::path::PathBuf;
9
10use crate::analyzer::ItemDependency;
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, Eq, PartialOrd, Ord)]
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, Default)]
164pub struct CouplingLocation {
165 pub file_path: Option<PathBuf>,
167 pub line: usize,
169}
170
171#[derive(Debug, Clone)]
173pub struct CouplingMetrics {
174 pub source: String,
176 pub target: String,
178 pub strength: IntegrationStrength,
180 pub distance: Distance,
182 pub volatility: Volatility,
184 pub source_crate: Option<String>,
186 pub target_crate: Option<String>,
188 pub target_visibility: Visibility,
190 pub location: CouplingLocation,
192}
193
194impl CouplingMetrics {
195 pub fn new(
197 source: String,
198 target: String,
199 strength: IntegrationStrength,
200 distance: Distance,
201 volatility: Volatility,
202 ) -> Self {
203 Self {
204 source,
205 target,
206 strength,
207 distance,
208 volatility,
209 source_crate: None,
210 target_crate: None,
211 target_visibility: Visibility::default(),
212 location: CouplingLocation::default(),
213 }
214 }
215
216 pub fn with_visibility(
218 source: String,
219 target: String,
220 strength: IntegrationStrength,
221 distance: Distance,
222 volatility: Volatility,
223 visibility: Visibility,
224 ) -> Self {
225 Self {
226 source,
227 target,
228 strength,
229 distance,
230 volatility,
231 source_crate: None,
232 target_crate: None,
233 target_visibility: visibility,
234 location: CouplingLocation::default(),
235 }
236 }
237
238 #[allow(clippy::too_many_arguments)]
240 pub fn with_location(
241 source: String,
242 target: String,
243 strength: IntegrationStrength,
244 distance: Distance,
245 volatility: Volatility,
246 visibility: Visibility,
247 file_path: PathBuf,
248 line: usize,
249 ) -> Self {
250 Self {
251 source,
252 target,
253 strength,
254 distance,
255 volatility,
256 source_crate: None,
257 target_crate: None,
258 target_visibility: visibility,
259 location: CouplingLocation {
260 file_path: Some(file_path),
261 line,
262 },
263 }
264 }
265
266 pub fn is_visibility_intrusive(&self) -> bool {
271 let same_crate = self.source_crate == self.target_crate;
272 let same_module =
273 self.distance == Distance::SameModule || self.distance == Distance::SameFunction;
274 self.target_visibility
275 .is_intrusive_from(same_crate, same_module)
276 }
277
278 pub fn effective_strength(&self) -> IntegrationStrength {
283 if self.is_visibility_intrusive() && self.strength != IntegrationStrength::Intrusive {
284 match self.strength {
286 IntegrationStrength::Contract => IntegrationStrength::Model,
287 IntegrationStrength::Model => IntegrationStrength::Functional,
288 IntegrationStrength::Functional => IntegrationStrength::Intrusive,
289 IntegrationStrength::Intrusive => IntegrationStrength::Intrusive,
290 }
291 } else {
292 self.strength
293 }
294 }
295
296 pub fn effective_strength_value(&self) -> f64 {
298 self.effective_strength().value()
299 }
300
301 pub fn strength_value(&self) -> f64 {
303 self.strength.value()
304 }
305
306 pub fn distance_value(&self) -> f64 {
308 self.distance.value()
309 }
310
311 pub fn volatility_value(&self) -> f64 {
313 self.volatility.value()
314 }
315}
316
317#[derive(Debug, Clone)]
319pub struct TypeDefinition {
320 pub name: String,
322 pub visibility: Visibility,
324 pub is_trait: bool,
326 pub is_newtype: bool,
328 pub inner_type: Option<String>,
330 pub has_serde_derive: bool,
332 pub public_field_count: usize,
334 pub total_field_count: usize,
336}
337
338#[derive(Debug, Clone)]
340pub struct FunctionDefinition {
341 pub name: String,
343 pub visibility: Visibility,
345 pub param_count: usize,
347 pub primitive_param_count: usize,
349 pub param_types: Vec<String>,
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355pub enum BalanceClassification {
356 HighCohesion,
358 LooseCoupling,
360 Acceptable,
362 Pain,
364 LocalComplexity,
366}
367
368impl BalanceClassification {
369 pub fn classify(
371 strength: IntegrationStrength,
372 distance: Distance,
373 volatility: Volatility,
374 ) -> Self {
375 let is_strong = strength.value() >= 0.5;
376 let is_far = distance.value() >= 0.5;
377 let is_volatile = volatility == Volatility::High;
378
379 match (is_strong, is_far, is_volatile) {
380 (true, false, _) => BalanceClassification::HighCohesion,
381 (false, true, _) => BalanceClassification::LooseCoupling,
382 (false, false, _) => BalanceClassification::LocalComplexity,
383 (true, true, false) => BalanceClassification::Acceptable,
384 (true, true, true) => BalanceClassification::Pain,
385 }
386 }
387
388 pub fn description_ja(&self) -> &'static str {
390 match self {
391 BalanceClassification::HighCohesion => "高凝集 (強+近)",
392 BalanceClassification::LooseCoupling => "疎結合 (弱+遠)",
393 BalanceClassification::Acceptable => "許容可能 (強+遠+安定)",
394 BalanceClassification::Pain => "要改善 (強+遠+変動)",
395 BalanceClassification::LocalComplexity => "局所複雑性 (弱+近)",
396 }
397 }
398
399 pub fn description_en(&self) -> &'static str {
401 match self {
402 BalanceClassification::HighCohesion => "High Cohesion",
403 BalanceClassification::LooseCoupling => "Loose Coupling",
404 BalanceClassification::Acceptable => "Acceptable",
405 BalanceClassification::Pain => "Needs Refactoring",
406 BalanceClassification::LocalComplexity => "Local Complexity",
407 }
408 }
409
410 pub fn is_ideal(&self) -> bool {
412 matches!(
413 self,
414 BalanceClassification::HighCohesion | BalanceClassification::LooseCoupling
415 )
416 }
417
418 pub fn needs_attention(&self) -> bool {
420 matches!(
421 self,
422 BalanceClassification::Pain | BalanceClassification::LocalComplexity
423 )
424 }
425}
426
427#[derive(Debug, Clone, Default)]
429pub struct DimensionStats {
430 pub strength_counts: StrengthCounts,
432 pub distance_counts: DistanceCounts,
434 pub volatility_counts: VolatilityCounts,
436 pub balance_counts: BalanceCounts,
438}
439
440impl DimensionStats {
441 pub fn total(&self) -> usize {
443 self.strength_counts.total()
444 }
445
446 pub fn strength_percentages(&self) -> (f64, f64, f64, f64) {
448 let total = self.total() as f64;
449 if total == 0.0 {
450 return (0.0, 0.0, 0.0, 0.0);
451 }
452 (
453 self.strength_counts.intrusive as f64 / total * 100.0,
454 self.strength_counts.functional as f64 / total * 100.0,
455 self.strength_counts.model as f64 / total * 100.0,
456 self.strength_counts.contract as f64 / total * 100.0,
457 )
458 }
459
460 pub fn distance_percentages(&self) -> (f64, f64, f64) {
462 let total = self.total() as f64;
463 if total == 0.0 {
464 return (0.0, 0.0, 0.0);
465 }
466 (
467 self.distance_counts.same_module as f64 / total * 100.0,
468 self.distance_counts.different_module as f64 / total * 100.0,
469 self.distance_counts.different_crate as f64 / total * 100.0,
470 )
471 }
472
473 pub fn volatility_percentages(&self) -> (f64, f64, f64) {
475 let total = self.total() as f64;
476 if total == 0.0 {
477 return (0.0, 0.0, 0.0);
478 }
479 (
480 self.volatility_counts.low as f64 / total * 100.0,
481 self.volatility_counts.medium as f64 / total * 100.0,
482 self.volatility_counts.high as f64 / total * 100.0,
483 )
484 }
485
486 pub fn ideal_count(&self) -> usize {
488 self.balance_counts.high_cohesion + self.balance_counts.loose_coupling
489 }
490
491 pub fn problematic_count(&self) -> usize {
493 self.balance_counts.pain + self.balance_counts.local_complexity
494 }
495
496 pub fn ideal_percentage(&self) -> f64 {
498 let total = self.total() as f64;
499 if total == 0.0 {
500 return 0.0;
501 }
502 self.ideal_count() as f64 / total * 100.0
503 }
504}
505
506#[derive(Debug, Clone, Default)]
508pub struct StrengthCounts {
509 pub intrusive: usize,
510 pub functional: usize,
511 pub model: usize,
512 pub contract: usize,
513}
514
515impl StrengthCounts {
516 pub fn total(&self) -> usize {
518 self.intrusive + self.functional + self.model + self.contract
519 }
520}
521
522#[derive(Debug, Clone, Default)]
524pub struct DistanceCounts {
525 pub same_module: usize,
526 pub different_module: usize,
527 pub different_crate: usize,
528}
529
530#[derive(Debug, Clone, Default)]
532pub struct VolatilityCounts {
533 pub low: usize,
534 pub medium: usize,
535 pub high: usize,
536}
537
538#[derive(Debug, Clone, Default)]
540pub struct BalanceCounts {
541 pub high_cohesion: usize,
542 pub loose_coupling: usize,
543 pub acceptable: usize,
544 pub pain: usize,
545 pub local_complexity: usize,
546}
547
548#[derive(Debug, Clone, Default)]
550pub struct ModuleMetrics {
551 pub path: PathBuf,
553 pub name: String,
555 pub trait_impl_count: usize,
557 pub inherent_impl_count: usize,
559 pub function_call_count: usize,
561 pub type_usage_count: usize,
563 pub external_deps: Vec<String>,
565 pub internal_deps: Vec<String>,
567 pub type_definitions: HashMap<String, TypeDefinition>,
569 pub function_definitions: HashMap<String, FunctionDefinition>,
571 pub item_dependencies: Vec<ItemDependency>,
573 pub is_test_module: bool,
575 pub test_function_count: usize,
577}
578
579impl ModuleMetrics {
580 pub fn new(path: PathBuf, name: String) -> Self {
581 Self {
582 path,
583 name,
584 ..Default::default()
585 }
586 }
587
588 pub fn add_type_definition(&mut self, name: String, visibility: Visibility, is_trait: bool) {
590 self.type_definitions.insert(
591 name.clone(),
592 TypeDefinition {
593 name,
594 visibility,
595 is_trait,
596 is_newtype: false,
597 inner_type: None,
598 has_serde_derive: false,
599 public_field_count: 0,
600 total_field_count: 0,
601 },
602 );
603 }
604
605 #[allow(clippy::too_many_arguments)]
607 pub fn add_type_definition_full(
608 &mut self,
609 name: String,
610 visibility: Visibility,
611 is_trait: bool,
612 is_newtype: bool,
613 inner_type: Option<String>,
614 has_serde_derive: bool,
615 public_field_count: usize,
616 total_field_count: usize,
617 ) {
618 self.type_definitions.insert(
619 name.clone(),
620 TypeDefinition {
621 name,
622 visibility,
623 is_trait,
624 is_newtype,
625 inner_type,
626 has_serde_derive,
627 public_field_count,
628 total_field_count,
629 },
630 );
631 }
632
633 pub fn add_function_definition(&mut self, name: String, visibility: Visibility) {
635 self.function_definitions.insert(
636 name.clone(),
637 FunctionDefinition {
638 name,
639 visibility,
640 param_count: 0,
641 primitive_param_count: 0,
642 param_types: Vec::new(),
643 },
644 );
645 }
646
647 pub fn add_function_definition_full(
649 &mut self,
650 name: String,
651 visibility: Visibility,
652 param_count: usize,
653 primitive_param_count: usize,
654 param_types: Vec<String>,
655 ) {
656 self.function_definitions.insert(
657 name.clone(),
658 FunctionDefinition {
659 name,
660 visibility,
661 param_count,
662 primitive_param_count,
663 param_types,
664 },
665 );
666 }
667
668 pub fn get_type_visibility(&self, name: &str) -> Option<Visibility> {
670 self.type_definitions.get(name).map(|t| t.visibility)
671 }
672
673 pub fn public_type_count(&self) -> usize {
675 self.type_definitions
676 .values()
677 .filter(|t| t.visibility == Visibility::Public)
678 .count()
679 }
680
681 pub fn private_type_count(&self) -> usize {
683 self.type_definitions
684 .values()
685 .filter(|t| t.visibility != Visibility::Public)
686 .count()
687 }
688
689 pub fn average_strength(&self) -> f64 {
691 let total = self.trait_impl_count + self.inherent_impl_count;
692 if total == 0 {
693 return 0.0;
694 }
695
696 let contract_weight = self.trait_impl_count as f64 * IntegrationStrength::Contract.value();
697 let intrusive_weight =
698 self.inherent_impl_count as f64 * IntegrationStrength::Intrusive.value();
699
700 (contract_weight + intrusive_weight) / total as f64
701 }
702
703 pub fn newtype_count(&self) -> usize {
705 self.type_definitions
706 .values()
707 .filter(|t| t.is_newtype)
708 .count()
709 }
710
711 pub fn serde_type_count(&self) -> usize {
713 self.type_definitions
714 .values()
715 .filter(|t| t.has_serde_derive)
716 .count()
717 }
718
719 pub fn newtype_ratio(&self) -> f64 {
721 let non_trait_types = self
722 .type_definitions
723 .values()
724 .filter(|t| !t.is_trait)
725 .count();
726 if non_trait_types == 0 {
727 return 0.0;
728 }
729 self.newtype_count() as f64 / non_trait_types as f64
730 }
731
732 pub fn types_with_public_fields(&self) -> usize {
734 self.type_definitions
735 .values()
736 .filter(|t| t.public_field_count > 0)
737 .count()
738 }
739
740 pub fn function_count(&self) -> usize {
742 self.function_definitions.len()
743 }
744
745 pub fn functions_with_primitive_obsession(&self) -> Vec<&FunctionDefinition> {
748 self.function_definitions
749 .values()
750 .filter(|f| {
751 f.param_count >= 3 && f.primitive_param_count as f64 / f.param_count as f64 >= 0.6
752 })
753 .collect()
754 }
755
756 pub fn is_god_module(&self, max_functions: usize, max_types: usize, max_impls: usize) -> bool {
759 self.function_count() > max_functions
760 || self.type_definitions.len() > max_types
761 || (self.trait_impl_count + self.inherent_impl_count) > max_impls
762 }
763}
764
765#[derive(Debug, Default)]
767pub struct ProjectMetrics {
768 pub modules: HashMap<String, ModuleMetrics>,
770 pub couplings: Vec<CouplingMetrics>,
772 pub file_changes: HashMap<String, usize>,
774 pub total_files: usize,
776 pub workspace_name: Option<String>,
778 pub workspace_members: Vec<String>,
780 pub crate_dependencies: HashMap<String, Vec<String>>,
782 pub type_registry: HashMap<String, (String, Visibility)>,
784 pub temporal_couplings: Vec<crate::volatility::TemporalCoupling>,
786}
787
788impl ProjectMetrics {
789 pub fn new() -> Self {
790 Self::default()
791 }
792
793 pub fn add_module(&mut self, metrics: ModuleMetrics) {
795 self.modules.insert(metrics.name.clone(), metrics);
796 }
797
798 pub fn add_coupling(&mut self, coupling: CouplingMetrics) {
800 self.couplings.push(coupling);
801 }
802
803 pub fn register_type(
805 &mut self,
806 type_name: String,
807 module_name: String,
808 visibility: Visibility,
809 ) {
810 self.type_registry
811 .insert(type_name, (module_name, visibility));
812 }
813
814 pub fn get_type_visibility(&self, type_name: &str) -> Option<Visibility> {
816 self.type_registry.get(type_name).map(|(_, vis)| *vis)
817 }
818
819 pub fn get_type_module(&self, type_name: &str) -> Option<&str> {
821 self.type_registry
822 .get(type_name)
823 .map(|(module, _)| module.as_str())
824 }
825
826 pub fn update_coupling_visibility(&mut self) {
831 let visibility_updates: Vec<(usize, Visibility)> = self
833 .couplings
834 .iter()
835 .enumerate()
836 .filter_map(|(idx, coupling)| {
837 let target_type = coupling
838 .target
839 .split("::")
840 .last()
841 .unwrap_or(&coupling.target);
842 self.type_registry
843 .get(target_type)
844 .map(|(_, vis)| (idx, *vis))
845 })
846 .collect();
847
848 for (idx, visibility) in visibility_updates {
850 self.couplings[idx].target_visibility = visibility;
851 }
852 }
853
854 pub fn module_count(&self) -> usize {
856 self.modules.len()
857 }
858
859 pub fn coupling_count(&self) -> usize {
861 self.couplings.len()
862 }
863
864 pub fn internal_coupling_count(&self) -> usize {
866 self.couplings
867 .iter()
868 .filter(|c| !crate::balance::is_external_crate(&c.target, &c.source))
869 .count()
870 }
871
872 pub fn average_strength(&self) -> Option<f64> {
874 if self.couplings.is_empty() {
875 return None;
876 }
877 let sum: f64 = self.couplings.iter().map(|c| c.strength_value()).sum();
878 Some(sum / self.couplings.len() as f64)
879 }
880
881 pub fn average_distance(&self) -> Option<f64> {
883 if self.couplings.is_empty() {
884 return None;
885 }
886 let sum: f64 = self.couplings.iter().map(|c| c.distance_value()).sum();
887 Some(sum / self.couplings.len() as f64)
888 }
889
890 pub fn update_volatility_from_git(&mut self) {
896 if self.file_changes.is_empty() {
897 return;
898 }
899
900 #[cfg(test)]
902 {
903 eprintln!("DEBUG: file_changes = {:?}", self.file_changes);
904 }
905
906 for coupling in &mut self.couplings {
907 let target_parts: Vec<&str> = coupling.target.split("::").collect();
918
919 let mut max_changes = 0usize;
921 for (file_path, &changes) in &self.file_changes {
922 let file_name = file_path
924 .rsplit('/')
925 .next()
926 .unwrap_or(file_path)
927 .trim_end_matches(".rs");
928
929 let matches = target_parts.iter().any(|part| {
931 let part_lower = part.to_lowercase();
932 let file_lower = file_name.to_lowercase();
933
934 if part_lower == file_lower {
936 return true;
937 }
938
939 if file_lower == "lib" && !part.is_empty() && *part != "*" {
942 if target_parts.len() >= 2 && target_parts[1] == *part {
945 return true;
946 }
947 }
948
949 let part_normalized = part_lower.replace('-', "_");
952 let file_normalized = file_lower.replace('-', "_");
953 if part_normalized == file_normalized {
954 return true;
955 }
956
957 if file_path.to_lowercase().contains(&part_lower) {
959 return true;
960 }
961
962 false
963 });
964
965 if matches {
966 max_changes = max_changes.max(changes);
967 }
968 }
969
970 coupling.volatility = Volatility::from_count(max_changes);
971 }
972 }
973
974 fn build_dependency_graph(&self) -> HashMap<String, HashSet<String>> {
976 let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
977
978 for coupling in &self.couplings {
979 if coupling.distance == Distance::DifferentCrate {
981 continue;
982 }
983
984 let source = coupling.source.clone();
986 let target = coupling.target.clone();
987
988 graph.entry(source).or_default().insert(target);
989 }
990
991 graph
992 }
993
994 pub fn detect_circular_dependencies(&self) -> Vec<Vec<String>> {
999 let graph = self.build_dependency_graph();
1000 let mut cycles: Vec<Vec<String>> = Vec::new();
1001 let mut visited: HashSet<String> = HashSet::new();
1002 let mut rec_stack: HashSet<String> = HashSet::new();
1003
1004 for node in graph.keys() {
1005 if !visited.contains(node) {
1006 let mut path = Vec::new();
1007 self.dfs_find_cycles(
1008 node,
1009 &graph,
1010 &mut visited,
1011 &mut rec_stack,
1012 &mut path,
1013 &mut cycles,
1014 );
1015 }
1016 }
1017
1018 let mut unique_cycles: Vec<Vec<String>> = Vec::new();
1020 for cycle in cycles {
1021 let normalized = Self::normalize_cycle(&cycle);
1022 if !unique_cycles
1023 .iter()
1024 .any(|c| Self::normalize_cycle(c) == normalized)
1025 {
1026 unique_cycles.push(cycle);
1027 }
1028 }
1029
1030 unique_cycles
1031 }
1032
1033 fn dfs_find_cycles(
1035 &self,
1036 node: &str,
1037 graph: &HashMap<String, HashSet<String>>,
1038 visited: &mut HashSet<String>,
1039 rec_stack: &mut HashSet<String>,
1040 path: &mut Vec<String>,
1041 cycles: &mut Vec<Vec<String>>,
1042 ) {
1043 visited.insert(node.to_string());
1044 rec_stack.insert(node.to_string());
1045 path.push(node.to_string());
1046
1047 if let Some(neighbors) = graph.get(node) {
1048 for neighbor in neighbors {
1049 if !visited.contains(neighbor) {
1050 self.dfs_find_cycles(neighbor, graph, visited, rec_stack, path, cycles);
1051 } else if rec_stack.contains(neighbor) {
1052 if let Some(start_idx) = path.iter().position(|n| n == neighbor) {
1054 let cycle: Vec<String> = path[start_idx..].to_vec();
1055 if cycle.len() >= 2 {
1056 cycles.push(cycle);
1057 }
1058 }
1059 }
1060 }
1061 }
1062
1063 path.pop();
1064 rec_stack.remove(node);
1065 }
1066
1067 fn normalize_cycle(cycle: &[String]) -> Vec<String> {
1070 if cycle.is_empty() {
1071 return Vec::new();
1072 }
1073
1074 let min_pos = cycle
1076 .iter()
1077 .enumerate()
1078 .min_by_key(|(_, s)| s.as_str())
1079 .map(|(i, _)| i)
1080 .unwrap_or(0);
1081
1082 let mut normalized: Vec<String> = cycle[min_pos..].to_vec();
1084 normalized.extend_from_slice(&cycle[..min_pos]);
1085 normalized
1086 }
1087
1088 pub fn circular_dependency_summary(&self) -> CircularDependencySummary {
1090 let cycles = self.detect_circular_dependencies();
1091 let affected_modules: HashSet<String> = cycles.iter().flatten().cloned().collect();
1092
1093 CircularDependencySummary {
1094 total_cycles: cycles.len(),
1095 affected_modules: affected_modules.len(),
1096 cycles,
1097 }
1098 }
1099
1100 pub fn calculate_dimension_stats(&self) -> DimensionStats {
1105 let mut stats = DimensionStats::default();
1106
1107 for coupling in &self.couplings {
1108 match coupling.strength {
1110 IntegrationStrength::Intrusive => stats.strength_counts.intrusive += 1,
1111 IntegrationStrength::Functional => stats.strength_counts.functional += 1,
1112 IntegrationStrength::Model => stats.strength_counts.model += 1,
1113 IntegrationStrength::Contract => stats.strength_counts.contract += 1,
1114 }
1115
1116 match coupling.distance {
1118 Distance::SameFunction | Distance::SameModule => {
1119 stats.distance_counts.same_module += 1
1120 }
1121 Distance::DifferentModule => stats.distance_counts.different_module += 1,
1122 Distance::DifferentCrate => stats.distance_counts.different_crate += 1,
1123 }
1124
1125 match coupling.volatility {
1127 Volatility::Low => stats.volatility_counts.low += 1,
1128 Volatility::Medium => stats.volatility_counts.medium += 1,
1129 Volatility::High => stats.volatility_counts.high += 1,
1130 }
1131
1132 let classification = BalanceClassification::classify(
1134 coupling.strength,
1135 coupling.distance,
1136 coupling.volatility,
1137 );
1138 match classification {
1139 BalanceClassification::HighCohesion => stats.balance_counts.high_cohesion += 1,
1140 BalanceClassification::LooseCoupling => stats.balance_counts.loose_coupling += 1,
1141 BalanceClassification::Acceptable => stats.balance_counts.acceptable += 1,
1142 BalanceClassification::Pain => stats.balance_counts.pain += 1,
1143 BalanceClassification::LocalComplexity => {
1144 stats.balance_counts.local_complexity += 1
1145 }
1146 }
1147 }
1148
1149 stats
1150 }
1151
1152 pub fn total_newtype_count(&self) -> usize {
1154 self.modules.values().map(|m| m.newtype_count()).sum()
1155 }
1156
1157 pub fn total_type_count(&self) -> usize {
1159 self.modules
1160 .values()
1161 .flat_map(|m| m.type_definitions.values())
1162 .filter(|t| !t.is_trait)
1163 .count()
1164 }
1165
1166 pub fn newtype_ratio(&self) -> f64 {
1168 let total = self.total_type_count();
1169 if total == 0 {
1170 return 0.0;
1171 }
1172 self.total_newtype_count() as f64 / total as f64
1173 }
1174
1175 pub fn serde_types(&self) -> Vec<(&str, &TypeDefinition)> {
1177 self.modules
1178 .iter()
1179 .flat_map(|(module_name, m)| {
1180 m.type_definitions
1181 .values()
1182 .filter(|t| t.has_serde_derive)
1183 .map(move |t| (module_name.as_str(), t))
1184 })
1185 .collect()
1186 }
1187
1188 pub fn god_modules(
1190 &self,
1191 max_functions: usize,
1192 max_types: usize,
1193 max_impls: usize,
1194 ) -> Vec<&str> {
1195 self.modules
1196 .iter()
1197 .filter(|(_, m)| m.is_god_module(max_functions, max_types, max_impls))
1198 .map(|(name, _)| name.as_str())
1199 .collect()
1200 }
1201
1202 pub fn functions_with_primitive_obsession(&self) -> Vec<(&str, &FunctionDefinition)> {
1204 self.modules
1205 .iter()
1206 .flat_map(|(module_name, m)| {
1207 m.functions_with_primitive_obsession()
1208 .into_iter()
1209 .map(move |f| (module_name.as_str(), f))
1210 })
1211 .collect()
1212 }
1213
1214 pub fn types_with_public_fields(&self) -> Vec<(&str, &TypeDefinition)> {
1216 self.modules
1217 .iter()
1218 .flat_map(|(module_name, m)| {
1219 m.type_definitions
1220 .values()
1221 .filter(|t| t.public_field_count > 0 && !t.is_trait)
1222 .map(move |t| (module_name.as_str(), t))
1223 })
1224 .collect()
1225 }
1226}
1227
1228#[derive(Debug, Clone)]
1230pub struct CircularDependencySummary {
1231 pub total_cycles: usize,
1233 pub affected_modules: usize,
1235 pub cycles: Vec<Vec<String>>,
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::*;
1242
1243 #[test]
1244 fn test_integration_strength_values() {
1245 assert_eq!(IntegrationStrength::Intrusive.value(), 1.0);
1246 assert_eq!(IntegrationStrength::Contract.value(), 0.25);
1247 }
1248
1249 #[test]
1250 fn test_distance_values() {
1251 assert_eq!(Distance::SameFunction.value(), 0.0);
1252 assert_eq!(Distance::DifferentCrate.value(), 1.0);
1253 }
1254
1255 #[test]
1256 fn test_volatility_from_count() {
1257 assert_eq!(Volatility::from_count(0), Volatility::Low);
1258 assert_eq!(Volatility::from_count(5), Volatility::Medium);
1259 assert_eq!(Volatility::from_count(15), Volatility::High);
1260 }
1261
1262 #[test]
1263 fn test_module_metrics_average_strength() {
1264 let mut metrics = ModuleMetrics::new(PathBuf::from("test.rs"), "test".to_string());
1265 metrics.trait_impl_count = 3;
1266 metrics.inherent_impl_count = 1;
1267
1268 let avg = metrics.average_strength();
1269 assert!(avg > 0.0 && avg < 1.0);
1270 }
1271
1272 #[test]
1273 fn test_project_metrics() {
1274 let mut project = ProjectMetrics::new();
1275
1276 let module = ModuleMetrics::new(PathBuf::from("lib.rs"), "lib".to_string());
1277 project.add_module(module);
1278
1279 assert_eq!(project.module_count(), 1);
1280 assert_eq!(project.coupling_count(), 0);
1281 }
1282
1283 #[test]
1284 fn test_circular_dependency_detection() {
1285 let mut project = ProjectMetrics::new();
1286
1287 project.add_coupling(CouplingMetrics::new(
1289 "module_a".to_string(),
1290 "module_b".to_string(),
1291 IntegrationStrength::Model,
1292 Distance::DifferentModule,
1293 Volatility::Low,
1294 ));
1295 project.add_coupling(CouplingMetrics::new(
1296 "module_b".to_string(),
1297 "module_c".to_string(),
1298 IntegrationStrength::Model,
1299 Distance::DifferentModule,
1300 Volatility::Low,
1301 ));
1302 project.add_coupling(CouplingMetrics::new(
1303 "module_c".to_string(),
1304 "module_a".to_string(),
1305 IntegrationStrength::Model,
1306 Distance::DifferentModule,
1307 Volatility::Low,
1308 ));
1309
1310 let cycles = project.detect_circular_dependencies();
1311 assert_eq!(cycles.len(), 1);
1312 assert_eq!(cycles[0].len(), 3);
1313 }
1314
1315 #[test]
1316 fn test_no_circular_dependencies() {
1317 let mut project = ProjectMetrics::new();
1318
1319 project.add_coupling(CouplingMetrics::new(
1321 "module_a".to_string(),
1322 "module_b".to_string(),
1323 IntegrationStrength::Model,
1324 Distance::DifferentModule,
1325 Volatility::Low,
1326 ));
1327 project.add_coupling(CouplingMetrics::new(
1328 "module_b".to_string(),
1329 "module_c".to_string(),
1330 IntegrationStrength::Model,
1331 Distance::DifferentModule,
1332 Volatility::Low,
1333 ));
1334
1335 let cycles = project.detect_circular_dependencies();
1336 assert!(cycles.is_empty());
1337 }
1338
1339 #[test]
1340 fn test_external_crates_excluded_from_cycles() {
1341 let mut project = ProjectMetrics::new();
1342
1343 project.add_coupling(CouplingMetrics::new(
1345 "module_a".to_string(),
1346 "serde::Serialize".to_string(),
1347 IntegrationStrength::Contract,
1348 Distance::DifferentCrate, Volatility::Low,
1350 ));
1351 project.add_coupling(CouplingMetrics::new(
1352 "serde::Serialize".to_string(),
1353 "module_a".to_string(),
1354 IntegrationStrength::Contract,
1355 Distance::DifferentCrate, Volatility::Low,
1357 ));
1358
1359 let cycles = project.detect_circular_dependencies();
1360 assert!(cycles.is_empty());
1361 }
1362
1363 #[test]
1364 fn test_circular_dependency_summary() {
1365 let mut project = ProjectMetrics::new();
1366
1367 project.add_coupling(CouplingMetrics::new(
1369 "module_a".to_string(),
1370 "module_b".to_string(),
1371 IntegrationStrength::Functional,
1372 Distance::DifferentModule,
1373 Volatility::Low,
1374 ));
1375 project.add_coupling(CouplingMetrics::new(
1376 "module_b".to_string(),
1377 "module_a".to_string(),
1378 IntegrationStrength::Functional,
1379 Distance::DifferentModule,
1380 Volatility::Low,
1381 ));
1382
1383 let summary = project.circular_dependency_summary();
1384 assert!(summary.total_cycles > 0);
1385 assert!(summary.affected_modules >= 2);
1386 }
1387
1388 #[test]
1389 fn test_visibility_intrusive_detection() {
1390 assert!(!Visibility::Public.is_intrusive_from(true, false));
1392 assert!(!Visibility::Public.is_intrusive_from(false, false));
1393
1394 assert!(!Visibility::PubCrate.is_intrusive_from(true, false));
1396 assert!(Visibility::PubCrate.is_intrusive_from(false, false));
1397
1398 assert!(Visibility::Private.is_intrusive_from(true, false));
1400 assert!(Visibility::Private.is_intrusive_from(false, false));
1401
1402 assert!(!Visibility::Private.is_intrusive_from(true, true));
1404 assert!(!Visibility::Private.is_intrusive_from(false, true));
1405 }
1406
1407 #[test]
1408 fn test_visibility_penalty() {
1409 assert_eq!(Visibility::Public.intrusive_penalty(), 0.0);
1410 assert_eq!(Visibility::PubCrate.intrusive_penalty(), 0.25);
1411 assert_eq!(Visibility::Private.intrusive_penalty(), 1.0);
1412 }
1413
1414 #[test]
1415 fn test_effective_strength() {
1416 let coupling = CouplingMetrics::with_visibility(
1418 "source".to_string(),
1419 "target".to_string(),
1420 IntegrationStrength::Model,
1421 Distance::DifferentModule,
1422 Volatility::Low,
1423 Visibility::Public,
1424 );
1425 assert_eq!(coupling.effective_strength(), IntegrationStrength::Model);
1426
1427 let coupling = CouplingMetrics::with_visibility(
1429 "source".to_string(),
1430 "target".to_string(),
1431 IntegrationStrength::Model,
1432 Distance::DifferentModule,
1433 Volatility::Low,
1434 Visibility::Private,
1435 );
1436 assert_eq!(
1437 coupling.effective_strength(),
1438 IntegrationStrength::Functional
1439 );
1440 }
1441
1442 #[test]
1443 fn test_type_registry() {
1444 let mut project = ProjectMetrics::new();
1445
1446 project.register_type(
1447 "MyStruct".to_string(),
1448 "my_module".to_string(),
1449 Visibility::Public,
1450 );
1451 project.register_type(
1452 "InternalType".to_string(),
1453 "my_module".to_string(),
1454 Visibility::PubCrate,
1455 );
1456
1457 assert_eq!(
1458 project.get_type_visibility("MyStruct"),
1459 Some(Visibility::Public)
1460 );
1461 assert_eq!(
1462 project.get_type_visibility("InternalType"),
1463 Some(Visibility::PubCrate)
1464 );
1465 assert_eq!(project.get_type_visibility("Unknown"), None);
1466
1467 assert_eq!(project.get_type_module("MyStruct"), Some("my_module"));
1468 }
1469
1470 #[test]
1471 fn test_module_type_definitions() {
1472 let mut module = ModuleMetrics::new(PathBuf::from("test.rs"), "test".to_string());
1473
1474 module.add_type_definition("PublicStruct".to_string(), Visibility::Public, false);
1475 module.add_type_definition("PrivateStruct".to_string(), Visibility::Private, false);
1476 module.add_type_definition("PublicTrait".to_string(), Visibility::Public, true);
1477
1478 assert_eq!(module.public_type_count(), 2);
1479 assert_eq!(module.private_type_count(), 1);
1480 assert_eq!(
1481 module.get_type_visibility("PublicStruct"),
1482 Some(Visibility::Public)
1483 );
1484 }
1485
1486 #[test]
1487 fn test_update_volatility_from_git() {
1488 let mut project = ProjectMetrics::new();
1489
1490 project.add_coupling(CouplingMetrics::new(
1492 "crate::main".to_string(),
1493 "crate::balance".to_string(),
1494 IntegrationStrength::Functional,
1495 Distance::DifferentModule,
1496 Volatility::Low, ));
1498 project.add_coupling(CouplingMetrics::new(
1499 "crate::main".to_string(),
1500 "crate::analyzer".to_string(),
1501 IntegrationStrength::Functional,
1502 Distance::DifferentModule,
1503 Volatility::Low,
1504 ));
1505 project.add_coupling(CouplingMetrics::new(
1506 "crate::main".to_string(),
1507 "crate::report".to_string(),
1508 IntegrationStrength::Functional,
1509 Distance::DifferentModule,
1510 Volatility::Low,
1511 ));
1512
1513 project
1515 .file_changes
1516 .insert("src/balance.rs".to_string(), 15); project
1518 .file_changes
1519 .insert("src/analyzer.rs".to_string(), 7); project.file_changes.insert("src/report.rs".to_string(), 2); project.update_volatility_from_git();
1524
1525 let balance_coupling = project
1527 .couplings
1528 .iter()
1529 .find(|c| c.target == "crate::balance")
1530 .unwrap();
1531 assert_eq!(balance_coupling.volatility, Volatility::High);
1532
1533 let analyzer_coupling = project
1534 .couplings
1535 .iter()
1536 .find(|c| c.target == "crate::analyzer")
1537 .unwrap();
1538 assert_eq!(analyzer_coupling.volatility, Volatility::Medium);
1539
1540 let report_coupling = project
1541 .couplings
1542 .iter()
1543 .find(|c| c.target == "crate::report")
1544 .unwrap();
1545 assert_eq!(report_coupling.volatility, Volatility::Low);
1546 }
1547
1548 #[test]
1549 fn test_volatility_with_type_targets() {
1550 let mut project = ProjectMetrics::new();
1552
1553 project.add_coupling(CouplingMetrics::new(
1555 "crate::main".to_string(),
1556 "crate::balance::BalanceScore".to_string(), IntegrationStrength::Functional,
1558 Distance::DifferentModule,
1559 Volatility::Low,
1560 ));
1561 project.add_coupling(CouplingMetrics::new(
1562 "crate::main".to_string(),
1563 "cargo-coupling::analyzer::analyze_file".to_string(), IntegrationStrength::Functional,
1565 Distance::DifferentModule,
1566 Volatility::Low,
1567 ));
1568
1569 project
1571 .file_changes
1572 .insert("src/balance.rs".to_string(), 15); project
1574 .file_changes
1575 .insert("src/analyzer.rs".to_string(), 7); project.update_volatility_from_git();
1579
1580 let balance_coupling = project
1582 .couplings
1583 .iter()
1584 .find(|c| c.target.contains("balance"))
1585 .unwrap();
1586 assert_eq!(
1587 balance_coupling.volatility,
1588 Volatility::High,
1589 "Expected High volatility for balance module (15 changes)"
1590 );
1591
1592 let analyzer_coupling = project
1593 .couplings
1594 .iter()
1595 .find(|c| c.target.contains("analyzer"))
1596 .unwrap();
1597 assert_eq!(
1598 analyzer_coupling.volatility,
1599 Volatility::Medium,
1600 "Expected Medium volatility for analyzer module (7 changes)"
1601 );
1602 }
1603
1604 #[test]
1605 fn test_volatility_extracted_module_targets() {
1606 let mut project = ProjectMetrics::new();
1609
1610 project.add_coupling(CouplingMetrics::new(
1612 "cargo-coupling::main".to_string(),
1613 "balance".to_string(), IntegrationStrength::Functional,
1615 Distance::DifferentModule,
1616 Volatility::Low,
1617 ));
1618 project.add_coupling(CouplingMetrics::new(
1619 "cargo-coupling::main".to_string(),
1620 "analyzer".to_string(), IntegrationStrength::Functional,
1622 Distance::DifferentModule,
1623 Volatility::Low,
1624 ));
1625 project.add_coupling(CouplingMetrics::new(
1626 "cargo-coupling::main".to_string(),
1627 "cli_output".to_string(), IntegrationStrength::Functional,
1629 Distance::DifferentModule,
1630 Volatility::Low,
1631 ));
1632
1633 project
1635 .file_changes
1636 .insert("src/balance.rs".to_string(), 15); project
1638 .file_changes
1639 .insert("src/analyzer.rs".to_string(), 7); project
1641 .file_changes
1642 .insert("src/cli_output.rs".to_string(), 3); project.update_volatility_from_git();
1646
1647 let balance = project
1649 .couplings
1650 .iter()
1651 .find(|c| c.target == "balance")
1652 .unwrap();
1653 assert_eq!(
1654 balance.volatility,
1655 Volatility::High,
1656 "balance should be High (15 changes)"
1657 );
1658
1659 let analyzer = project
1660 .couplings
1661 .iter()
1662 .find(|c| c.target == "analyzer")
1663 .unwrap();
1664 assert_eq!(
1665 analyzer.volatility,
1666 Volatility::Medium,
1667 "analyzer should be Medium (7 changes)"
1668 );
1669
1670 let cli_output = project
1671 .couplings
1672 .iter()
1673 .find(|c| c.target == "cli_output")
1674 .unwrap();
1675 assert_eq!(
1676 cli_output.volatility,
1677 Volatility::Medium,
1678 "cli_output should be Medium (3 changes)"
1679 );
1680 }
1681}