use pedant_core::resolution::rust::CargoTargetKind;
use pedant_graph::{
BetweennessCentrality, BoundaryCrossingComponent, CodeGraph, CondensationEdge,
DegreeCentrality, GraphAnalysis, GraphAnalysisLimits, GraphCertainty, GraphComponent,
GraphComponents, GraphEdgeId, GraphEdgeKind, GraphEdgeSelection, GraphNeighbor, GraphNode,
GraphNodeId, GraphNodeLocation, LayoutEdgeMetadata, LayoutNodeMetadata, MisplacementCandidate,
ModuleCohesion,
};
use super::corpus_analysis::{
ANALYSIS_COMPONENT_CORPUS, ANALYSIS_DIVERGENCE_CORPUS, ANALYSIS_PARTITION_CORPUS,
ANALYSIS_TRAVERSAL_CORPUS,
};
use super::corpus_generated::GeneratedFile;
use super::fixture;
use super::render;
const ANALYSIS_LIBRARY: fixture::DeclaredTarget = ("app", CargoTargetKind::Library, "app");
pub fn graph_of(files: &[(&str, &str)]) -> CodeGraph {
let (directory, _, graph) = fixture::project_target(files, ANALYSIS_LIBRARY);
drop(directory);
graph
}
pub fn generated_graph(files: &[GeneratedFile]) -> CodeGraph {
let borrowed: Vec<(&str, &str)> = files
.iter()
.map(|(path, contents)| (*path, contents.as_str()))
.collect();
graph_of(&borrowed)
}
pub fn traversal_graph() -> CodeGraph {
graph_of(ANALYSIS_TRAVERSAL_CORPUS)
}
pub fn component_graph() -> CodeGraph {
graph_of(ANALYSIS_COMPONENT_CORPUS)
}
pub fn partition_graph() -> CodeGraph {
graph_of(ANALYSIS_PARTITION_CORPUS)
}
pub fn divergence_graph() -> CodeGraph {
graph_of(ANALYSIS_DIVERGENCE_CORPUS)
}
pub fn selection_graph() -> CodeGraph {
let (directory, _, graph) = fixture::project_corpus_library();
drop(directory);
graph
}
pub fn generous_limits() -> GraphAnalysisLimits {
GraphAnalysisLimits::new(u32::MAX, u32::MAX, u32::MAX, u64::MAX)
}
pub fn analysis(graph: &CodeGraph, selection: GraphEdgeSelection) -> GraphAnalysis<'_> {
GraphAnalysis::new(graph, selection, generous_limits())
.unwrap_or_else(|error| panic!("the fixture graph should admit an analysis: {error}"))
}
pub fn calls_only() -> GraphEdgeSelection {
GraphEdgeSelection::new(
&[GraphEdgeKind::Call],
&[GraphCertainty::Resolved, GraphCertainty::Possible],
)
}
pub fn definition(graph: &CodeGraph, name: &str) -> GraphNodeId {
sole(
selected_nodes(graph, |node| {
node.name() == name && render::is_definition(node)
}),
name,
)
}
pub fn file_node(graph: &CodeGraph, path: &str) -> GraphNodeId {
sole(
selected_nodes(graph, |node| render::file_path(node) == Some(path)),
path,
)
}
pub fn unit_root(graph: &CodeGraph) -> GraphNodeId {
sole(
selected_nodes(graph, |node| node.location().is_none()),
"the unit root",
)
}
pub fn unit_container(graph: &CodeGraph, name: &str) -> GraphNodeId {
sole(
selected_nodes(graph, |node| {
node.location().is_none() && node.name() == name
}),
name,
)
}
pub fn foreign_node(graph: &CodeGraph) -> GraphNodeId {
let other = selection_graph();
let last = other
.nodes()
.last()
.expect("the mixed-edge corpus states nodes")
.id();
assert!(
last.index() as usize >= graph.nodes().len(),
"the foreign identity must address no node of the graph under test"
);
last
}
fn selected_nodes(graph: &CodeGraph, admits: impl Fn(&GraphNode) -> bool) -> Vec<GraphNodeId> {
graph
.nodes()
.iter()
.filter(|node| admits(node))
.map(|node| node.id())
.collect()
}
fn sole(found: Vec<GraphNodeId>, subject: &str) -> GraphNodeId {
match found.as_slice() {
[only] => *only,
other => panic!("the corpus states {} nodes for {subject}", other.len()),
}
}
pub fn span_file(graph: &CodeGraph, node: GraphNodeId) -> GraphNodeId {
match graph.node(node).and_then(|found| found.location()) {
Some(GraphNodeLocation::Span { file, .. }) => *file,
other => panic!("node {} has no span location: {other:?}", node.index()),
}
}
pub fn name(graph: &CodeGraph, node: GraphNodeId) -> String {
graph
.node(node)
.map(|found| found.name().to_owned())
.unwrap_or_else(|| panic!("node {} is not in this graph", node.index()))
}
pub fn names(graph: &CodeGraph, nodes: &[GraphNodeId]) -> Vec<String> {
nodes.iter().map(|node| name(graph, *node)).collect()
}
pub fn edge_text(graph: &CodeGraph, edge: GraphEdgeId) -> String {
let found = graph
.edge(edge)
.unwrap_or_else(|| panic!("edge {} is not in this graph", edge.index()));
format!(
"{}>{}|{:?}|{:?}",
name(graph, found.source()),
name(graph, found.target()),
found.kind(),
found.certainty(),
)
}
pub fn edge_texts(graph: &CodeGraph, edges: &[GraphEdgeId]) -> Vec<String> {
edges.iter().map(|edge| edge_text(graph, *edge)).collect()
}
pub fn neighbor_texts(graph: &CodeGraph, neighbors: &[GraphNeighbor]) -> Vec<String> {
neighbors
.iter()
.map(|neighbor| format!("{}:{}", name(graph, neighbor.node()), neighbor.distance()))
.collect()
}
pub fn admitted_ids(
graph: &CodeGraph,
kinds: &[GraphEdgeKind],
certainties: &[GraphCertainty],
) -> Vec<GraphEdgeId> {
graph
.edges()
.iter()
.filter(|edge| kinds.contains(&edge.kind()) && certainties.contains(&edge.certainty()))
.map(|edge| edge.id())
.collect()
}
pub fn admitted_between(
graph: &CodeGraph,
selection: GraphEdgeSelection,
pair: (GraphNodeId, GraphNodeId),
) -> Vec<GraphEdgeId> {
let (source, target) = pair;
graph
.edges()
.iter()
.filter(|edge| selection.allows(edge) && edge.source() == source && edge.target() == target)
.map(|edge| edge.id())
.collect()
}
pub fn degree_texts(graph: &CodeGraph, degrees: &[DegreeCentrality]) -> Vec<String> {
degrees
.iter()
.map(|record| {
format!(
"{}|{}|{}",
name(graph, record.node()),
record.incoming(),
record.outgoing()
)
})
.collect()
}
pub fn bits(score: f64) -> String {
format!("{:016x}", score.to_bits())
}
pub fn optional_bits(score: Option<f64>) -> String {
match score {
Some(value) => bits(value),
None => "none".to_owned(),
}
}
pub fn betweenness_texts(graph: &CodeGraph, scores: &[BetweennessCentrality]) -> Vec<String> {
scores
.iter()
.map(|record| {
format!(
"{}|{}|{}",
name(graph, record.node()),
bits(record.raw()),
bits(record.normalized())
)
})
.collect()
}
pub fn cohesion_texts(graph: &CodeGraph, cohesion: &[ModuleCohesion]) -> Vec<String> {
cohesion
.iter()
.map(|record| {
format!(
"{}|{}|{}|{}",
name(graph, record.root()),
record.internal_edges(),
record.boundary_edges(),
optional_bits(record.score())
)
})
.collect()
}
pub fn candidate_texts(graph: &CodeGraph, candidates: &[MisplacementCandidate]) -> Vec<String> {
candidates
.iter()
.map(|record| {
format!(
"{}>{}|{}|{}|{}|{}",
name(graph, record.symbol()),
name(graph, record.candidate_partition()),
name(graph, record.declared_partition()),
record.foreign_edges(),
record.total_outgoing_edges(),
bits(record.affinity())
)
})
.collect()
}
pub fn boundary_texts(
graph: &CodeGraph,
components: &GraphComponents,
crossing: &[BoundaryCrossingComponent],
) -> Vec<String> {
crossing
.iter()
.map(|record| {
let held = components
.component(record.component())
.unwrap_or_else(|| panic!("no component {}", record.component().index()));
format!(
"{}|{}",
sorted_names(graph, held.members()),
sorted_names(graph, record.partitions())
)
})
.collect()
}
pub fn sorted_names(graph: &CodeGraph, nodes: &[GraphNodeId]) -> String {
let mut named = names(graph, nodes);
named.sort();
named.join(",")
}
pub fn layout_node_texts(graph: &CodeGraph, nodes: &[LayoutNodeMetadata]) -> Vec<String> {
nodes
.iter()
.map(|record| {
format!(
"{}|{}|{}|{}|{}|{}|{}",
name(graph, record.node()),
optional_name(graph, record.containment_parent()),
name(graph, record.partition()),
record.incoming_degree(),
record.outgoing_degree(),
bits(record.raw_betweenness()),
bits(record.normalized_betweenness())
)
})
.collect()
}
pub fn layout_edge_texts(graph: &CodeGraph, edges: &[LayoutEdgeMetadata]) -> Vec<String> {
edges
.iter()
.map(|record| {
format!(
"{}>{}|{:?}|{:?}",
name(graph, record.source()),
name(graph, record.target()),
record.kind(),
record.certainty()
)
})
.collect()
}
pub fn optional_name(graph: &CodeGraph, node: Option<GraphNodeId>) -> String {
match node {
Some(found) => name(graph, found),
None => "none".to_owned(),
}
}
pub fn component_texts(graph: &CodeGraph, components: &[GraphComponent]) -> Vec<String> {
components
.iter()
.map(|component| {
format!(
"{}|{}|{}",
component.id().index(),
component.is_cyclic(),
names(graph, component.members()).join(",")
)
})
.collect()
}
pub fn membership_texts(graph: &CodeGraph, components: &GraphComponents) -> Vec<String> {
graph
.nodes()
.iter()
.map(|node| {
let component = components
.component_of(node.id())
.unwrap_or_else(|| panic!("{} belongs to no component", node.name()));
format!("{}|{}", node.name(), component.index())
})
.collect()
}
pub fn condensation_texts(graph: &CodeGraph, edges: &[CondensationEdge]) -> Vec<String> {
edges
.iter()
.map(|edge| {
format!(
"{}>{}|{}",
edge.source().index(),
edge.target().index(),
edge_texts(graph, edge.edges()).join(",")
)
})
.collect()
}
pub fn node_at(graph: &CodeGraph, at: usize) -> GraphNodeId {
graph
.nodes()
.get(at)
.map(|node| node.id())
.unwrap_or_else(|| panic!("this graph holds no node at {at}"))
}
pub fn metric_selections() -> [(&'static str, GraphEdgeSelection); 5] {
[
("empty", GraphEdgeSelection::new(&[], &[])),
("all", GraphEdgeSelection::all()),
("code relations", GraphEdgeSelection::code_relations()),
("calls", calls_only()),
(
"resolved dependencies",
GraphEdgeSelection::new(&[GraphEdgeKind::DependsOn], &[GraphCertainty::Resolved]),
),
]
}