use pedant_graph::{
CachedGraphAnalysis, CodeGraph, CondensationGraph, DeclaredModulePartition, GraphAnalysis,
GraphAnalysisError, GraphComponents, GraphDirection, GraphDivergence, GraphNodeId, GraphPath,
GraphSubgraph, LayoutAssistMetadata,
};
use super::analysis_fixture::{
betweenness_texts, boundary_texts, candidate_texts, cohesion_texts, component_texts,
condensation_texts, degree_texts, edge_texts, layout_edge_texts, layout_node_texts,
membership_texts, name, names, neighbor_texts, optional_bits, optional_name,
};
#[derive(Clone, Copy)]
pub struct Walk {
node: GraphNodeId,
depth: u32,
direction: GraphDirection,
}
impl Walk {
pub fn new(node: GraphNodeId, depth: u32, direction: GraphDirection) -> Self {
Self {
node,
depth,
direction,
}
}
fn label(self, operation: &str) -> String {
format!(
"{operation}|{}|{}|{:?}",
self.node.index(),
self.depth,
self.direction
)
}
}
pub struct Asked {
walks: Box<[Walk]>,
routes: Box<[(GraphNodeId, GraphNodeId)]>,
}
impl Asked {
pub fn over(graph: &CodeGraph) -> Self {
let held: Box<[GraphNodeId]> = graph.nodes().iter().map(|node| node.id()).take(6).collect();
let directions = [
GraphDirection::Outgoing,
GraphDirection::Incoming,
GraphDirection::Both,
];
let walks = held
.iter()
.flat_map(|node| {
directions.iter().flat_map(move |direction| {
[0_u32, 1, 3]
.iter()
.map(move |depth| Walk::new(*node, *depth, *direction))
})
})
.collect();
let routes = ordered_pairs(&held).collect();
Self { walks, routes }
}
pub fn one(walk: Walk, route: (GraphNodeId, GraphNodeId)) -> Self {
Self {
walks: Box::new([walk]),
routes: Box::new([route]),
}
}
}
pub trait AnalysisReading {
fn read_graph(&self) -> &CodeGraph;
fn read_partition(&self) -> &DeclaredModulePartition;
fn neighbor_row(&self, walk: Walk) -> String;
fn subgraph_row(&self, walk: Walk) -> String;
fn route_row(&self, route: (GraphNodeId, GraphNodeId)) -> String;
fn derived_rows(&self) -> Vec<(String, String)>;
fn degree_row(&self) -> String;
fn betweenness_row(&self) -> String;
fn layout_row(&self) -> String;
}
impl AnalysisReading for GraphAnalysis<'_> {
fn read_graph(&self) -> &CodeGraph {
self.graph()
}
fn read_partition(&self) -> &DeclaredModulePartition {
self.declared_partition()
}
fn neighbor_row(&self, walk: Walk) -> String {
answered(
self.neighbors(walk.node, walk.depth, walk.direction),
|held| neighbor_texts(self.graph(), &held).join(","),
)
}
fn subgraph_row(&self, walk: Walk) -> String {
answered(
self.subgraph(walk.node, walk.depth, walk.direction),
|held| subgraph_text(self.graph(), &held),
)
}
fn route_row(&self, route: (GraphNodeId, GraphNodeId)) -> String {
answered(self.path(route.0, route.1), |held| {
optional_route_text(self.graph(), held.as_ref())
})
}
fn derived_rows(&self) -> Vec<(String, String)> {
derived_rows(
self.graph(),
(
&self.strongly_connected_components(),
&self.condensation(),
&self.divergence(),
),
)
}
fn degree_row(&self) -> String {
degree_texts(self.graph(), &self.degree_centrality()).join(",")
}
fn betweenness_row(&self) -> String {
answered(self.betweenness_centrality(), |held| {
betweenness_texts(self.graph(), &held).join(",")
})
}
fn layout_row(&self) -> String {
answered(self.layout_assist(), |held| {
layout_text(self.graph(), &held)
})
}
}
impl AnalysisReading for CachedGraphAnalysis {
fn read_graph(&self) -> &CodeGraph {
self.graph()
}
fn read_partition(&self) -> &DeclaredModulePartition {
self.declared_partition()
}
fn neighbor_row(&self, walk: Walk) -> String {
answered(
self.neighbors(walk.node, walk.depth, walk.direction),
|held| neighbor_texts(self.graph(), &held).join(","),
)
}
fn subgraph_row(&self, walk: Walk) -> String {
answered(
self.subgraph(walk.node, walk.depth, walk.direction),
|held| subgraph_text(self.graph(), &held),
)
}
fn route_row(&self, route: (GraphNodeId, GraphNodeId)) -> String {
answered(self.path(route.0, route.1), |held| {
optional_route_text(self.graph(), held.as_deref())
})
}
fn derived_rows(&self) -> Vec<(String, String)> {
derived_rows(
self.graph(),
(
&self.strongly_connected_components(),
&self.condensation(),
&self.divergence(),
),
)
}
fn degree_row(&self) -> String {
degree_texts(self.graph(), &self.degree_centrality()).join(",")
}
fn betweenness_row(&self) -> String {
answered(self.betweenness_centrality(), |held| {
betweenness_texts(self.graph(), &held).join(",")
})
}
fn layout_row(&self) -> String {
answered(self.layout_assist(), |held| {
layout_text(self.graph(), &held)
})
}
}
pub fn rows(analysis: &impl AnalysisReading, asked: &Asked) -> Vec<(String, String)> {
let mut rows = vec![(
"partition".to_owned(),
partition_text(analysis.read_graph(), analysis.read_partition()),
)];
for walk in &asked.walks {
rows.push((walk.label("neighbors"), analysis.neighbor_row(*walk)));
rows.push((walk.label("subgraph"), analysis.subgraph_row(*walk)));
}
for route in &asked.routes {
rows.push((route_label(*route), analysis.route_row(*route)));
}
rows.extend(analysis.derived_rows());
rows.push(("degree".to_owned(), analysis.degree_row()));
rows.push(("betweenness".to_owned(), analysis.betweenness_row()));
rows.push(("layout".to_owned(), analysis.layout_row()));
rows
}
pub fn readings(analysis: &impl AnalysisReading, seed: GraphNodeId) -> Vec<(String, String)> {
rows(
analysis,
&Asked::one(Walk::new(seed, 2, GraphDirection::Both), (seed, seed)),
)
}
pub fn first_node(graph: &CodeGraph) -> GraphNodeId {
graph
.nodes()
.first()
.unwrap_or_else(|| panic!("the corpus states nodes"))
.id()
}
pub fn next_node(graph: &CodeGraph, node: GraphNodeId) -> GraphNodeId {
let nodes = graph.nodes();
let at = nodes
.iter()
.position(|found| found.id() == node)
.unwrap_or_else(|| panic!("this graph holds no node {}", node.index()));
nodes[(at + 1) % nodes.len()].id()
}
pub fn node_ids(graph: &CodeGraph) -> Box<[GraphNodeId]> {
graph.nodes().iter().map(|node| node.id()).collect()
}
pub fn ordered_pairs(
nodes: &[GraphNodeId],
) -> impl Iterator<Item = (GraphNodeId, GraphNodeId)> + '_ {
nodes
.iter()
.flat_map(move |source| nodes.iter().map(move |target| (*source, *target)))
}
pub fn answered<T>(
answer: Result<T, GraphAnalysisError>,
render: impl FnOnce(T) -> String,
) -> String {
match answer {
Ok(held) => render(held),
Err(refused) => format!("refused|{refused}"),
}
}
fn derived_rows(
graph: &CodeGraph,
derived: (&GraphComponents, &CondensationGraph, &GraphDivergence),
) -> Vec<(String, String)> {
let (components, condensation, divergence) = derived;
vec![
(
"components".to_owned(),
format!(
"{}|{}",
component_texts(graph, components.components()).join(","),
membership_texts(graph, components).join(",")
),
),
(
"condensation".to_owned(),
format!(
"{}|{}",
condensation_texts(graph, condensation.edges()).join(","),
condensation
.topological_order()
.iter()
.map(|component| component.index().to_string())
.collect::<Vec<String>>()
.join(",")
),
),
(
"divergence".to_owned(),
format!(
"{}|{}|{}|{}",
cohesion_texts(graph, divergence.cohesion()).join(","),
optional_bits(divergence.modularity()),
candidate_texts(graph, divergence.candidates()).join(","),
boundary_texts(graph, components, divergence.boundary_components()).join(",")
),
),
]
}
fn route_label(route: (GraphNodeId, GraphNodeId)) -> String {
let (source, target) = route;
format!("path|{}>{}", source.index(), target.index())
}
fn partition_text(graph: &CodeGraph, partition: &DeclaredModulePartition) -> String {
let grouped: Vec<String> = partition
.roots()
.iter()
.map(|root| {
let members = partition
.members(*root)
.unwrap_or_else(|| panic!("root {} owns no members", root.index()));
format!("{}:{}", name(graph, *root), names(graph, members).join("+"))
})
.collect();
let owners: Vec<String> = graph
.nodes()
.iter()
.map(|node| optional_name(graph, partition.owner(node.id())))
.collect();
format!("{}|{}", grouped.join(","), owners.join(","))
}
fn subgraph_text(graph: &CodeGraph, held: &GraphSubgraph) -> String {
let containment: Vec<String> = held
.containment()
.iter()
.map(|edge| {
format!(
"{}>{}",
name(graph, edge.parent()),
name(graph, edge.child())
)
})
.collect();
format!(
"{}|{}|{}",
names(graph, held.nodes()).join(","),
edge_texts(graph, held.edges()).join(","),
containment.join(",")
)
}
fn optional_route_text(graph: &CodeGraph, held: Option<&GraphPath>) -> String {
match held {
Some(route) => format!(
"{}|{}",
names(graph, route.nodes()).join(","),
edge_texts(graph, route.edges()).join(",")
),
None => "unreachable".to_owned(),
}
}
fn layout_text(graph: &CodeGraph, held: &LayoutAssistMetadata) -> String {
format!(
"{}|{}",
layout_node_texts(graph, held.nodes()).join(","),
layout_edge_texts(graph, held.edges()).join(",")
)
}