1pub mod builder;
34pub mod edge;
35pub mod node;
36pub mod resolver;
37pub mod scc;
38
39use std::collections::HashMap;
40
41pub use builder::GraphBuilder;
42pub use edge::{EdgeData, EdgeKind};
43pub use node::{
44 DataGraphNode, ExternalClassification, ExternalNode, FileNode, NodeData, SymbolNode,
45};
46pub use scc::{DeployabilityHint, Scc, SccAnalysis};
47
48use crate::language::LangId;
49use crate::model::{FileId, SnapshotId, SymbolId};
50use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex};
51
52#[derive(Debug, Clone)]
54pub struct CodeGraph {
55 graph: DiGraph<NodeData, EdgeData>,
57
58 edge_index: HashMap<(NodeIndex, NodeIndex, EdgeKind), EdgeIndex>,
62
63 pub(crate) file_to_index: HashMap<FileId, NodeIndex>,
65
66 pub(crate) symbol_to_index: HashMap<SymbolId, NodeIndex>,
68
69 pub(crate) external_index: HashMap<String, NodeIndex>,
71
72 pub snapshot_id: SnapshotId,
74}
75
76impl CodeGraph {
77 pub fn new(snapshot_id: SnapshotId) -> Self {
79 Self {
80 graph: DiGraph::new(),
81 edge_index: HashMap::new(),
82 file_to_index: HashMap::new(),
83 symbol_to_index: HashMap::new(),
84 external_index: HashMap::new(),
85 snapshot_id,
86 }
87 }
88
89 pub(crate) fn from_parts(
94 graph: DiGraph<NodeData, EdgeData>,
95 edge_index: HashMap<(NodeIndex, NodeIndex, EdgeKind), EdgeIndex>,
96 file_to_index: HashMap<FileId, NodeIndex>,
97 symbol_to_index: HashMap<SymbolId, NodeIndex>,
98 external_index: HashMap<String, NodeIndex>,
99 snapshot_id: SnapshotId,
100 ) -> Self {
101 Self {
102 graph,
103 edge_index,
104 file_to_index,
105 symbol_to_index,
106 external_index,
107 snapshot_id,
108 }
109 }
110 pub fn graph(&self) -> &DiGraph<NodeData, EdgeData> {
113 &self.graph
114 }
115
116 pub fn add_node(&mut self, node: NodeData) -> NodeIndex {
125 self.graph.add_node(node)
126 }
127 pub fn file_node_index(&self, file_id: FileId) -> Option<NodeIndex> {
128 self.file_to_index.get(&file_id).copied()
129 }
130 pub fn symbol_node_index(&self, symbol_id: SymbolId) -> Option<NodeIndex> {
131 self.symbol_to_index.get(&symbol_id).copied()
132 }
133 pub fn file_node(&self, file_id: FileId) -> Option<&FileNode> {
134 let idx = self.file_node_index(file_id)?;
135 self.graph.node_weight(idx).and_then(|data| data.as_file())
136 }
137 pub fn symbol_node(&self, symbol_id: SymbolId) -> Option<&SymbolNode> {
138 let idx = self.symbol_node_index(symbol_id)?;
139 self.graph
140 .node_weight(idx)
141 .and_then(|data| data.as_symbol())
142 }
143 pub fn file_count(&self) -> usize {
144 self.file_to_index.len()
145 }
146 pub fn symbol_count(&self) -> usize {
147 self.symbol_to_index.len()
148 }
149 pub fn external_count(&self) -> usize {
150 self.external_index.len()
151 }
152 pub fn node_count(&self) -> usize {
153 self.graph.node_count()
154 }
155 pub fn edge_count(&self) -> usize {
156 self.graph.edge_count()
157 }
158 pub fn get_or_create_external_node(&mut self, raw_path: String, language: LangId) -> NodeIndex {
161 if let Some(&idx) = self.external_index.get(&raw_path) {
162 return idx;
163 }
164 let node = NodeData::External(ExternalNode {
165 raw_path: raw_path.clone(),
166 language,
167 classification: None,
168 });
169 let idx = self.graph.add_node(node);
170 self.external_index.insert(raw_path, idx);
171 idx
172 }
173 pub fn add_edge_normalized(
177 &mut self,
178 source: NodeIndex,
179 target: NodeIndex,
180 kind: EdgeKind,
181 confidence: f32,
182 ) {
183 let confidence = confidence.clamp(0.0, 1.0);
184 let key = (source, target, kind);
185 if let Some(&edge_idx) = self.edge_index.get(&key) {
186 let edge = &mut self.graph[edge_idx];
187 edge.confidence = edge.confidence.max(confidence);
188 return;
189 }
190 let edge_idx = self.graph.add_edge(
191 source,
192 target,
193 EdgeData {
194 kind,
195 confidence,
196 flow_kind: None,
197 },
198 );
199 self.edge_index.insert(key, edge_idx);
200 }
201
202 pub fn add_edge_normalized_with_flow(
207 &mut self,
208 source: NodeIndex,
209 target: NodeIndex,
210 kind: EdgeKind,
211 confidence: f32,
212 flow_kind: Option<crate::model::FlowKind>,
213 ) {
214 let confidence = confidence.clamp(0.0, 1.0);
215 let key = (source, target, kind);
216 if let Some(&edge_idx) = self.edge_index.get(&key) {
217 let edge = &mut self.graph[edge_idx];
218 edge.confidence = edge.confidence.max(confidence);
219 if edge.flow_kind.is_none() {
220 edge.flow_kind = flow_kind;
221 }
222 return;
223 }
224 let edge_idx = self.graph.add_edge(
225 source,
226 target,
227 EdgeData {
228 kind,
229 confidence,
230 flow_kind,
231 },
232 );
233 self.edge_index.insert(key, edge_idx);
234 }
235
236 pub fn files(&self) -> impl Iterator<Item = (FileId, &FileNode)> + '_ {
237 self.file_to_index.iter().filter_map(|(file_id, &idx)| {
238 self.graph
239 .node_weight(idx)
240 .and_then(|data| data.as_file().map(|f| (*file_id, f)))
241 })
242 }
243 pub fn symbols(&self) -> impl Iterator<Item = (SymbolId, &SymbolNode)> + '_ {
244 self.symbol_to_index.iter().filter_map(|(symbol_id, &idx)| {
245 self.graph
246 .node_weight(idx)
247 .and_then(|data| data.as_symbol().map(|s| (*symbol_id, s)))
248 })
249 }
250 pub fn edges_of_kind(
251 &self,
252 kind: EdgeKind,
253 ) -> impl Iterator<Item = (NodeIndex, NodeIndex)> + '_ {
254 self.graph.edge_indices().filter_map(move |edge_idx| {
255 let (source, target) = self.graph.edge_endpoints(edge_idx)?;
256 let weight = self.graph.edge_weight(edge_idx)?;
257 if weight.kind == kind {
258 Some((source, target))
259 } else {
260 None
261 }
262 })
263 }
264
265 pub fn reference_edges(&self) -> impl Iterator<Item = (SymbolId, SymbolId, f32)> + '_ {
270 self.graph.edge_indices().filter_map(move |edge_idx| {
271 let weight = self.graph.edge_weight(edge_idx)?;
272 if weight.kind != EdgeKind::Reference {
273 return None;
274 }
275 let (source, target) = self.graph.edge_endpoints(edge_idx)?;
276 let source_id = self.graph.node_weight(source)?.as_symbol()?.id;
277 let target_id = self.graph.node_weight(target)?.as_symbol()?.id;
278 Some((source_id, target_id, weight.confidence))
279 })
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::language::LangId;
287 use crate::model::{LineColumn, SourceRange, Symbol, SymbolKind, Visibility, ids::SnapshotId};
288 use std::path::PathBuf;
289
290 fn test_range() -> SourceRange {
291 SourceRange {
292 byte_start: 0,
293 byte_end: 10,
294 start: LineColumn { line: 1, column: 0 },
295 end: LineColumn {
296 line: 1,
297 column: 10,
298 },
299 }
300 }
301
302 fn test_file_node(id: u32, path: &str) -> NodeData {
303 NodeData::File(FileNode {
304 id: FileId::new(id).unwrap(),
305 path: PathBuf::from(path),
306 language: LangId::Rust,
307 snapshot_id: SnapshotId::new(1).unwrap(),
308 })
309 }
310
311 fn test_symbol(id: u32, name: &str) -> Symbol {
312 Symbol {
313 id: SymbolId::new(id).unwrap(),
314 name: name.to_string(),
315 kind: SymbolKind::Function,
316 language: LangId::Rust,
317 file_path: PathBuf::from("test.rs"),
318 source_range: test_range(),
319 visibility: Some(Visibility::Public),
320 signature: None,
321 docstring: None,
322 is_async: false,
323 }
324 }
325
326 #[test]
327 fn code_graph_new_empty() {
328 let graph = CodeGraph::new(SnapshotId::new(1).unwrap());
329 assert_eq!(graph.node_count(), 0);
330 assert_eq!(graph.edge_count(), 0);
331 assert_eq!(graph.snapshot_id.to_raw(), 1);
332 }
333
334 #[test]
335 fn code_graph_file_lookup_returns_none_for_missing() {
336 let graph = CodeGraph::new(SnapshotId::new(1).unwrap());
337 assert!(graph.file_node(FileId::new(1).unwrap()).is_none());
338 assert!(graph.file_node_index(FileId::new(1).unwrap()).is_none());
339 }
340
341 #[test]
342 fn code_graph_symbol_lookup_returns_none_for_missing() {
343 let graph = CodeGraph::new(SnapshotId::new(1).unwrap());
344 assert!(graph.symbol_node(SymbolId::new(1).unwrap()).is_none());
345 assert!(graph.symbol_node_index(SymbolId::new(1).unwrap()).is_none());
346 }
347
348 #[test]
349 fn builder_produces_valid_code_graph() {
350 let mut builder = GraphBuilder::new(SnapshotId::new(1).unwrap());
351 let file_id = builder.add_file(PathBuf::from("test.rs"), LangId::Rust);
352 let symbol = test_symbol(1, "main");
353 let _sym_idx = builder.add_symbol(&symbol).unwrap();
354
355 let graph = builder.build();
356
357 assert_eq!(graph.file_count(), 1);
358 assert_eq!(graph.symbol_count(), 1);
359 assert_eq!(graph.node_count(), 2);
360 assert_eq!(graph.edge_count(), 1); let file_lookup = graph.file_node(file_id);
364 assert!(file_lookup.is_some());
365 assert_eq!(file_lookup.unwrap().language, LangId::Rust);
366
367 let sym_lookup = graph.symbol_node(SymbolId::new(1).unwrap());
368 assert!(sym_lookup.is_some());
369 assert_eq!(sym_lookup.unwrap().name, "main");
370 }
371
372 #[test]
373 fn code_graph_iteration_over_files() {
374 let mut builder = GraphBuilder::new(SnapshotId::new(1).unwrap());
375 builder.add_file(PathBuf::from("a.rs"), LangId::Rust);
376 builder.add_file(PathBuf::from("b.py"), LangId::Python);
377
378 let graph = builder.build();
379 let files: Vec<_> = graph.files().collect();
380
381 assert_eq!(files.len(), 2);
382 }
383
384 #[test]
385 fn code_graph_iteration_over_symbols() {
386 let mut builder = GraphBuilder::new(SnapshotId::new(1).unwrap());
387 let _file_id = builder.add_file(PathBuf::from("test.rs"), LangId::Rust);
388 let sym1 = test_symbol(1, "func_a");
389 let sym2 = test_symbol(2, "func_b");
390 builder.add_symbol(&sym1).unwrap();
391 builder.add_symbol(&sym2).unwrap();
392
393 let graph = builder.build();
394 let symbols: Vec<_> = graph.symbols().collect();
395
396 assert_eq!(symbols.len(), 2);
397 }
398
399 #[test]
400 fn code_graph_edges_of_kind_filtering() {
401 let mut builder = GraphBuilder::new(SnapshotId::new(1).unwrap());
402 let file1 = builder.add_file(PathBuf::from("a.rs"), LangId::Rust);
403 let _file2 = builder.add_file(PathBuf::from("b.rs"), LangId::Rust);
404
405 builder.add_import(file1, PathBuf::from("b.rs"));
407
408 let graph = builder.build();
409
410 let ownership_edges: Vec<_> = graph.edges_of_kind(EdgeKind::Ownership).collect();
411 let import_edges: Vec<_> = graph.edges_of_kind(EdgeKind::Import).collect();
412
413 assert_eq!(ownership_edges.len(), 0); assert_eq!(import_edges.len(), 1);
415 }
416
417 #[test]
418 fn build_populated_edge_index_normalizes_post_build_edges() {
419 let mut builder = GraphBuilder::new(SnapshotId::new(1).unwrap());
420 let file_a = builder.add_file(PathBuf::from("a.rs"), LangId::Rust);
421 let file_b = builder.add_file(PathBuf::from("b.rs"), LangId::Rust);
422 builder.add_import(file_a, PathBuf::from("b.rs"));
423 let mut graph = builder.build();
424 assert_eq!(graph.edge_count(), 1);
425
426 let a_idx = graph.file_node_index(file_a).unwrap();
427 let b_idx = graph.file_node_index(file_b).unwrap();
428
429 graph.add_edge_normalized(a_idx, b_idx, EdgeKind::Import, 0.5);
431 graph.add_edge_normalized(a_idx, b_idx, EdgeKind::Import, 0.8);
432 assert_eq!(graph.edge_count(), 1);
433
434 graph.add_edge_normalized(a_idx, b_idx, EdgeKind::Reference, 0.9);
436 assert_eq!(graph.edge_count(), 2);
437 }
438
439 #[test]
440 fn add_edge_normalized_handles_multiple_edge_kinds_between_same_nodes() {
441 let mut graph = CodeGraph::new(SnapshotId::new(1).unwrap());
442 let n1 = graph.add_node(test_file_node(1, "a.rs"));
443 let n2 = graph.add_node(test_file_node(2, "b.rs"));
444
445 graph.add_edge_normalized(n1, n2, EdgeKind::Reference, 0.7);
447 graph.add_edge_normalized(n1, n2, EdgeKind::Import, 0.5);
449 graph.add_edge_normalized(n1, n2, EdgeKind::Reference, 0.9);
451
452 let ref_count = graph
453 .graph()
454 .edges_connecting(n1, n2)
455 .filter(|e| e.weight().kind == EdgeKind::Reference)
456 .count();
457 assert_eq!(
458 ref_count, 1,
459 "Expected 1 Reference edge, but found {ref_count}"
460 );
461 assert_eq!(graph.edge_count(), 2);
462 }
463
464 #[test]
465 fn add_edge_normalized_with_flow_preserves_first_flow_kind() {
466 use crate::model::{DataNodeId, DataScope, FlowKind};
467 let mut graph = CodeGraph::new(SnapshotId::new(1).unwrap());
468 let n1 = graph.add_node(NodeData::Data(DataGraphNode {
469 id: DataNodeId::new(1).unwrap(),
470 symbol_id: None,
471 name: Some("x".into()),
472 scope: DataScope::Local,
473 type_hint: None,
474 source_range: test_range(),
475 }));
476 let n2 = graph.add_node(NodeData::Data(DataGraphNode {
477 id: DataNodeId::new(2).unwrap(),
478 symbol_id: None,
479 name: Some("y".into()),
480 scope: DataScope::Local,
481 type_hint: None,
482 source_range: test_range(),
483 }));
484
485 graph.add_edge_normalized_with_flow(n1, n2, EdgeKind::Flow, 0.9, Some(FlowKind::DefUse));
486 graph.add_edge_normalized_with_flow(n1, n2, EdgeKind::Flow, 0.8, Some(FlowKind::Argument));
487
488 assert_eq!(graph.edge_count(), 1);
489 let edge = graph.graph().edges_connecting(n1, n2).next().unwrap();
490 assert_eq!(edge.weight().flow_kind, Some(FlowKind::DefUse));
491 assert_eq!(edge.weight().confidence, 0.9);
492 }
493
494 #[test]
495 fn add_edge_normalized_duplicate_never_grows_edge_count() {
496 let mut graph = CodeGraph::new(SnapshotId::new(1).unwrap());
497 let n1 = graph.add_node(test_file_node(1, "a.rs"));
498 let n2 = graph.add_node(test_file_node(2, "b.rs"));
499
500 graph.add_edge_normalized(n1, n2, EdgeKind::Import, 0.5);
501 for confidence in [0.6, 0.3, 0.9, 0.4, 0.7] {
502 graph.add_edge_normalized(n1, n2, EdgeKind::Import, confidence);
503 }
504
505 assert_eq!(graph.edge_count(), 1);
506 let edge = graph.graph().edges_connecting(n1, n2).next().unwrap();
507 assert_eq!(edge.weight().confidence, 0.9);
508 }
509
510 #[test]
511 fn add_edge_normalized_counts_only_distinct_triples() {
512 let mut graph = CodeGraph::new(SnapshotId::new(1).unwrap());
513 let n1 = graph.add_node(test_file_node(1, "a.rs"));
514 let n2 = graph.add_node(test_file_node(2, "b.rs"));
515 let n3 = graph.add_node(test_file_node(3, "c.rs"));
516
517 graph.add_edge_normalized(n1, n2, EdgeKind::Import, 0.5);
518 graph.add_edge_normalized(n1, n2, EdgeKind::Reference, 0.6);
519 graph.add_edge_normalized(n1, n3, EdgeKind::Reference, 0.7);
520 graph.add_edge_normalized(n1, n2, EdgeKind::Import, 0.9);
521
522 assert_eq!(graph.edge_count(), 3);
523 }
524}