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::{ExternalClassification, ExternalNode, FileNode, NodeData, SymbolNode};
44pub use scc::{DeployabilityHint, Scc, SccAnalysis};
45
46use crate::language::LangId;
47use crate::model::{FileId, SnapshotId, SymbolId};
48use petgraph::graph::{DiGraph, NodeIndex};
49
50#[derive(Debug, Clone)]
52pub struct CodeGraph {
53 pub graph: DiGraph<NodeData, EdgeData>,
55
56 pub(crate) file_to_index: HashMap<FileId, NodeIndex>,
58
59 pub(crate) symbol_to_index: HashMap<SymbolId, NodeIndex>,
61
62 pub(crate) external_index: HashMap<String, NodeIndex>,
64
65 pub snapshot_id: SnapshotId,
67}
68
69impl CodeGraph {
70 pub fn new(snapshot_id: SnapshotId) -> Self {
72 Self {
73 graph: DiGraph::new(),
74 file_to_index: HashMap::new(),
75 symbol_to_index: HashMap::new(),
76 external_index: HashMap::new(),
77 snapshot_id,
78 }
79 }
80 pub fn file_node_index(&self, file_id: FileId) -> Option<NodeIndex> {
81 self.file_to_index.get(&file_id).copied()
82 }
83 pub fn symbol_node_index(&self, symbol_id: SymbolId) -> Option<NodeIndex> {
84 self.symbol_to_index.get(&symbol_id).copied()
85 }
86 pub fn file_node(&self, file_id: FileId) -> Option<&FileNode> {
87 let idx = self.file_node_index(file_id)?;
88 self.graph.node_weight(idx).and_then(|data| data.as_file())
89 }
90 pub fn symbol_node(&self, symbol_id: SymbolId) -> Option<&SymbolNode> {
91 let idx = self.symbol_node_index(symbol_id)?;
92 self.graph
93 .node_weight(idx)
94 .and_then(|data| data.as_symbol())
95 }
96 pub fn file_count(&self) -> usize {
97 self.file_to_index.len()
98 }
99 pub fn symbol_count(&self) -> usize {
100 self.symbol_to_index.len()
101 }
102 pub fn external_count(&self) -> usize {
103 self.external_index.len()
104 }
105 pub fn node_count(&self) -> usize {
106 self.graph.node_count()
107 }
108 pub fn edge_count(&self) -> usize {
109 self.graph.edge_count()
110 }
111 pub fn get_or_create_external_node(&mut self, raw_path: String, language: LangId) -> NodeIndex {
114 if let Some(&idx) = self.external_index.get(&raw_path) {
115 return idx;
116 }
117 let node = NodeData::External(ExternalNode {
118 raw_path: raw_path.clone(),
119 language,
120 classification: None,
121 });
122 let idx = self.graph.add_node(node);
123 self.external_index.insert(raw_path, idx);
124 idx
125 }
126 pub fn add_edge_normalized(
129 &mut self,
130 source: NodeIndex,
131 target: NodeIndex,
132 kind: EdgeKind,
133 confidence: f32,
134 ) {
135 if let Some(edge_idx) = self.graph.find_edge(source, target) {
136 let existing = &mut self.graph[edge_idx];
137 if existing.kind == kind {
138 existing.confidence = existing.confidence.max(confidence);
139 return;
140 }
141 }
142 self.graph
143 .add_edge(source, target, EdgeData { kind, confidence });
144 }
145 pub fn files(&self) -> impl Iterator<Item = (FileId, &FileNode)> + '_ {
146 self.file_to_index.iter().filter_map(|(file_id, &idx)| {
147 self.graph
148 .node_weight(idx)
149 .and_then(|data| data.as_file().map(|f| (*file_id, f)))
150 })
151 }
152 pub fn symbols(&self) -> impl Iterator<Item = (SymbolId, &SymbolNode)> + '_ {
153 self.symbol_to_index.iter().filter_map(|(symbol_id, &idx)| {
154 self.graph
155 .node_weight(idx)
156 .and_then(|data| data.as_symbol().map(|s| (*symbol_id, s)))
157 })
158 }
159 pub fn edges_of_kind(
160 &self,
161 kind: EdgeKind,
162 ) -> impl Iterator<Item = (NodeIndex, NodeIndex)> + '_ {
163 self.graph.edge_indices().filter_map(move |edge_idx| {
164 let (source, target) = self.graph.edge_endpoints(edge_idx)?;
165 let weight = self.graph.edge_weight(edge_idx)?;
166 if weight.kind == kind {
167 Some((source, target))
168 } else {
169 None
170 }
171 })
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::language::LangId;
179 use crate::model::{LineColumn, SourceRange, Symbol, SymbolKind, Visibility, ids::SnapshotId};
180 use std::path::PathBuf;
181
182 fn test_range() -> SourceRange {
183 SourceRange {
184 byte_start: 0,
185 byte_end: 10,
186 start: LineColumn { line: 1, column: 0 },
187 end: LineColumn {
188 line: 1,
189 column: 10,
190 },
191 }
192 }
193
194 fn test_symbol(id: u32, name: &str) -> Symbol {
195 Symbol {
196 id: SymbolId(id),
197 name: name.to_string(),
198 kind: SymbolKind::Function,
199 language: LangId::Rust,
200 file_path: PathBuf::from("test.rs"),
201 source_range: test_range(),
202 visibility: Some(Visibility::Public),
203 signature: None,
204 docstring: None,
205 is_async: false,
206 }
207 }
208
209 #[test]
210 fn code_graph_new_empty() {
211 let graph = CodeGraph::new(SnapshotId(1));
212 assert_eq!(graph.node_count(), 0);
213 assert_eq!(graph.edge_count(), 0);
214 assert_eq!(graph.snapshot_id.0, 1);
215 }
216
217 #[test]
218 fn code_graph_file_lookup_returns_none_for_missing() {
219 let graph = CodeGraph::new(SnapshotId(1));
220 assert!(graph.file_node(FileId(0)).is_none());
221 assert!(graph.file_node_index(FileId(0)).is_none());
222 }
223
224 #[test]
225 fn code_graph_symbol_lookup_returns_none_for_missing() {
226 let graph = CodeGraph::new(SnapshotId(1));
227 assert!(graph.symbol_node(SymbolId(0)).is_none());
228 assert!(graph.symbol_node_index(SymbolId(0)).is_none());
229 }
230
231 #[test]
232 fn builder_produces_valid_code_graph() {
233 let mut builder = GraphBuilder::new(SnapshotId(1));
234 let file_id = builder.add_file(PathBuf::from("test.rs"), LangId::Rust);
235 let symbol = test_symbol(1, "main");
236 let _sym_idx = builder.add_symbol(&symbol).unwrap();
237
238 let graph = builder.build();
239
240 assert_eq!(graph.file_count(), 1);
241 assert_eq!(graph.symbol_count(), 1);
242 assert_eq!(graph.node_count(), 2);
243 assert_eq!(graph.edge_count(), 1); let file_lookup = graph.file_node(file_id);
247 assert!(file_lookup.is_some());
248 assert_eq!(file_lookup.unwrap().language, LangId::Rust);
249
250 let sym_lookup = graph.symbol_node(SymbolId(1));
251 assert!(sym_lookup.is_some());
252 assert_eq!(sym_lookup.unwrap().name, "main");
253 }
254
255 #[test]
256 fn code_graph_iteration_over_files() {
257 let mut builder = GraphBuilder::new(SnapshotId(1));
258 builder.add_file(PathBuf::from("a.rs"), LangId::Rust);
259 builder.add_file(PathBuf::from("b.py"), LangId::Python);
260
261 let graph = builder.build();
262 let files: Vec<_> = graph.files().collect();
263
264 assert_eq!(files.len(), 2);
265 }
266
267 #[test]
268 fn code_graph_iteration_over_symbols() {
269 let mut builder = GraphBuilder::new(SnapshotId(1));
270 let _file_id = builder.add_file(PathBuf::from("test.rs"), LangId::Rust);
271 let sym1 = test_symbol(1, "func_a");
272 let sym2 = test_symbol(2, "func_b");
273 builder.add_symbol(&sym1).unwrap();
274 builder.add_symbol(&sym2).unwrap();
275
276 let graph = builder.build();
277 let symbols: Vec<_> = graph.symbols().collect();
278
279 assert_eq!(symbols.len(), 2);
280 }
281
282 #[test]
283 fn code_graph_edges_of_kind_filtering() {
284 let mut builder = GraphBuilder::new(SnapshotId(1));
285 let file1 = builder.add_file(PathBuf::from("a.rs"), LangId::Rust);
286 let _file2 = builder.add_file(PathBuf::from("b.rs"), LangId::Rust);
287
288 builder.add_import(file1, PathBuf::from("b.rs"));
290
291 let graph = builder.build();
292
293 let ownership_edges: Vec<_> = graph.edges_of_kind(EdgeKind::Ownership).collect();
294 let import_edges: Vec<_> = graph.edges_of_kind(EdgeKind::Import).collect();
295
296 assert_eq!(ownership_edges.len(), 0); assert_eq!(import_edges.len(), 1);
298 }
299}