1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use super::Module;
4
5#[derive(Debug, Clone)]
7pub struct LayerInfo {
8 pub name: String,
9 pub level: usize,
10 pub file_count: usize,
11 pub fan_in: usize,
12 pub fan_out: usize,
13 pub instability: f64,
14 pub lcom4: usize,
15 pub tcc: f64,
16 pub cohesion: f64,
17}
18
19#[derive(Debug, Clone)]
21pub struct LayerViolation {
22 pub from_module: String,
23 pub to_module: String,
24 pub from_level: usize,
25 pub to_level: usize,
26}
27
28pub fn infer_layers(
31 modules: &[Module],
32 file_imports: &[(String, String)],
33) -> (Vec<LayerInfo>, Vec<LayerViolation>) {
34 let file_to_mod: HashMap<&str, &str> = modules
35 .iter()
36 .flat_map(|m| m.files.iter().map(|f| (f.as_str(), m.name.as_str())))
37 .collect();
38
39 let (fan_in, fan_out) = compute_module_fans(&file_to_mod, file_imports);
40 let mut layers = build_layers(modules, &fan_in, &fan_out);
41
42 layers.sort_by(|a, b| a.instability.partial_cmp(&b.instability).unwrap());
43 for (i, l) in layers.iter_mut().enumerate() {
44 l.level = i;
45 }
46
47 let violations = detect_violations(&layers, &file_to_mod, file_imports);
48 (layers, violations)
49}
50
51type FanMap<'a> = HashMap<&'a str, HashSet<&'a str>>;
52
53fn compute_module_fans<'a>(
54 file_to_mod: &HashMap<&'a str, &'a str>,
55 file_imports: &[(String, String)],
56) -> (FanMap<'a>, FanMap<'a>) {
57 let mut fan_in: HashMap<&str, HashSet<&str>> = HashMap::new();
58 let mut fan_out: HashMap<&str, HashSet<&str>> = HashMap::new();
59 for (from, to) in file_imports {
60 let fm = file_to_mod.get(from.as_str()).copied().unwrap_or("");
61 let tm = file_to_mod.get(to.as_str()).copied().unwrap_or("");
62 if !fm.is_empty() && !tm.is_empty() && fm != tm {
63 fan_out.entry(fm).or_default().insert(tm);
64 fan_in.entry(tm).or_default().insert(fm);
65 }
66 }
67 (fan_in, fan_out)
68}
69
70fn build_layers(
71 modules: &[Module],
72 fan_in: &HashMap<&str, HashSet<&str>>,
73 fan_out: &HashMap<&str, HashSet<&str>>,
74) -> Vec<LayerInfo> {
75 modules
76 .iter()
77 .map(|m| {
78 let fi = fan_in.get(m.name.as_str()).map(|s| s.len()).unwrap_or(0);
79 let fo = fan_out.get(m.name.as_str()).map(|s| s.len()).unwrap_or(0);
80 let total = fi + fo;
81 LayerInfo {
82 name: m.name.clone(),
83 level: 0,
84 file_count: m.files.len(),
85 fan_in: fi,
86 fan_out: fo,
87 instability: if total > 0 {
88 fo as f64 / total as f64
89 } else {
90 0.5
91 },
92 lcom4: m.lcom4,
93 tcc: m.tcc,
94 cohesion: m.cohesion,
95 }
96 })
97 .collect()
98}
99
100fn is_parent_child(a: &str, b: &str) -> bool {
101 let a = a.trim_end_matches("/*").trim_end_matches('/');
102 let b = b.trim_end_matches("/*").trim_end_matches('/');
103 a.starts_with(&format!("{b}/")) || b.starts_with(&format!("{a}/"))
104}
105
106fn detect_violations(
107 layers: &[LayerInfo],
108 file_to_mod: &HashMap<&str, &str>,
109 file_imports: &[(String, String)],
110) -> Vec<LayerViolation> {
111 let level_map: BTreeMap<&str, usize> =
112 layers.iter().map(|l| (l.name.as_str(), l.level)).collect();
113
114 let mut seen: BTreeMap<(&str, &str), (usize, usize)> = BTreeMap::new();
115 for (from, to) in file_imports {
116 let fm = file_to_mod.get(from.as_str()).copied().unwrap_or("");
117 let tm = file_to_mod.get(to.as_str()).copied().unwrap_or("");
118 if fm == tm || fm.is_empty() || tm.is_empty() {
119 continue;
120 }
121 if is_parent_child(fm, tm) {
123 continue;
124 }
125 if let (Some(&fl), Some(&tl)) = (level_map.get(fm), level_map.get(tm))
126 && fl < tl
127 {
128 seen.entry((fm, tm)).or_insert((fl, tl));
129 }
130 }
131
132 seen.into_iter()
133 .map(|((from, to), (fl, tl))| LayerViolation {
134 from_module: from.to_string(),
135 to_module: to.to_string(),
136 from_level: fl,
137 to_level: tl,
138 })
139 .collect()
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn stable_module_importing_volatile_is_violation() {
148 let modules = vec![
149 Module {
150 name: "core".into(),
151 files: vec!["core/a.rs".into()],
152 lcom4: 1,
153 tcc: 1.0,
154 cohesion: 1.0,
155 },
156 Module {
157 name: "ui".into(),
158 files: vec!["ui/b.rs".into()],
159 lcom4: 1,
160 tcc: 1.0,
161 cohesion: 1.0,
162 },
163 ];
164 let imports = vec![("core/a.rs".into(), "ui/b.rs".into())];
170 let (layers, violations) = infer_layers(&modules, &imports);
171 assert_eq!(layers.len(), 2);
172 assert!(violations.is_empty());
174 }
175}