1use std::collections::{BTreeMap, BTreeSet};
15
16use crate::graph::CodeGraph;
17use crate::id::{GraphNodeId, position};
18use crate::node::GraphNodeKind;
19
20use super::components::{self, GraphComponent, GraphComponentId, GraphComponents};
21use super::degree::{self, DegreeCentrality};
22use super::index::{AnalysisContext, SelectedAdjacency, SelectedIndexes};
23use super::partition::DeclaredModulePartition;
24
25#[derive(Clone, Copy, Debug)]
27pub struct ModuleCohesion {
28 root: GraphNodeId,
29 internal: u32,
30 boundary: u32,
31 score: Option<f64>,
32}
33
34impl ModuleCohesion {
35 pub fn root(self) -> GraphNodeId {
37 self.root
38 }
39
40 pub fn internal_edges(self) -> u32 {
42 self.internal
43 }
44
45 pub fn boundary_edges(self) -> u32 {
47 self.boundary
48 }
49
50 pub fn score(self) -> Option<f64> {
53 self.score
54 }
55}
56
57#[derive(Clone, Copy, Debug)]
66pub struct MisplacementCandidate {
67 symbol: GraphNodeId,
68 declared: GraphNodeId,
69 candidate: GraphNodeId,
70 foreign: u32,
71 total: u32,
72 affinity: f64,
73}
74
75impl MisplacementCandidate {
76 pub fn symbol(self) -> GraphNodeId {
78 self.symbol
79 }
80
81 pub fn declared_partition(self) -> GraphNodeId {
83 self.declared
84 }
85
86 pub fn candidate_partition(self) -> GraphNodeId {
88 self.candidate
89 }
90
91 pub fn foreign_edges(self) -> u32 {
93 self.foreign
94 }
95
96 pub fn total_outgoing_edges(self) -> u32 {
98 self.total
99 }
100
101 pub fn affinity(self) -> f64 {
103 self.affinity
104 }
105}
106
107#[derive(Debug)]
109pub struct BoundaryCrossingComponent {
110 component: GraphComponentId,
111 partitions: Box<[GraphNodeId]>,
112}
113
114impl BoundaryCrossingComponent {
115 pub fn component(&self) -> GraphComponentId {
117 self.component
118 }
119
120 pub fn partitions(&self) -> &[GraphNodeId] {
122 &self.partitions
123 }
124}
125
126#[derive(Debug)]
128pub struct GraphDivergence {
129 cohesion: Box<[ModuleCohesion]>,
130 modularity: Option<f64>,
131 candidates: Box<[MisplacementCandidate]>,
132 boundary: Box<[BoundaryCrossingComponent]>,
133}
134
135impl GraphDivergence {
136 pub fn cohesion(&self) -> &[ModuleCohesion] {
138 &self.cohesion
139 }
140
141 pub fn modularity(&self) -> Option<f64> {
144 self.modularity
145 }
146
147 pub fn candidates(&self) -> &[MisplacementCandidate] {
149 &self.candidates
150 }
151
152 pub fn boundary_components(&self) -> &[BoundaryCrossingComponent] {
155 &self.boundary
156 }
157}
158
159pub(crate) fn divergence(analysis: &AnalysisContext<'_>) -> GraphDivergence {
165 let indexes = analysis.indexes();
166 let partition = analysis.declared_partition();
167 let cohesion = measured(indexes, partition);
168 let summed = totals(°ree::degree(analysis), partition);
169 GraphDivergence {
170 modularity: modularity(&cohesion, &summed, indexes.selected_edges()),
171 candidates: candidates(analysis.graph(), indexes, partition),
172 boundary: crossings(&components::discover(analysis), partition),
173 cohesion,
174 }
175}
176
177fn measured(
179 indexes: &SelectedIndexes,
180 partition: &DeclaredModulePartition,
181) -> Box<[ModuleCohesion]> {
182 let mut counted: BTreeMap<GraphNodeId, (u32, u32)> = empty_counts(partition.roots());
183 for (at, owner) in partition.owners().iter().enumerate() {
184 let node = GraphNodeId::new(position(at));
185 if let Some(pair) = counted.get_mut(owner) {
186 record(pair, *owner, indexes.outgoing(node), partition);
187 }
188 }
189 counted.into_iter().map(stated_cohesion).collect()
190}
191
192fn empty_counts(roots: &[GraphNodeId]) -> BTreeMap<GraphNodeId, (u32, u32)> {
194 roots.iter().map(|root| (*root, (0, 0))).collect()
195}
196
197fn record(
202 counted: &mut (u32, u32),
203 owner: GraphNodeId,
204 leaving: &[SelectedAdjacency],
205 partition: &DeclaredModulePartition,
206) {
207 for entry in leaving {
208 match partition.owner(entry.node()) == Some(owner) {
209 true => counted.0 += 1,
210 false => counted.1 += 1,
211 }
212 }
213}
214
215fn stated_cohesion(counted: (GraphNodeId, (u32, u32))) -> ModuleCohesion {
217 let (root, (internal, boundary)) = counted;
218 ModuleCohesion {
219 root,
220 internal,
221 boundary,
222 score: share(internal, internal + boundary),
223 }
224}
225
226fn share(part: u32, total: u32) -> Option<f64> {
228 match total {
229 0 => None,
230 _ => Some(f64::from(part) / f64::from(total)),
231 }
232}
233
234fn totals(degrees: &[DegreeCentrality], partition: &DeclaredModulePartition) -> Box<[(f64, f64)]> {
240 let mut summed: BTreeMap<GraphNodeId, (f64, f64)> = partition
241 .roots()
242 .iter()
243 .map(|root| (*root, (0.0, 0.0)))
244 .collect();
245 for (owner, degree) in partition.owners().iter().zip(degrees) {
246 accumulate(&mut summed, *owner, *degree);
247 }
248 summed.into_values().collect()
249}
250
251fn accumulate(
253 summed: &mut BTreeMap<GraphNodeId, (f64, f64)>,
254 owner: GraphNodeId,
255 degree: DegreeCentrality,
256) {
257 if let Some(pair) = summed.get_mut(&owner) {
258 pair.0 += f64::from(degree.outgoing());
259 pair.1 += f64::from(degree.incoming());
260 }
261}
262
263fn modularity(cohesion: &[ModuleCohesion], summed: &[(f64, f64)], edges: u32) -> Option<f64> {
266 match edges {
267 0 => None,
268 _ => Some(newman(cohesion, summed, f64::from(edges))),
269 }
270}
271
272fn newman(cohesion: &[ModuleCohesion], summed: &[(f64, f64)], edges: f64) -> f64 {
279 let internal: f64 = cohesion
280 .iter()
281 .map(|stated| f64::from(stated.internal))
282 .sum();
283 let expected: f64 = summed
284 .iter()
285 .map(|(leaving, arriving)| leaving * arriving)
286 .sum();
287 internal / edges - expected / (edges * edges)
288}
289
290fn candidates(
292 graph: &CodeGraph,
293 indexes: &SelectedIndexes,
294 partition: &DeclaredModulePartition,
295) -> Box<[MisplacementCandidate]> {
296 graph
297 .nodes()
298 .iter()
299 .filter(|node| is_symbol(node.kind()))
300 .flat_map(|node| foreign(indexes, partition, node.id()))
301 .collect()
302}
303
304fn is_symbol(kind: &GraphNodeKind) -> bool {
311 match kind {
312 GraphNodeKind::Function { .. }
313 | GraphNodeKind::Type { .. }
314 | GraphNodeKind::Value { .. } => true,
315 GraphNodeKind::File | GraphNodeKind::Container { .. } => false,
316 }
317}
318
319fn foreign<'a>(
322 indexes: &'a SelectedIndexes,
323 partition: &'a DeclaredModulePartition,
324 symbol: GraphNodeId,
325) -> impl Iterator<Item = MisplacementCandidate> + 'a {
326 partition
327 .owner(symbol)
328 .into_iter()
329 .flat_map(move |declared| reaching(indexes, partition, (symbol, declared)))
330}
331
332fn reaching(
338 indexes: &SelectedIndexes,
339 partition: &DeclaredModulePartition,
340 declaration: (GraphNodeId, GraphNodeId),
341) -> impl Iterator<Item = MisplacementCandidate> {
342 let (symbol, declared) = declaration;
343 let leaving = indexes.outgoing(symbol);
344 let total = position(leaving.len());
345 let mut reached: BTreeMap<GraphNodeId, u32> = BTreeMap::new();
346 for entry in leaving {
347 claim(&mut reached, partition.owner(entry.node()), declared);
348 }
349 reached
350 .into_iter()
351 .map(move |found| stated_candidate((symbol, declared), found, total))
352}
353
354fn claim(
356 reached: &mut BTreeMap<GraphNodeId, u32>,
357 owner: Option<GraphNodeId>,
358 declared: GraphNodeId,
359) {
360 match owner {
361 Some(found) if found != declared => *reached.entry(found).or_insert(0) += 1,
362 _ => (),
363 }
364}
365
366fn stated_candidate(
368 declaration: (GraphNodeId, GraphNodeId),
369 reached: (GraphNodeId, u32),
370 total: u32,
371) -> MisplacementCandidate {
372 let (symbol, declared) = declaration;
373 let (candidate, foreign) = reached;
374 MisplacementCandidate {
375 symbol,
376 declared,
377 candidate,
378 foreign,
379 total,
380 affinity: f64::from(foreign) / f64::from(total),
381 }
382}
383
384fn crossings(
386 components: &GraphComponents,
387 partition: &DeclaredModulePartition,
388) -> Box<[BoundaryCrossingComponent]> {
389 components
390 .components()
391 .iter()
392 .filter_map(|component| crossing(component, partition))
393 .collect()
394}
395
396fn crossing(
398 component: &GraphComponent,
399 partition: &DeclaredModulePartition,
400) -> Option<BoundaryCrossingComponent> {
401 let partitions: BTreeSet<GraphNodeId> = component
402 .members()
403 .iter()
404 .filter_map(|member| partition.owner(*member))
405 .collect();
406 match partitions.len() > 1 {
407 false => None,
408 true => Some(BoundaryCrossingComponent {
409 component: component.id(),
410 partitions: partitions.into_iter().collect(),
411 }),
412 }
413}