use petgraph::graph::NodeIndex;
use serde::Serialize;
use crate::graph::CodeGraph;
use crate::graph::edge::EdgeKind;
use crate::graph::node::{FileNode, NodeData, SymbolNode};
use crate::graph::scc::{DeployabilityHint, SccAnalysis};
#[derive(Debug, Clone, Serialize)]
pub struct GraphOutput {
pub metadata: GraphMetadata,
pub nodes: Vec<SerializedNode>,
pub edges: Vec<SerializedEdge>,
pub sccs: Vec<SerializedScc>,
pub deployability: DeployabilityStats,
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphMetadata {
pub snapshot_id: u64,
pub node_count: usize,
pub edge_count: usize,
pub scc_count: usize,
pub file_count: usize,
pub symbol_count: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct SerializedNode {
pub id: usize,
pub kind: String,
pub path: Option<String>,
pub language: Option<String>,
pub name: Option<String>,
#[serde(rename = "symbol_kind", skip_serializing_if = "Option::is_none")]
pub symbol_kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SerializedEdge {
pub source: usize,
pub target: usize,
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f32>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SerializedScc {
pub index: usize,
pub nodes: Vec<usize>,
pub is_cyclic: bool,
pub hint: String,
pub size: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct DeployabilityStats {
pub cyclic_clusters: usize,
pub independent_units: usize,
pub self_loops: usize,
pub total_components: usize,
}
impl GraphOutput {
pub fn from_graph(graph: &CodeGraph, scc_analysis: &SccAnalysis, snapshot_id: u64) -> Self {
let metadata = Self::build_metadata(graph, scc_analysis, snapshot_id);
let nodes = Self::serialize_nodes(graph);
let edges = Self::serialize_edges(graph);
let sccs = Self::serialize_sccs(scc_analysis);
let deployability = Self::build_deployability_stats(scc_analysis);
Self {
metadata,
nodes,
edges,
sccs,
deployability,
}
}
fn build_metadata(
graph: &CodeGraph,
scc_analysis: &SccAnalysis,
snapshot_id: u64,
) -> GraphMetadata {
let node_count = graph.graph.node_count();
let edge_count = graph.graph.edge_count();
let scc_count = scc_analysis.components.len();
let mut file_count = 0;
let mut symbol_count = 0;
for node_data in graph.graph.node_weights() {
match node_data {
NodeData::File(_) => file_count += 1,
NodeData::Symbol(_) => symbol_count += 1,
NodeData::External(_) => {}
}
}
GraphMetadata {
snapshot_id,
node_count,
edge_count,
scc_count,
file_count,
symbol_count,
}
}
fn serialize_nodes(graph: &CodeGraph) -> Vec<SerializedNode> {
graph
.graph
.node_indices()
.map(|idx| {
let node_data = &graph.graph[idx];
Self::serialize_node(idx, node_data)
})
.collect()
}
fn serialize_node(idx: NodeIndex, node_data: &NodeData) -> SerializedNode {
match node_data {
NodeData::File(file_node) => Self::serialize_file_node(idx, file_node),
NodeData::Symbol(symbol_node) => Self::serialize_symbol_node(idx, symbol_node),
NodeData::External(external_node) => SerializedNode {
id: idx.index(),
kind: "external".to_string(),
path: Some(external_node.raw_path.clone()),
language: Some(external_node.language.as_ref().to_string()),
name: None,
symbol_kind: None,
visibility: None,
},
}
}
fn serialize_file_node(idx: NodeIndex, file_node: &FileNode) -> SerializedNode {
SerializedNode {
id: idx.index(),
kind: "file".to_string(),
path: Some(file_node.path.to_string_lossy().to_string()),
language: Some(file_node.language.as_ref().to_string()),
name: None,
symbol_kind: None,
visibility: None,
}
}
fn serialize_symbol_node(idx: NodeIndex, symbol_node: &SymbolNode) -> SerializedNode {
let visibility = symbol_node.visibility.map(|v| format!("{:?}", v));
SerializedNode {
id: idx.index(),
kind: "symbol".to_string(),
path: None,
language: None,
name: Some(symbol_node.name.clone()),
symbol_kind: Some(format!("{:?}", symbol_node.kind)),
visibility,
}
}
fn serialize_edges(graph: &CodeGraph) -> Vec<SerializedEdge> {
graph
.graph
.edge_indices()
.filter_map(|edge_idx| {
let (source, target) = graph.graph.edge_endpoints(edge_idx)?;
let edge_data = graph.graph.edge_weight(edge_idx)?;
let confidence = if edge_data.confidence < 1.0 {
Some(edge_data.confidence)
} else {
None
};
let kind_str = match edge_data.kind {
EdgeKind::Ownership => "ownership",
EdgeKind::Import => "import",
EdgeKind::Reference => "reference",
};
Some(SerializedEdge {
source: source.index(),
target: target.index(),
kind: kind_str.to_string(),
confidence,
})
})
.collect()
}
fn serialize_sccs(scc_analysis: &SccAnalysis) -> Vec<SerializedScc> {
scc_analysis
.components
.iter()
.map(|scc| {
let nodes: Vec<usize> = scc.nodes.iter().map(|n| n.index()).collect();
let hint_str = scc.hint.to_string();
SerializedScc {
index: scc.index,
nodes,
is_cyclic: scc.is_cyclic,
hint: hint_str.to_string(),
size: scc.nodes.len(),
}
})
.collect()
}
fn build_deployability_stats(scc_analysis: &SccAnalysis) -> DeployabilityStats {
let mut cyclic_clusters = 0;
let mut independent_units = 0;
let mut self_loops = 0;
for scc in &scc_analysis.components {
match scc.hint {
DeployabilityHint::Independent | DeployabilityHint::AcyclicDependency => {
independent_units += 1;
}
DeployabilityHint::CyclicCluster => {
cyclic_clusters += 1;
}
DeployabilityHint::SelfLoop => {
self_loops += 1;
}
}
}
DeployabilityStats {
cyclic_clusters,
independent_units,
self_loops,
total_components: scc_analysis.components.len(),
}
}
}
pub fn serialize_graph(
graph: &CodeGraph,
scc_analysis: &SccAnalysis,
snapshot_id: u64,
format: &crate::output::OutputFormat,
) -> anyhow::Result<String> {
let output = GraphOutput::from_graph(graph, scc_analysis, snapshot_id);
format.serialize(&output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::builder::GraphBuilder;
use crate::language::LangId;
use crate::model::{
LineColumn, SourceRange, Symbol, SymbolId, SymbolKind, Visibility, ids::SnapshotId,
};
use crate::output::OutputFormat;
use std::path::PathBuf;
fn sample_symbol(id: u32, name: &str, kind: SymbolKind, path: &str) -> Symbol {
Symbol {
id: SymbolId(id),
name: name.to_string(),
kind,
language: LangId::Rust,
file_path: PathBuf::from(path),
source_range: SourceRange {
byte_start: 0,
byte_end: 10,
start: LineColumn { line: 1, column: 0 },
end: LineColumn {
line: 1,
column: 10,
},
},
visibility: Some(Visibility::Public),
signature: None,
docstring: None,
is_async: false,
}
}
#[test]
fn graph_output_has_required_keys() {
let mut builder = GraphBuilder::new(SnapshotId(1));
let _file_id = builder.add_file(PathBuf::from("src/main.rs"), LangId::Rust);
let symbol = sample_symbol(1, "main", SymbolKind::Function, "src/main.rs");
let _sym_idx = builder.add_symbol(&symbol).unwrap();
let graph = builder.build();
let scc_result = SccAnalysis::analyze(&graph.graph);
let output = GraphOutput::from_graph(&graph, &scc_result, 1);
assert_eq!(output.metadata.file_count, 1);
assert_eq!(output.metadata.symbol_count, 1);
assert!(!output.nodes.is_empty());
}
#[test]
fn serialized_node_kinds() {
let mut builder = GraphBuilder::new(SnapshotId(1));
let _file_id = builder.add_file(PathBuf::from("src/lib.rs"), LangId::Rust);
let symbol = sample_symbol(1, "lib_fn", SymbolKind::Function, "src/lib.rs");
let _sym_idx = builder.add_symbol(&symbol).unwrap();
let graph = builder.build();
let scc_result = SccAnalysis::analyze(&graph.graph);
let json = serialize_graph(&graph, &scc_result, 1, &OutputFormat::Json).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("metadata").is_some());
assert!(parsed.get("nodes").is_some());
assert!(parsed.get("edges").is_some());
assert!(parsed.get("sccs").is_some());
assert!(parsed.get("deployability").is_some());
let nodes = parsed["nodes"].as_array().unwrap();
let file_nodes: Vec<_> = nodes.iter().filter(|n| n["kind"] == "file").collect();
let symbol_nodes: Vec<_> = nodes.iter().filter(|n| n["kind"] == "symbol").collect();
assert_eq!(file_nodes.len(), 1);
assert_eq!(symbol_nodes.len(), 1);
assert!(file_nodes[0]["language"].is_string());
assert!(symbol_nodes[0]["name"].is_string());
}
#[test]
fn empty_graph_produces_empty_output() {
let builder = GraphBuilder::new(SnapshotId(1));
let graph = builder.build();
let scc_result = SccAnalysis::analyze(&graph.graph);
let output = GraphOutput::from_graph(&graph, &scc_result, 1);
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"node_count\":0"));
assert!(json.contains("\"sccs\":[]"));
}
#[test]
fn scc_serialization_contains_hint() {
let mut builder = GraphBuilder::new(SnapshotId(1));
let _file_id = builder.add_file(PathBuf::from("src/a.rs"), LangId::Rust);
let sym1 = sample_symbol(1, "func_a", SymbolKind::Function, "src/a.rs");
let sym2 = sample_symbol(2, "func_b", SymbolKind::Function, "src/a.rs");
builder.add_symbol(&sym1).unwrap();
builder.add_symbol(&sym2).unwrap();
builder.add_reference(sym1.id, sym2.id, 1.0);
builder.add_reference(sym2.id, sym1.id, 1.0);
let graph = builder.build();
let scc_result = SccAnalysis::analyze(&graph.graph);
let json = serialize_graph(&graph, &scc_result, 1, &OutputFormat::Json).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let sccs = parsed["sccs"].as_array().unwrap();
let cyclic_scc = sccs.iter().find(|s| s["is_cyclic"].as_bool().unwrap());
assert!(
cyclic_scc.is_some(),
"Expected to find a cyclic SCC in the output"
);
assert_eq!(
cyclic_scc.unwrap()["hint"],
"cyclic_cluster",
"Cyclic SCC should have cyclic_cluster hint"
);
}
}