1use petgraph::graph::NodeIndex;
7use serde::Serialize;
8
9use crate::graph::CodeGraph;
10use crate::graph::edge::EdgeKind;
11use crate::graph::node::{FileNode, NodeData, SymbolNode};
12use crate::graph::scc::{DeployabilityHint, SccAnalysis};
13
14#[derive(Debug, Clone, Serialize)]
16pub struct GraphOutput {
17 pub metadata: GraphMetadata,
19 pub nodes: Vec<SerializedNode>,
21 pub edges: Vec<SerializedEdge>,
23 pub sccs: Vec<SerializedScc>,
25 pub deployability: DeployabilityStats,
27}
28
29#[derive(Debug, Clone, Serialize)]
31pub struct GraphMetadata {
32 pub snapshot_id: u64,
34 pub node_count: usize,
36 pub edge_count: usize,
38 pub scc_count: usize,
40 pub file_count: usize,
42 pub symbol_count: usize,
44}
45
46#[derive(Debug, Clone, Serialize)]
48pub struct SerializedNode {
49 pub id: usize,
51 pub kind: String,
53 pub path: Option<String>,
55 pub language: Option<String>,
57 pub name: Option<String>,
59 #[serde(rename = "symbol_kind", skip_serializing_if = "Option::is_none")]
61 pub symbol_kind: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub visibility: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize)]
69pub struct SerializedEdge {
70 pub source: usize,
72 pub target: usize,
74 pub kind: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub confidence: Option<f32>,
79}
80
81#[derive(Debug, Clone, Serialize)]
83pub struct SerializedScc {
84 pub index: usize,
86 pub nodes: Vec<usize>,
88 pub is_cyclic: bool,
90 pub hint: String,
92 pub size: usize,
94}
95
96#[derive(Debug, Clone, Serialize)]
98pub struct DeployabilityStats {
99 pub cyclic_clusters: usize,
101 pub independent_units: usize,
103 pub self_loops: usize,
105 pub total_components: usize,
107}
108
109impl GraphOutput {
110 pub fn from_graph(graph: &CodeGraph, scc_analysis: &SccAnalysis, snapshot_id: u64) -> Self {
112 let metadata = Self::build_metadata(graph, scc_analysis, snapshot_id);
113 let nodes = Self::serialize_nodes(graph);
114 let edges = Self::serialize_edges(graph);
115 let sccs = Self::serialize_sccs(scc_analysis);
116 let deployability = Self::build_deployability_stats(scc_analysis);
117
118 Self {
119 metadata,
120 nodes,
121 edges,
122 sccs,
123 deployability,
124 }
125 }
126
127 fn build_metadata(
128 graph: &CodeGraph,
129 scc_analysis: &SccAnalysis,
130 snapshot_id: u64,
131 ) -> GraphMetadata {
132 let node_count = graph.graph.node_count();
133 let edge_count = graph.graph.edge_count();
134 let scc_count = scc_analysis.components.len();
135
136 let mut file_count = 0;
137 let mut symbol_count = 0;
138
139 for node_data in graph.graph.node_weights() {
140 match node_data {
141 NodeData::File(_) => file_count += 1,
142 NodeData::Symbol(_) => symbol_count += 1,
143 NodeData::External(_) => {}
144 }
145 }
146
147 GraphMetadata {
148 snapshot_id,
149 node_count,
150 edge_count,
151 scc_count,
152 file_count,
153 symbol_count,
154 }
155 }
156
157 fn serialize_nodes(graph: &CodeGraph) -> Vec<SerializedNode> {
158 graph
159 .graph
160 .node_indices()
161 .map(|idx| {
162 let node_data = &graph.graph[idx];
163 Self::serialize_node(idx, node_data)
164 })
165 .collect()
166 }
167
168 fn serialize_node(idx: NodeIndex, node_data: &NodeData) -> SerializedNode {
169 match node_data {
170 NodeData::File(file_node) => Self::serialize_file_node(idx, file_node),
171 NodeData::Symbol(symbol_node) => Self::serialize_symbol_node(idx, symbol_node),
172 NodeData::External(external_node) => SerializedNode {
173 id: idx.index(),
174 kind: "external".to_string(),
175 path: Some(external_node.raw_path.clone()),
176 language: Some(external_node.language.as_ref().to_string()),
177 name: None,
178 symbol_kind: None,
179 visibility: None,
180 },
181 }
182 }
183
184 fn serialize_file_node(idx: NodeIndex, file_node: &FileNode) -> SerializedNode {
185 SerializedNode {
186 id: idx.index(),
187 kind: "file".to_string(),
188 path: Some(file_node.path.to_string_lossy().to_string()),
189 language: Some(file_node.language.as_ref().to_string()),
190 name: None,
191 symbol_kind: None,
192 visibility: None,
193 }
194 }
195
196 fn serialize_symbol_node(idx: NodeIndex, symbol_node: &SymbolNode) -> SerializedNode {
197 let visibility = symbol_node.visibility.map(|v| format!("{:?}", v));
198
199 SerializedNode {
200 id: idx.index(),
201 kind: "symbol".to_string(),
202 path: None,
203 language: None,
204 name: Some(symbol_node.name.clone()),
205 symbol_kind: Some(format!("{:?}", symbol_node.kind)),
206 visibility,
207 }
208 }
209
210 fn serialize_edges(graph: &CodeGraph) -> Vec<SerializedEdge> {
211 graph
212 .graph
213 .edge_indices()
214 .filter_map(|edge_idx| {
215 let (source, target) = graph.graph.edge_endpoints(edge_idx)?;
216 let edge_data = graph.graph.edge_weight(edge_idx)?;
217 let confidence = if edge_data.confidence < 1.0 {
218 Some(edge_data.confidence)
219 } else {
220 None
221 };
222
223 let kind_str = match edge_data.kind {
224 EdgeKind::Ownership => "ownership",
225 EdgeKind::Import => "import",
226 EdgeKind::Reference => "reference",
227 };
228
229 Some(SerializedEdge {
230 source: source.index(),
231 target: target.index(),
232 kind: kind_str.to_string(),
233 confidence,
234 })
235 })
236 .collect()
237 }
238
239 fn serialize_sccs(scc_analysis: &SccAnalysis) -> Vec<SerializedScc> {
240 scc_analysis
241 .components
242 .iter()
243 .map(|scc| {
244 let nodes: Vec<usize> = scc.nodes.iter().map(|n| n.index()).collect();
245 let hint_str = scc.hint.to_string();
246
247 SerializedScc {
248 index: scc.index,
249 nodes,
250 is_cyclic: scc.is_cyclic,
251 hint: hint_str.to_string(),
252 size: scc.nodes.len(),
253 }
254 })
255 .collect()
256 }
257
258 fn build_deployability_stats(scc_analysis: &SccAnalysis) -> DeployabilityStats {
259 let mut cyclic_clusters = 0;
260 let mut independent_units = 0;
261 let mut self_loops = 0;
262
263 for scc in &scc_analysis.components {
264 match scc.hint {
265 DeployabilityHint::Independent | DeployabilityHint::AcyclicDependency => {
266 independent_units += 1;
267 }
268 DeployabilityHint::CyclicCluster => {
269 cyclic_clusters += 1;
270 }
271 DeployabilityHint::SelfLoop => {
272 self_loops += 1;
273 }
274 }
275 }
276
277 DeployabilityStats {
278 cyclic_clusters,
279 independent_units,
280 self_loops,
281 total_components: scc_analysis.components.len(),
282 }
283 }
284}
285
286pub fn serialize_graph(
288 graph: &CodeGraph,
289 scc_analysis: &SccAnalysis,
290 snapshot_id: u64,
291 format: &crate::output::OutputFormat,
292) -> anyhow::Result<String> {
293 let output = GraphOutput::from_graph(graph, scc_analysis, snapshot_id);
294 format.serialize(&output)
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::graph::builder::GraphBuilder;
301 use crate::language::LangId;
302 use crate::model::{
303 LineColumn, SourceRange, Symbol, SymbolId, SymbolKind, Visibility, ids::SnapshotId,
304 };
305 use crate::output::OutputFormat;
306 use std::path::PathBuf;
307
308 fn sample_symbol(id: u32, name: &str, kind: SymbolKind, path: &str) -> Symbol {
309 Symbol {
310 id: SymbolId(id),
311 name: name.to_string(),
312 kind,
313 language: LangId::Rust,
314 file_path: PathBuf::from(path),
315 source_range: SourceRange {
316 byte_start: 0,
317 byte_end: 10,
318 start: LineColumn { line: 1, column: 0 },
319 end: LineColumn {
320 line: 1,
321 column: 10,
322 },
323 },
324 visibility: Some(Visibility::Public),
325 signature: None,
326 docstring: None,
327 is_async: false,
328 }
329 }
330
331 #[test]
332 fn graph_output_has_required_keys() {
333 let mut builder = GraphBuilder::new(SnapshotId(1));
334 let _file_id = builder.add_file(PathBuf::from("src/main.rs"), LangId::Rust);
335 let symbol = sample_symbol(1, "main", SymbolKind::Function, "src/main.rs");
336 let _sym_idx = builder.add_symbol(&symbol).unwrap();
337
338 let graph = builder.build();
339 let scc_result = SccAnalysis::analyze(&graph.graph);
340
341 let output = GraphOutput::from_graph(&graph, &scc_result, 1);
342
343 assert_eq!(output.metadata.file_count, 1);
344 assert_eq!(output.metadata.symbol_count, 1);
345 assert!(!output.nodes.is_empty());
346 }
347
348 #[test]
349 fn serialized_node_kinds() {
350 let mut builder = GraphBuilder::new(SnapshotId(1));
351 let _file_id = builder.add_file(PathBuf::from("src/lib.rs"), LangId::Rust);
352 let symbol = sample_symbol(1, "lib_fn", SymbolKind::Function, "src/lib.rs");
353 let _sym_idx = builder.add_symbol(&symbol).unwrap();
354
355 let graph = builder.build();
356 let scc_result = SccAnalysis::analyze(&graph.graph);
357
358 let json = serialize_graph(&graph, &scc_result, 1, &OutputFormat::Json).unwrap();
359 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
360
361 assert!(parsed.get("metadata").is_some());
363 assert!(parsed.get("nodes").is_some());
364 assert!(parsed.get("edges").is_some());
365 assert!(parsed.get("sccs").is_some());
366 assert!(parsed.get("deployability").is_some());
367
368 let nodes = parsed["nodes"].as_array().unwrap();
370 let file_nodes: Vec<_> = nodes.iter().filter(|n| n["kind"] == "file").collect();
371 let symbol_nodes: Vec<_> = nodes.iter().filter(|n| n["kind"] == "symbol").collect();
372
373 assert_eq!(file_nodes.len(), 1);
374 assert_eq!(symbol_nodes.len(), 1);
375 assert!(file_nodes[0]["language"].is_string());
376 assert!(symbol_nodes[0]["name"].is_string());
377 }
378
379 #[test]
380 fn empty_graph_produces_empty_output() {
381 let builder = GraphBuilder::new(SnapshotId(1));
382 let graph = builder.build();
383 let scc_result = SccAnalysis::analyze(&graph.graph);
384
385 let output = GraphOutput::from_graph(&graph, &scc_result, 1);
386 let json = serde_json::to_string(&output).unwrap();
387
388 assert!(json.contains("\"node_count\":0"));
389 assert!(json.contains("\"sccs\":[]"));
390 }
391
392 #[test]
393 fn scc_serialization_contains_hint() {
394 let mut builder = GraphBuilder::new(SnapshotId(1));
397 let _file_id = builder.add_file(PathBuf::from("src/a.rs"), LangId::Rust);
398 let sym1 = sample_symbol(1, "func_a", SymbolKind::Function, "src/a.rs");
399 let sym2 = sample_symbol(2, "func_b", SymbolKind::Function, "src/a.rs");
400 builder.add_symbol(&sym1).unwrap();
401 builder.add_symbol(&sym2).unwrap();
402
403 builder.add_reference(sym1.id, sym2.id, 1.0);
405 builder.add_reference(sym2.id, sym1.id, 1.0);
406
407 let graph = builder.build();
408 let scc_result = SccAnalysis::analyze(&graph.graph);
409
410 let json = serialize_graph(&graph, &scc_result, 1, &OutputFormat::Json).unwrap();
411 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
412
413 let sccs = parsed["sccs"].as_array().unwrap();
415 let cyclic_scc = sccs.iter().find(|s| s["is_cyclic"].as_bool().unwrap());
416
417 assert!(
418 cyclic_scc.is_some(),
419 "Expected to find a cyclic SCC in the output"
420 );
421 assert_eq!(
422 cyclic_scc.unwrap()["hint"],
423 "cyclic_cluster",
424 "Cyclic SCC should have cyclic_cluster hint"
425 );
426 }
427}