1use std::collections::{HashSet, VecDeque};
11
12use super::index::SemanticIndex;
13use super::types::{RefKind, Reference, Symbol};
14
15#[derive(Debug, Clone)]
21pub struct InvocationPath {
22 pub entry_point: u32,
24 pub chain: Vec<u32>,
27 pub call_lines: Vec<u32>,
29}
30
31impl InvocationPath {
32 pub fn new(entry_point: u32, chain: Vec<u32>, call_lines: Vec<u32>) -> Self {
34 Self {
35 entry_point,
36 chain,
37 call_lines,
38 }
39 }
40
41 pub fn target(&self) -> u32 {
43 *self.chain.last().unwrap_or(&self.entry_point)
44 }
45
46 pub fn depth(&self) -> usize {
48 self.chain.len().saturating_sub(1)
49 }
50
51 pub fn is_direct(&self) -> bool {
53 self.chain.len() <= 2
54 }
55}
56
57#[derive(Debug, Clone)]
63pub struct TraceResult {
64 pub target: u32,
66 pub paths: Vec<InvocationPath>,
68 pub visited_count: usize,
70}
71
72impl TraceResult {
73 pub fn has_paths(&self) -> bool {
75 !self.paths.is_empty()
76 }
77
78 pub fn entry_point_count(&self) -> usize {
80 let entries: HashSet<_> = self.paths.iter().map(|p| p.entry_point).collect();
81 entries.len()
82 }
83
84 pub fn min_depth(&self) -> Option<usize> {
86 self.paths.iter().map(|p| p.depth()).min()
87 }
88
89 pub fn max_depth(&self) -> Option<usize> {
91 self.paths.iter().map(|p| p.depth()).max()
92 }
93}
94
95pub fn trace_symbol(
112 index: &SemanticIndex,
113 target_id: u32,
114 max_depth: Option<usize>,
115) -> TraceResult {
116 let max_depth = max_depth.unwrap_or(50);
117
118 if index.symbol(target_id).is_none() {
120 return TraceResult {
121 target: target_id,
122 paths: Vec::new(),
123 visited_count: 0,
124 };
125 }
126
127 let mut visited: HashSet<u32> = HashSet::new();
129 let mut queue: VecDeque<(u32, Vec<u32>, Vec<u32>)> = VecDeque::new();
130 let mut paths: Vec<InvocationPath> = Vec::new();
131
132 queue.push_back((target_id, vec![target_id], Vec::new()));
134 visited.insert(target_id);
135
136 while let Some((current, chain, call_lines)) = queue.pop_front() {
137 if chain.len() > max_depth {
139 continue;
140 }
141
142 let symbol = match index.symbol(current) {
144 Some(s) => s,
145 None => continue,
146 };
147
148 if symbol.is_entry_point() {
150 let mut path_chain = chain.clone();
152 let mut path_lines = call_lines.clone();
153 path_chain.reverse();
154 path_lines.reverse();
155
156 paths.push(InvocationPath::new(current, path_chain, path_lines));
157 }
158
159 for &caller_id in index.callers(current) {
161 if visited.contains(&caller_id) {
162 continue;
165 }
166
167 let call_line = index
169 .edges
170 .iter()
171 .find(|e| e.from_symbol == caller_id && e.to_symbol == current)
172 .map(|e| e.line)
173 .unwrap_or(0);
174
175 let mut new_chain = chain.clone();
177 new_chain.push(caller_id);
178
179 let mut new_lines = call_lines.clone();
180 new_lines.push(call_line);
181
182 visited.insert(caller_id);
183 queue.push_back((caller_id, new_chain, new_lines));
184 }
185 }
186
187 TraceResult {
188 target: target_id,
189 paths,
190 visited_count: visited.len(),
191 }
192}
193
194pub fn trace_symbol_by_name(
198 index: &SemanticIndex,
199 name: &str,
200 max_depth: Option<usize>,
201) -> Vec<TraceResult> {
202 let symbol_ids = match index.symbols_by_name(name) {
203 Some(ids) => ids.clone(),
204 None => return Vec::new(),
205 };
206
207 symbol_ids
208 .iter()
209 .map(|&id| trace_symbol(index, id, max_depth))
210 .collect()
211}
212
213#[derive(Debug, Clone)]
219pub struct ReferenceContext {
220 pub reference: Reference,
222 pub token_id: u32,
224 pub file_id: u16,
226 pub line: u32,
228 pub column: u16,
230 pub scope_id: u32,
232 pub symbol_name: Option<String>,
234}
235
236pub fn find_refs(index: &SemanticIndex, symbol_id: u32) -> Vec<ReferenceContext> {
238 let mut results = Vec::new();
239
240 for reference in index.references_to(symbol_id) {
241 if let Some(token) = index.token(reference.token_id) {
242 let symbol_name = index
243 .symbol(symbol_id)
244 .and_then(|s| index.symbol_name(s))
245 .map(|s| s.to_string());
246
247 results.push(ReferenceContext {
248 reference: *reference,
249 token_id: reference.token_id,
250 file_id: token.file_id,
251 line: token.line,
252 column: token.column,
253 scope_id: token.scope_id,
254 symbol_name,
255 });
256 }
257 }
258
259 results
260}
261
262pub fn find_refs_of_kind(
264 index: &SemanticIndex,
265 symbol_id: u32,
266 kind: RefKind,
267) -> Vec<ReferenceContext> {
268 find_refs(index, symbol_id)
269 .into_iter()
270 .filter(|r| r.reference.ref_kind() == kind)
271 .collect()
272}
273
274pub fn find_call_refs(index: &SemanticIndex, symbol_id: u32) -> Vec<ReferenceContext> {
276 find_refs_of_kind(index, symbol_id, RefKind::Call)
277}
278
279pub fn find_read_refs(index: &SemanticIndex, symbol_id: u32) -> Vec<ReferenceContext> {
281 find_refs_of_kind(index, symbol_id, RefKind::Read)
282}
283
284pub fn find_write_refs(index: &SemanticIndex, symbol_id: u32) -> Vec<ReferenceContext> {
286 find_refs_of_kind(index, symbol_id, RefKind::Write)
287}
288
289pub fn find_dead_symbols(index: &SemanticIndex) -> Vec<&Symbol> {
300 index
301 .symbols
302 .iter()
303 .filter(|s| {
304 if s.is_entry_point() {
306 return false;
307 }
308
309 let has_callers = !index.callers(s.id).is_empty();
311 if has_callers {
312 return false;
313 }
314
315 let has_refs = index.references_to(s.id).next().is_some();
317 !has_refs
318 })
319 .collect()
320}
321
322pub fn format_call_chain(index: &SemanticIndex, chain: &[u32]) -> String {
328 chain
329 .iter()
330 .filter_map(|&id| index.symbol(id).and_then(|s| index.symbol_name(s)))
331 .collect::<Vec<_>>()
332 .join(" -> ")
333}
334
335pub fn format_invocation_path(index: &SemanticIndex, path: &InvocationPath) -> String {
337 let mut result = String::new();
338
339 for (i, &symbol_id) in path.chain.iter().enumerate() {
340 if let Some(symbol) = index.symbol(symbol_id) {
341 let name = index.symbol_name(symbol).unwrap_or("<unknown>");
342 let file = index
343 .file_path(symbol.file_id)
344 .map(|p| p.to_string_lossy().to_string())
345 .unwrap_or_else(|| "<unknown>".into());
346 let start_line = symbol.start_line;
347
348 if i == 0 {
349 result.push_str(&format!("{} ({}:{})", name, file, start_line));
350 } else {
351 let call_line = path.call_lines.get(i - 1).copied().unwrap_or(0);
352 result.push_str(&format!(
353 "\n -> {} ({}:{}) [called at line {}]",
354 name, file, start_line, call_line
355 ));
356 }
357 }
358 }
359
360 result
361}
362
363#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::trace::types::{Edge, SymbolFlags, SymbolKind};
371
372 fn create_test_index() -> SemanticIndex {
373 let mut index = SemanticIndex::new();
374
375 let file_id = index.add_file("test.rs".into());
377
378 let names: Vec<_> = ["main", "a", "b", "target"]
380 .iter()
381 .map(|n| index.strings.intern(n))
382 .collect();
383
384 index.add_symbol(
386 Symbol::new(
387 0,
388 names[0],
389 file_id,
390 SymbolKind::Function,
391 SymbolFlags::IS_ENTRY_POINT,
392 1,
393 10,
394 ),
395 "main",
396 );
397
398 for (i, name) in ["a", "b", "target"].iter().enumerate() {
400 index.add_symbol(
401 Symbol::new(
402 (i + 1) as u32,
403 names[i + 1],
404 file_id,
405 SymbolKind::Function,
406 SymbolFlags::empty(),
407 ((i + 1) * 10 + 1) as u32,
408 ((i + 2) * 10) as u32,
409 ),
410 name,
411 );
412 }
413
414 index.add_edge(Edge::new(0, 1, 5)); index.add_edge(Edge::new(1, 2, 15)); index.add_edge(Edge::new(2, 3, 25)); index
420 }
421
422 #[test]
423 fn test_trace_symbol() {
424 let index = create_test_index();
425
426 let result = trace_symbol(&index, 3, None);
428
429 assert!(result.has_paths());
430 assert_eq!(result.paths.len(), 1);
431
432 let path = &result.paths[0];
433 assert_eq!(path.entry_point, 0); assert_eq!(path.chain, vec![0, 1, 2, 3]); assert_eq!(path.depth(), 3);
436 }
437
438 #[test]
439 fn test_trace_direct_call() {
440 let index = create_test_index();
441
442 let result = trace_symbol(&index, 1, None);
444
445 assert!(result.has_paths());
446 assert_eq!(result.paths.len(), 1);
447
448 let path = &result.paths[0];
449 assert_eq!(path.chain, vec![0, 1]); assert!(path.is_direct());
451 }
452
453 #[test]
454 fn test_trace_nonexistent() {
455 let index = create_test_index();
456
457 let result = trace_symbol(&index, 999, None);
459
460 assert!(!result.has_paths());
461 assert_eq!(result.visited_count, 0);
462 }
463
464 #[test]
465 fn test_format_call_chain() {
466 let index = create_test_index();
467 let chain = vec![0, 1, 2, 3];
468
469 let formatted = format_call_chain(&index, &chain);
470 assert_eq!(formatted, "main -> a -> b -> target");
471 }
472
473 #[test]
474 fn test_find_dead_symbols() {
475 let mut index = SemanticIndex::new();
476 let file_id = index.add_file("test.rs".into());
477
478 let name1 = index.strings.intern("main");
480 index.add_symbol(
481 Symbol::new(
482 0,
483 name1,
484 file_id,
485 SymbolKind::Function,
486 SymbolFlags::IS_ENTRY_POINT,
487 1,
488 10,
489 ),
490 "main",
491 );
492
493 let name2 = index.strings.intern("used");
495 index.add_symbol(
496 Symbol::new(
497 1,
498 name2,
499 file_id,
500 SymbolKind::Function,
501 SymbolFlags::empty(),
502 15,
503 25,
504 ),
505 "used",
506 );
507
508 let name3 = index.strings.intern("dead");
510 index.add_symbol(
511 Symbol::new(
512 2,
513 name3,
514 file_id,
515 SymbolKind::Function,
516 SymbolFlags::empty(),
517 30,
518 40,
519 ),
520 "dead",
521 );
522
523 index.add_edge(Edge::new(0, 1, 5));
525
526 let dead = find_dead_symbols(&index);
527 assert_eq!(dead.len(), 1);
528 assert_eq!(dead[0].id, 2);
529 }
530}