1use super::types::{DepGraph, IndexSymbol, IndexSymbolKind, SymbolIndex, Visibility};
7use serde::Serialize;
8
9#[derive(Debug, Clone, Serialize)]
11pub struct SymbolInfo {
12 pub id: u32,
14 pub name: String,
16 pub kind: String,
18 pub file: String,
20 pub line: u32,
22 pub end_line: u32,
24 pub signature: Option<String>,
26 pub visibility: String,
28}
29
30#[derive(Debug, Clone, Serialize)]
32pub struct ReferenceInfo {
33 pub symbol: SymbolInfo,
35 pub kind: String,
37}
38
39#[derive(Debug, Clone, Serialize)]
41pub struct CallGraphEdge {
42 pub caller_id: u32,
44 pub callee_id: u32,
46 pub caller: String,
48 pub callee: String,
50 pub file: String,
52 pub line: u32,
54}
55
56#[derive(Debug, Clone, Serialize)]
58pub struct CallGraph {
59 pub nodes: Vec<SymbolInfo>,
61 pub edges: Vec<CallGraphEdge>,
63 pub stats: CallGraphStats,
65}
66
67#[derive(Debug, Clone, Serialize)]
69pub struct CallGraphStats {
70 pub total_symbols: usize,
72 pub total_calls: usize,
74 pub functions: usize,
76 pub classes: usize,
78}
79
80impl SymbolInfo {
81 pub fn from_index_symbol(sym: &IndexSymbol, index: &SymbolIndex) -> Self {
83 let file_path = index
84 .get_file_by_id(sym.file_id.as_u32())
85 .map(|f| f.path.clone())
86 .unwrap_or_else(|| "<unknown>".to_owned());
87
88 Self {
89 id: sym.id.as_u32(),
90 name: sym.name.clone(),
91 kind: format_symbol_kind(sym.kind),
92 file: file_path,
93 line: sym.span.start_line,
94 end_line: sym.span.end_line,
95 signature: sym.signature.clone(),
96 visibility: format_visibility(sym.visibility),
97 }
98 }
99}
100
101pub fn find_symbol(index: &SymbolIndex, name: &str) -> Vec<SymbolInfo> {
106 let mut results: Vec<SymbolInfo> = index
107 .find_symbols(name)
108 .into_iter()
109 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
110 .collect();
111
112 results.sort_by(|a, b| {
114 (&a.file, a.line).cmp(&(&b.file, b.line))
115 });
116 results.dedup_by(|a, b| a.file == b.file && a.line == b.line);
117
118 results
119}
120
121pub fn get_callers_by_name(index: &SymbolIndex, graph: &DepGraph, name: &str) -> Vec<SymbolInfo> {
125 let mut callers = Vec::new();
126
127 for sym in index.find_symbols(name) {
129 let symbol_id = sym.id.as_u32();
130
131 for caller_id in graph.get_callers(symbol_id) {
133 if let Some(caller_sym) = index.get_symbol(caller_id) {
134 callers.push(SymbolInfo::from_index_symbol(caller_sym, index));
135 }
136 }
137 }
138
139 callers.sort_by_key(|s| s.id);
141 callers.dedup_by_key(|s| s.id);
142
143 callers
144}
145
146pub fn get_callees_by_name(index: &SymbolIndex, graph: &DepGraph, name: &str) -> Vec<SymbolInfo> {
150 let mut callees = Vec::new();
151
152 for sym in index.find_symbols(name) {
154 let symbol_id = sym.id.as_u32();
155
156 for callee_id in graph.get_callees(symbol_id) {
158 if let Some(callee_sym) = index.get_symbol(callee_id) {
159 callees.push(SymbolInfo::from_index_symbol(callee_sym, index));
160 }
161 }
162 }
163
164 callees.sort_by_key(|s| s.id);
166 callees.dedup_by_key(|s| s.id);
167
168 callees
169}
170
171pub fn get_references_by_name(
176 index: &SymbolIndex,
177 graph: &DepGraph,
178 name: &str,
179) -> Vec<ReferenceInfo> {
180 let mut references = Vec::new();
181
182 for sym in index.find_symbols(name) {
184 let symbol_id = sym.id.as_u32();
185
186 for caller_id in graph.get_callers(symbol_id) {
188 if let Some(caller_sym) = index.get_symbol(caller_id) {
189 references.push(ReferenceInfo {
190 symbol: SymbolInfo::from_index_symbol(caller_sym, index),
191 kind: "call".to_owned(),
192 });
193 }
194 }
195
196 for ref_id in graph.get_referencers(symbol_id) {
198 if let Some(ref_sym) = index.get_symbol(ref_id) {
199 if !graph.get_callers(symbol_id).contains(&ref_id) {
201 references.push(ReferenceInfo {
202 symbol: SymbolInfo::from_index_symbol(ref_sym, index),
203 kind: "reference".to_owned(),
204 });
205 }
206 }
207 }
208 }
209
210 references.sort_by_key(|r| r.symbol.id);
212 references.dedup_by_key(|r| r.symbol.id);
213
214 references
215}
216
217pub fn get_call_graph(index: &SymbolIndex, graph: &DepGraph) -> CallGraph {
222 get_call_graph_filtered(index, graph, None, None)
223}
224
225pub fn get_call_graph_filtered(
231 index: &SymbolIndex,
232 graph: &DepGraph,
233 max_nodes: Option<usize>,
234 max_edges: Option<usize>,
235) -> CallGraph {
236 let mut nodes: Vec<SymbolInfo> = index
238 .symbols
239 .iter()
240 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
241 .collect();
242
243 if let Some(limit) = max_nodes {
245 nodes.truncate(limit);
246 }
247
248 let node_ids: std::collections::HashSet<u32> = nodes.iter().map(|n| n.id).collect();
250
251 let mut edges: Vec<CallGraphEdge> = graph
253 .calls
254 .iter()
255 .filter(|(caller, callee)| {
256 max_nodes.is_none() || (node_ids.contains(caller) && node_ids.contains(callee))
258 })
259 .filter_map(|&(caller_id, callee_id)| {
260 let caller_sym = index.get_symbol(caller_id)?;
261 let callee_sym = index.get_symbol(callee_id)?;
262
263 let file_path = index
264 .get_file_by_id(caller_sym.file_id.as_u32())
265 .map(|f| f.path.clone())
266 .unwrap_or_else(|| "<unknown>".to_owned());
267
268 Some(CallGraphEdge {
269 caller_id,
270 callee_id,
271 caller: caller_sym.name.clone(),
272 callee: callee_sym.name.clone(),
273 file: file_path,
274 line: caller_sym.span.start_line,
275 })
276 })
277 .collect();
278
279 if let Some(limit) = max_edges {
281 edges.truncate(limit);
282 }
283
284 let functions = nodes
286 .iter()
287 .filter(|n| n.kind == "function" || n.kind == "method")
288 .count();
289 let classes = nodes
290 .iter()
291 .filter(|n| n.kind == "class" || n.kind == "struct")
292 .count();
293
294 let stats =
295 CallGraphStats { total_symbols: nodes.len(), total_calls: edges.len(), functions, classes };
296
297 CallGraph { nodes, edges, stats }
298}
299
300pub fn get_callers_by_id(index: &SymbolIndex, graph: &DepGraph, symbol_id: u32) -> Vec<SymbolInfo> {
302 graph
303 .get_callers(symbol_id)
304 .into_iter()
305 .filter_map(|id| index.get_symbol(id))
306 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
307 .collect()
308}
309
310pub fn get_callees_by_id(index: &SymbolIndex, graph: &DepGraph, symbol_id: u32) -> Vec<SymbolInfo> {
312 graph
313 .get_callees(symbol_id)
314 .into_iter()
315 .filter_map(|id| index.get_symbol(id))
316 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
317 .collect()
318}
319
320fn format_symbol_kind(kind: IndexSymbolKind) -> String {
323 match kind {
324 IndexSymbolKind::Function => "function",
325 IndexSymbolKind::Method => "method",
326 IndexSymbolKind::Class => "class",
327 IndexSymbolKind::Struct => "struct",
328 IndexSymbolKind::Interface => "interface",
329 IndexSymbolKind::Trait => "trait",
330 IndexSymbolKind::Enum => "enum",
331 IndexSymbolKind::Constant => "constant",
332 IndexSymbolKind::Variable => "variable",
333 IndexSymbolKind::Module => "module",
334 IndexSymbolKind::Import => "import",
335 IndexSymbolKind::Export => "export",
336 IndexSymbolKind::TypeAlias => "type_alias",
337 IndexSymbolKind::Macro => "macro",
338 }
339 .to_owned()
340}
341
342fn format_visibility(vis: Visibility) -> String {
343 match vis {
344 Visibility::Public => "public",
345 Visibility::Private => "private",
346 Visibility::Protected => "protected",
347 Visibility::Internal => "internal",
348 }
349 .to_owned()
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use crate::index::types::{FileEntry, FileId, Language, Span, SymbolId};
356
357 fn create_test_index() -> (SymbolIndex, DepGraph) {
358 let mut index = SymbolIndex::default();
359
360 index.files.push(FileEntry {
362 id: FileId::new(0),
363 path: "test.py".to_string(),
364 language: Language::Python,
365 symbols: 0..2,
366 imports: vec![],
367 content_hash: [0u8; 32],
368 lines: 25,
369 tokens: 100,
370 });
371
372 index.symbols.push(IndexSymbol {
374 id: SymbolId::new(0),
375 name: "main".to_string(),
376 kind: IndexSymbolKind::Function,
377 file_id: FileId::new(0),
378 span: Span { start_line: 1, start_col: 0, end_line: 10, end_col: 0 },
379 signature: Some("def main()".to_string()),
380 parent: None,
381 visibility: Visibility::Public,
382 docstring: None,
383 });
384
385 index.symbols.push(IndexSymbol {
386 id: SymbolId::new(1),
387 name: "helper".to_string(),
388 kind: IndexSymbolKind::Function,
389 file_id: FileId::new(0),
390 span: Span { start_line: 12, start_col: 0, end_line: 20, end_col: 0 },
391 signature: Some("def helper()".to_string()),
392 parent: None,
393 visibility: Visibility::Private,
394 docstring: None,
395 });
396
397 index.symbols_by_name.insert("main".to_string(), vec![0]);
399 index.symbols_by_name.insert("helper".to_string(), vec![1]);
400
401 let mut graph = DepGraph::new();
403 graph.add_call(0, 1); (index, graph)
406 }
407
408 #[test]
409 fn test_find_symbol() {
410 let (index, _graph) = create_test_index();
411
412 let results = find_symbol(&index, "main");
413 assert_eq!(results.len(), 1);
414 assert_eq!(results[0].name, "main");
415 assert_eq!(results[0].kind, "function");
416 assert_eq!(results[0].file, "test.py");
417 }
418
419 #[test]
420 fn test_get_callers() {
421 let (index, graph) = create_test_index();
422
423 let callers = get_callers_by_name(&index, &graph, "helper");
425 assert_eq!(callers.len(), 1);
426 assert_eq!(callers[0].name, "main");
427 }
428
429 #[test]
430 fn test_get_callees() {
431 let (index, graph) = create_test_index();
432
433 let callees = get_callees_by_name(&index, &graph, "main");
435 assert_eq!(callees.len(), 1);
436 assert_eq!(callees[0].name, "helper");
437 }
438
439 #[test]
440 fn test_get_call_graph() {
441 let (index, graph) = create_test_index();
442
443 let call_graph = get_call_graph(&index, &graph);
444 assert_eq!(call_graph.nodes.len(), 2);
445 assert_eq!(call_graph.edges.len(), 1);
446 assert_eq!(call_graph.stats.functions, 2);
447
448 assert_eq!(call_graph.edges[0].caller, "main");
450 assert_eq!(call_graph.edges[0].callee, "helper");
451 }
452}