use std::collections::{BTreeMap, BTreeSet};
use crate::graph::CodeGraph;
use crate::id::{GraphNodeId, position};
use crate::node::GraphNodeKind;
use super::components::{self, GraphComponent, GraphComponentId, GraphComponents};
use super::degree::{self, DegreeCentrality};
use super::index::{AnalysisContext, SelectedAdjacency, SelectedIndexes};
use super::partition::DeclaredModulePartition;
#[derive(Clone, Copy, Debug)]
pub struct ModuleCohesion {
root: GraphNodeId,
internal: u32,
boundary: u32,
score: Option<f64>,
}
impl ModuleCohesion {
pub fn root(self) -> GraphNodeId {
self.root
}
pub fn internal_edges(self) -> u32 {
self.internal
}
pub fn boundary_edges(self) -> u32 {
self.boundary
}
pub fn score(self) -> Option<f64> {
self.score
}
}
#[derive(Clone, Copy, Debug)]
pub struct MisplacementCandidate {
symbol: GraphNodeId,
declared: GraphNodeId,
candidate: GraphNodeId,
foreign: u32,
total: u32,
affinity: f64,
}
impl MisplacementCandidate {
pub fn symbol(self) -> GraphNodeId {
self.symbol
}
pub fn declared_partition(self) -> GraphNodeId {
self.declared
}
pub fn candidate_partition(self) -> GraphNodeId {
self.candidate
}
pub fn foreign_edges(self) -> u32 {
self.foreign
}
pub fn total_outgoing_edges(self) -> u32 {
self.total
}
pub fn affinity(self) -> f64 {
self.affinity
}
}
#[derive(Debug)]
pub struct BoundaryCrossingComponent {
component: GraphComponentId,
partitions: Box<[GraphNodeId]>,
}
impl BoundaryCrossingComponent {
pub fn component(&self) -> GraphComponentId {
self.component
}
pub fn partitions(&self) -> &[GraphNodeId] {
&self.partitions
}
}
#[derive(Debug)]
pub struct GraphDivergence {
cohesion: Box<[ModuleCohesion]>,
modularity: Option<f64>,
candidates: Box<[MisplacementCandidate]>,
boundary: Box<[BoundaryCrossingComponent]>,
}
impl GraphDivergence {
pub fn cohesion(&self) -> &[ModuleCohesion] {
&self.cohesion
}
pub fn modularity(&self) -> Option<f64> {
self.modularity
}
pub fn candidates(&self) -> &[MisplacementCandidate] {
&self.candidates
}
pub fn boundary_components(&self) -> &[BoundaryCrossingComponent] {
&self.boundary
}
}
pub(crate) fn divergence(analysis: &AnalysisContext<'_>) -> GraphDivergence {
let indexes = analysis.indexes();
let partition = analysis.declared_partition();
let cohesion = measured(indexes, partition);
let summed = totals(°ree::degree(analysis), partition);
GraphDivergence {
modularity: modularity(&cohesion, &summed, indexes.selected_edges()),
candidates: candidates(analysis.graph(), indexes, partition),
boundary: crossings(&components::discover(analysis), partition),
cohesion,
}
}
fn measured(
indexes: &SelectedIndexes,
partition: &DeclaredModulePartition,
) -> Box<[ModuleCohesion]> {
let mut counted: BTreeMap<GraphNodeId, (u32, u32)> = empty_counts(partition.roots());
for (at, owner) in partition.owners().iter().enumerate() {
let node = GraphNodeId::new(position(at));
if let Some(pair) = counted.get_mut(owner) {
record(pair, *owner, indexes.outgoing(node), partition);
}
}
counted.into_iter().map(stated_cohesion).collect()
}
fn empty_counts(roots: &[GraphNodeId]) -> BTreeMap<GraphNodeId, (u32, u32)> {
roots.iter().map(|root| (*root, (0, 0))).collect()
}
fn record(
counted: &mut (u32, u32),
owner: GraphNodeId,
leaving: &[SelectedAdjacency],
partition: &DeclaredModulePartition,
) {
for entry in leaving {
match partition.owner(entry.node()) == Some(owner) {
true => counted.0 += 1,
false => counted.1 += 1,
}
}
}
fn stated_cohesion(counted: (GraphNodeId, (u32, u32))) -> ModuleCohesion {
let (root, (internal, boundary)) = counted;
ModuleCohesion {
root,
internal,
boundary,
score: share(internal, internal + boundary),
}
}
fn share(part: u32, total: u32) -> Option<f64> {
match total {
0 => None,
_ => Some(f64::from(part) / f64::from(total)),
}
}
fn totals(degrees: &[DegreeCentrality], partition: &DeclaredModulePartition) -> Box<[(f64, f64)]> {
let mut summed: BTreeMap<GraphNodeId, (f64, f64)> = partition
.roots()
.iter()
.map(|root| (*root, (0.0, 0.0)))
.collect();
for (owner, degree) in partition.owners().iter().zip(degrees) {
accumulate(&mut summed, *owner, *degree);
}
summed.into_values().collect()
}
fn accumulate(
summed: &mut BTreeMap<GraphNodeId, (f64, f64)>,
owner: GraphNodeId,
degree: DegreeCentrality,
) {
if let Some(pair) = summed.get_mut(&owner) {
pair.0 += f64::from(degree.outgoing());
pair.1 += f64::from(degree.incoming());
}
}
fn modularity(cohesion: &[ModuleCohesion], summed: &[(f64, f64)], edges: u32) -> Option<f64> {
match edges {
0 => None,
_ => Some(newman(cohesion, summed, f64::from(edges))),
}
}
fn newman(cohesion: &[ModuleCohesion], summed: &[(f64, f64)], edges: f64) -> f64 {
let internal: f64 = cohesion
.iter()
.map(|stated| f64::from(stated.internal))
.sum();
let expected: f64 = summed
.iter()
.map(|(leaving, arriving)| leaving * arriving)
.sum();
internal / edges - expected / (edges * edges)
}
fn candidates(
graph: &CodeGraph,
indexes: &SelectedIndexes,
partition: &DeclaredModulePartition,
) -> Box<[MisplacementCandidate]> {
graph
.nodes()
.iter()
.filter(|node| is_symbol(node.kind()))
.flat_map(|node| foreign(indexes, partition, node.id()))
.collect()
}
fn is_symbol(kind: &GraphNodeKind) -> bool {
match kind {
GraphNodeKind::Function { .. }
| GraphNodeKind::Type { .. }
| GraphNodeKind::Value { .. } => true,
GraphNodeKind::File | GraphNodeKind::Container { .. } => false,
}
}
fn foreign<'a>(
indexes: &'a SelectedIndexes,
partition: &'a DeclaredModulePartition,
symbol: GraphNodeId,
) -> impl Iterator<Item = MisplacementCandidate> + 'a {
partition
.owner(symbol)
.into_iter()
.flat_map(move |declared| reaching(indexes, partition, (symbol, declared)))
}
fn reaching(
indexes: &SelectedIndexes,
partition: &DeclaredModulePartition,
declaration: (GraphNodeId, GraphNodeId),
) -> impl Iterator<Item = MisplacementCandidate> {
let (symbol, declared) = declaration;
let leaving = indexes.outgoing(symbol);
let total = position(leaving.len());
let mut reached: BTreeMap<GraphNodeId, u32> = BTreeMap::new();
for entry in leaving {
claim(&mut reached, partition.owner(entry.node()), declared);
}
reached
.into_iter()
.map(move |found| stated_candidate((symbol, declared), found, total))
}
fn claim(
reached: &mut BTreeMap<GraphNodeId, u32>,
owner: Option<GraphNodeId>,
declared: GraphNodeId,
) {
match owner {
Some(found) if found != declared => *reached.entry(found).or_insert(0) += 1,
_ => (),
}
}
fn stated_candidate(
declaration: (GraphNodeId, GraphNodeId),
reached: (GraphNodeId, u32),
total: u32,
) -> MisplacementCandidate {
let (symbol, declared) = declaration;
let (candidate, foreign) = reached;
MisplacementCandidate {
symbol,
declared,
candidate,
foreign,
total,
affinity: f64::from(foreign) / f64::from(total),
}
}
fn crossings(
components: &GraphComponents,
partition: &DeclaredModulePartition,
) -> Box<[BoundaryCrossingComponent]> {
components
.components()
.iter()
.filter_map(|component| crossing(component, partition))
.collect()
}
fn crossing(
component: &GraphComponent,
partition: &DeclaredModulePartition,
) -> Option<BoundaryCrossingComponent> {
let partitions: BTreeSet<GraphNodeId> = component
.members()
.iter()
.filter_map(|member| partition.owner(*member))
.collect();
match partitions.len() > 1 {
false => None,
true => Some(BoundaryCrossingComponent {
component: component.id(),
partitions: partitions.into_iter().collect(),
}),
}
}