magellan/graph/algorithms.rs
1//! Graph algorithms for code analysis
2//!
3//! This module provides wrapper functions for sqlitegraph's algorithm library,
4//! exposing reachability analysis, dead code detection, cycle detection, call
5//! graph condensation, and impact analysis through Magellan's CodeGraph API.
6//!
7//! # Graph Views
8//!
9//! Magellan stores multiple edge types in the same graph:
10//! - **DEFINES**: File → Symbol (defines/contains relationship)
11//! - **REFERENCES**: Reference → Symbol (what symbol is referenced)
12//! - **CALLS**: Call node → Symbol (call graph edges)
13//! - **CALLER**: Symbol → Call node (reverse call graph edges)
14//!
15//! For call graph algorithms (reachability, dead code detection, SCC), we filter
16//! to **CALLS** edges only to traverse the call graph structure.
17//!
18//! # Clustered Adjacency
19//!
20//! Magellan uses sqlitegraph's clustered adjacency storage for ~10x graph
21//! traversal performance improvement when available.
22//!
23//! # Entity IDs vs Symbol IDs
24//!
25//! sqlitegraph algorithms work with **entity IDs** (i64 database row IDs),
26//! while Magellan's public API uses **stable symbol IDs** (32-char BLAKE3 hashes).
27//!
28//! This module provides translation functions:
29//! - `resolve_symbol_entity()`: Symbol ID → entity ID
30//! - `symbol_by_entity_id()`: entity ID → SymbolInfo
31//!
32//! # Algorithm Functions
33//!
34//! - [`CodeGraph::reachable_symbols()`]: Forward reachability from a symbol
35//! - [`CodeGraph::reverse_reachable_symbols()`]: Reverse reachability (callers)
36//! - [`CodeGraph::dead_symbols()`]: Dead code detection from entry point
37//! - [`CodeGraph::detect_cycles()`]: Find cycles using SCC decomposition
38//! - [`CodeGraph::find_cycles_containing()`]: Find cycles containing a specific symbol
39//! - [`CodeGraph::condense_call_graph()`]: Collapse SCCs to create condensation DAG
40//! - [`CodeGraph::enumerate_paths()`]: Path enumeration between symbols
41//! - [`CodeGraph::backward_slice()`]: Backward program slice (what affects this symbol)
42//! - [`CodeGraph::forward_slice()`]: Forward program slice (what this symbol affects)
43//!
44//! # Example
45//!
46//! \`\`\`no_run
47//! use magellan::CodeGraph;
48//!
49//! let mut graph = CodeGraph::open("codegraph.db")?;
50//!
51//! // Find all functions reachable from main
52//! let reachable = graph.reachable_symbols("main_symbol_id", None)?;
53//!
54//! // Find dead code unreachable from main
55//! let dead = graph.dead_symbols("main_symbol_id")?;
56//!
57//! // Find cycles (mutual recursion)
58//! let cycles = graph.detect_cycles()?;
59//!
60//! // Condense call graph for DAG analysis
61//! let condensed = graph.condense_call_graph()?;
62//! \`\`\`
63
64use anyhow::Result;
65use ahash::AHashSet;
66use rusqlite::params;
67use sqlitegraph::{algo, GraphBackend, SnapshotId, SqliteGraphBackend};
68use std::collections::{HashMap, HashSet};
69use std::sync::Arc;
70
71use crate::graph::schema::SymbolNode;
72
73use super::CodeGraph;
74
75/// Helper to get the underlying SqliteGraph from a trait object
76///
77/// This is a temporary workaround until algorithm methods are added to GraphBackend trait.
78/// For SQLite backend, this extracts the concrete SqliteGraphBackend and calls .graph().
79///
80/// # Safety
81///
82/// This function uses unsafe pointer casting because the GraphBackend trait doesn't
83/// provide downcasting methods. It's safe as long as:
84/// - The backend is actually a SqliteGraphBackend (not V3 or other backend)
85/// - The Arc hasn't been cloned/reinterpreted
86///
87/// TODO: Remove this when sqlitegraph adds `as_any()` to GraphBackend trait
88fn get_sqlite_graph(backend: &Arc<dyn GraphBackend>) -> Result<&sqlitegraph::graph::SqliteGraph, anyhow::Error> {
89 // SAFETY: This is a temporary workaround. We use pointer casting to downcast
90 // from Arc<dyn GraphBackend> to Arc<SqliteGraphBackend>. This is safe because:
91 // 1. We only call this when we know the backend is SqliteGraphBackend
92 // 2. The Arc layout is stable for trait objects
93 // 3. We only read, don't modify
94 unsafe {
95 let ptr = Arc::as_ptr(backend);
96 // Double-check pointer is non-null before dereferencing
97 if ptr.is_null() {
98 return Err(anyhow::anyhow!("Null backend pointer"));
99 }
100 // Reinterpret as SqliteGraphBackend pointer
101 let concrete = &*(ptr as *const SqliteGraphBackend);
102 Ok(concrete.graph())
103 }
104}
105
106/// Symbol information for algorithm results
107///
108/// Contains the key metadata needed to identify and locate a symbol.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct SymbolInfo {
111 /// Stable symbol ID (32-char BLAKE3 hash)
112 pub symbol_id: Option<String>,
113 /// Fully-qualified name
114 pub fqn: Option<String>,
115 /// File path containing the symbol
116 pub file_path: String,
117 /// Symbol kind (Function, Method, Class, etc.)
118 pub kind: String,
119}
120
121/// Dead symbol information
122///
123/// Extends [`SymbolInfo`] with a reason why the symbol is considered dead.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct DeadSymbol {
126 /// Base symbol information
127 pub symbol: SymbolInfo,
128 /// Reason why this symbol is unreachable/dead
129 pub reason: String,
130}
131
132/// Cycle kind classification
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum CycleKind {
135 /// Multiple symbols calling each other (SCC with >1 member)
136 MutualRecursion,
137 /// Single symbol that calls itself (direct self-loop)
138 SelfLoop,
139}
140
141/// Cycle information for detected cycles
142///
143/// Represents a strongly connected component (SCC) with more than one member,
144/// indicating mutual recursion or a cycle in the call graph.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct Cycle {
147 /// All symbols that participate in this cycle
148 pub members: Vec<SymbolInfo>,
149 /// Classification of the cycle type
150 pub kind: CycleKind,
151}
152
153/// Cycle detection report
154///
155/// Result of running [`CodeGraph::detect_cycles()`], containing all cycles
156/// found in the call graph.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct CycleReport {
159 /// All detected cycles
160 pub cycles: Vec<Cycle>,
161 /// Total number of cycles found
162 pub total_count: usize,
163}
164
165/// Supernode in a condensation graph
166///
167/// Represents an SCC collapsed into a single node for DAG analysis.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct Supernode {
170 /// Supernode ID (stable identifier for this SCC)
171 pub id: i64,
172 /// All symbols that are members of this SCC/supernode
173 pub members: Vec<SymbolInfo>,
174}
175
176/// Condensation graph (DAG after SCC collapse)
177///
178/// Represents the call graph after collapsing all SCCs into supernodes.
179/// The condensation graph is always a DAG (no cycles).
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct CondensationGraph {
182 /// All supernodes in the condensed graph
183 pub supernodes: Vec<Supernode>,
184 /// Edges between supernodes (from_supernode_id, to_supernode_id)
185 pub edges: Vec<(i64, i64)>,
186}
187
188/// Condensation result with symbol-to-supernode mapping
189///
190/// Result of running [`CodeGraph::condense_call_graph()`], providing
191/// both the condensed DAG and the mapping from original symbols to supernodes.
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct CondensationResult {
194 /// The condensed DAG
195 pub graph: CondensationGraph,
196 /// Maps symbol_id to the supernode ID containing that symbol
197 pub original_to_supernode: HashMap<String, i64>,
198}
199
200/// Direction of program slicing
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum SliceDirection {
203 /// Backward slice: what affects this symbol (reverse reachability)
204 Backward,
205 /// Forward slice: what this symbol affects (forward reachability)
206 Forward,
207}
208
209/// Program slice result
210///
211/// Contains the slice results and statistics for a program slicing operation.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct ProgramSlice {
214 /// Target symbol for the slice
215 pub target: SymbolInfo,
216 /// Direction of the slice
217 pub direction: SliceDirection,
218 /// Symbols included in the slice
219 pub included_symbols: Vec<SymbolInfo>,
220 /// Number of symbols in the slice
221 pub symbol_count: usize,
222}
223
224/// Program slice result with statistics
225///
226/// Wraps a [`ProgramSlice`] with additional statistics about the slice.
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct SliceResult {
229 /// The slice itself
230 pub slice: ProgramSlice,
231 /// Statistics about the slice
232 pub statistics: SliceStatistics,
233}
234
235/// Statistics for a program slice
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct SliceStatistics {
238 /// Total number of symbols in the slice
239 pub total_symbols: usize,
240 /// Number of data dependencies
241 /// Note: Set to 0 for call-graph fallback (not computed without full CFG)
242 pub data_dependencies: usize,
243 /// Number of control dependencies
244 /// For call-graph fallback, this equals total_symbols (callers/callees)
245 pub control_dependencies: usize,
246}
247
248/// Execution path in the call graph
249///
250/// Represents a single path through the call graph from a starting symbol
251/// to an ending symbol, with metadata about the path.
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct ExecutionPath {
254 /// Symbols along the path in order from start to end
255 pub symbols: Vec<SymbolInfo>,
256 /// Number of symbols in the path
257 pub length: usize,
258}
259
260/// Path enumeration result
261///
262/// Contains all discovered execution paths and statistics about the enumeration.
263#[derive(Debug, Clone)]
264pub struct PathEnumerationResult {
265 /// All discovered paths
266 pub paths: Vec<ExecutionPath>,
267 /// Total number of paths enumerated
268 pub total_enumerated: usize,
269 /// Whether enumeration was cut off due to bounds
270 pub bounded_hit: bool,
271 /// Statistics about the discovered paths
272 pub statistics: PathStatistics,
273}
274
275/// Statistics for path enumeration
276#[derive(Debug, Clone)]
277pub struct PathStatistics {
278 /// Average path length
279 pub avg_length: f64,
280 /// Minimum path length
281 pub min_length: usize,
282 /// Maximum path length
283 pub max_length: usize,
284 /// Number of unique symbols across all paths
285 pub unique_symbols: usize,
286}
287
288impl CodeGraph {
289 /// Resolve a stable symbol ID or FQN to its entity ID
290 ///
291 /// First tries to lookup by symbol_id (32-char BLAKE3 hash).
292 /// If not found, falls back to FQN lookup for convenience.
293 ///
294 /// # Arguments
295 /// * `symbol_id_or_fqn` - Stable symbol ID (32-char BLAKE3 hash) or FQN
296 ///
297 /// # Returns
298 /// The entity ID (i64 database row ID) for the symbol
299 ///
300 /// # Errors
301 /// Returns an error if the symbol is not found in the database
302 fn resolve_symbol_entity(&self, symbol_id_or_fqn: &str) -> Result<i64> {
303 let conn = self.chunks.connect()?;
304
305 // First try: lookup by symbol_id
306 let mut stmt = conn
307 .prepare_cached(
308 "SELECT id FROM graph_entities
309 WHERE kind = 'Symbol'
310 AND json_extract(data, '$.symbol_id') = ?1",
311 )
312 .map_err(|e| anyhow::anyhow!("Failed to prepare symbol ID query: {}", e))?;
313
314 let result = stmt.query_row(params![symbol_id_or_fqn], |row| row.get::<_, i64>(0));
315
316 match result {
317 Ok(entity_id) => return Ok(entity_id),
318 Err(rusqlite::Error::QueryReturnedNoRows) => {
319 // Fallback: try FQN lookup
320 }
321 Err(e) => {
322 return Err(anyhow::anyhow!("Failed to query symbol ID: {}", e));
323 }
324 }
325
326 // Fallback: lookup by FQN or display_fqn
327 let mut stmt = conn
328 .prepare_cached(
329 "SELECT id FROM graph_entities
330 WHERE kind = 'Symbol'
331 AND (json_extract(data, '$.fqn') = ?1
332 OR json_extract(data, '$.display_fqn') = ?1
333 OR json_extract(data, '$.canonical_fqn') = ?1)",
334 )
335 .map_err(|e| anyhow::anyhow!("Failed to prepare FQN query: {}", e))?;
336
337 stmt.query_row(params![symbol_id_or_fqn], |row| row.get::<_, i64>(0))
338 .map_err(|e| match e {
339 rusqlite::Error::QueryReturnedNoRows => {
340 anyhow::anyhow!(
341 "Symbol '{}' not found in database (tried symbol_id, fqn, display_fqn, canonical_fqn)",
342 symbol_id_or_fqn
343 )
344 }
345 _ => anyhow::anyhow!("Failed to query symbol by FQN: {}", e),
346 })
347 }
348
349 /// Get symbol information by entity ID
350 ///
351 /// # Arguments
352 /// * `entity_id` - Entity ID (i64 database row ID)
353 ///
354 /// # Returns
355 /// SymbolInfo with metadata from the symbol node
356 pub fn symbol_by_entity_id(&self, entity_id: i64) -> Result<SymbolInfo> {
357 let snapshot = SnapshotId::current();
358 let node = self
359 .calls
360 .backend
361 .get_node(snapshot, entity_id)
362 .map_err(|e| anyhow::anyhow!("Failed to get entity {}: {}", entity_id, e))?;
363
364 if node.kind != "Symbol" {
365 return Err(anyhow::anyhow!(
366 "Entity {} is not a Symbol (kind: {})",
367 entity_id,
368 node.kind
369 ));
370 }
371
372 let symbol_node: SymbolNode = serde_json::from_value(node.data)
373 .map_err(|e| anyhow::anyhow!("Failed to parse SymbolNode data: {}", e))?;
374
375 Ok(SymbolInfo {
376 symbol_id: symbol_node.symbol_id,
377 fqn: symbol_node.fqn.or_else(|| symbol_node.display_fqn.clone()),
378 file_path: node
379 .file_path
380 .unwrap_or_else(|| "?".to_string()),
381 kind: symbol_node.kind,
382 })
383 }
384
385 /// Get all Symbol entity IDs in the call graph
386 ///
387 /// Returns entity IDs for all Symbol nodes that are part of the call graph
388 /// (have incoming or outgoing CALLS edges).
389 ///
390 /// # Returns
391 /// Vector of entity IDs for call graph symbols
392 fn all_call_graph_entities(&self) -> Result<Vec<i64>> {
393 let conn = self.chunks.connect()?;
394
395 // Find all symbols that participate in CALLS edges
396 // (either as caller via CALLER edges or as callee via CALLS edges)
397 let mut stmt = conn
398 .prepare_cached(
399 "SELECT DISTINCT from_id FROM graph_edges WHERE edge_type = 'CALLER'
400 UNION
401 SELECT DISTINCT to_id FROM graph_edges WHERE edge_type = 'CALLS'",
402 )
403 .map_err(|e| anyhow::anyhow!("Failed to prepare call graph query: {}", e))?;
404
405 let entity_ids = stmt
406 .query_map([], |row| row.get::<_, i64>(0))
407 .map_err(|e| anyhow::anyhow!("Failed to execute call graph query: {}", e))?
408 .collect::<Result<Vec<_>, _>>()
409 .map_err(|e| anyhow::anyhow!("Failed to collect call graph results: {}", e))?;
410
411 Ok(entity_ids)
412 }
413
414 /// Find all symbols reachable from a given symbol (forward reachability)
415 ///
416 /// Computes the transitive closure of the call graph starting from the
417 /// specified symbol. Returns all symbols that can be reached by following
418 /// CALLS edges from the starting symbol.
419 ///
420 /// # Graph View
421 ///
422 /// This operates on the **call graph** (CALLS edges only), not other edge types.
423 /// The starting symbol itself is NOT included in the results.
424 ///
425 /// # Arguments
426 /// * `symbol_id` - Stable symbol ID to start from (or FQN as fallback)
427 /// * `max_depth` - Optional maximum depth limit (None = unlimited)
428 ///
429 /// # Returns
430 /// Vector of [`SymbolInfo`] for reachable symbols, sorted deterministically
431 ///
432 /// # Example
433 ///
434 /// \`\`\`no_run
435 /// # use magellan::CodeGraph;
436 /// # let mut graph = CodeGraph::open("test.db").unwrap();
437 /// // Find all functions called from main (directly or indirectly)
438 /// let reachable = graph.reachable_symbols("main", None)?;
439 /// \`\`\`
440 pub fn reachable_symbols(
441 &self,
442 symbol_id: &str,
443 _max_depth: Option<usize>,
444 ) -> Result<Vec<SymbolInfo>> {
445 let entity_id = self.resolve_symbol_entity(symbol_id)?;
446 let backend = &self.calls.backend;
447
448 // Use sqlitegraph's reachable_from algorithm
449 // This traverses outgoing edges from the start node
450 // Note: sqlitegraph 1.3.0 API takes (graph, start), not (backend, snapshot, start, depth)
451 let reachable_entity_ids = algo::reachable_from(get_sqlite_graph(backend)?, entity_id)?;
452
453 // Convert entity IDs to SymbolInfo
454 let mut symbols = Vec::new();
455 for id in reachable_entity_ids {
456 // Skip the starting symbol itself
457 if id == entity_id {
458 continue;
459 }
460
461 if let Ok(info) = self.symbol_by_entity_id(id) {
462 symbols.push(info);
463 }
464 }
465
466 // Sort deterministically for stable output
467 symbols.sort_by(|a, b| {
468 a.file_path
469 .cmp(&b.file_path)
470 .then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
471 .then_with(|| a.kind.cmp(&b.kind))
472 });
473
474 Ok(symbols)
475 }
476
477 /// Find all symbols that can reach a given symbol (reverse reachability)
478 ///
479 /// Computes the reverse transitive closure of the call graph. Returns all
480 /// symbols from which the specified symbol can be reached by following
481 /// CALLS edges (i.e., all callers that directly or indirectly call this symbol).
482 ///
483 /// # Graph View
484 ///
485 /// This operates on the **call graph** (CALLS edges only).
486 ///
487 /// # Arguments
488 /// * `symbol_id` - Stable symbol ID to analyze
489 /// * `max_depth` - Optional maximum depth limit (None = unlimited)
490 ///
491 /// # Returns
492 /// Vector of [`SymbolInfo`] for symbols that can reach the target, sorted deterministically
493 ///
494 /// # Example
495 ///
496 /// \`\`\`no_run
497 /// # use magellan::CodeGraph;
498 /// # let mut graph = CodeGraph::open("test.db").unwrap();
499 /// // Find all functions that directly or indirectly call 'helper_function'
500 /// let callers = graph.reverse_reachable_symbols("helper_symbol_id", None)?;
501 /// \`\`\`
502 pub fn reverse_reachable_symbols(
503 &self,
504 symbol_id: &str,
505 _max_depth: Option<usize>,
506 ) -> Result<Vec<SymbolInfo>> {
507 let entity_id = self.resolve_symbol_entity(symbol_id)?;
508 let backend = &self.calls.backend;
509
510 // Use sqlitegraph's reverse_reachable_from algorithm
511 // This traverses incoming edges to the target node
512 // Note: sqlitegraph 1.3.0 API takes (graph, target)
513 let reachable_entity_ids = algo::reverse_reachable_from(get_sqlite_graph(backend)?, entity_id)?;
514
515 // Convert entity IDs to SymbolInfo
516 let mut symbols = Vec::new();
517 for id in reachable_entity_ids {
518 // Skip the starting symbol itself
519 if id == entity_id {
520 continue;
521 }
522
523 if let Ok(info) = self.symbol_by_entity_id(id) {
524 symbols.push(info);
525 }
526 }
527
528 // Sort deterministically for stable output
529 symbols.sort_by(|a, b| {
530 a.file_path
531 .cmp(&b.file_path)
532 .then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
533 .then_with(|| a.kind.cmp(&b.kind))
534 });
535
536 Ok(symbols)
537 }
538
539 /// Find dead code unreachable from an entry point symbol
540 ///
541 /// Identifies all symbols in the call graph that cannot be reached from
542 /// the specified entry point (e.g., `main`, `test_main`). This is useful
543 /// for detecting unused functions and dead code.
544 ///
545 /// # Graph View
546 ///
547 /// This operates on the **call graph** (CALLS edges only). Symbols not
548 /// participating in call edges are not considered.
549 ///
550 /// # Limitations
551 ///
552 /// - Only considers the call graph. Symbols called via reflection,
553 /// function pointers, or dynamic dispatch may be incorrectly flagged.
554 /// - Test functions, benchmark code, and platform-specific code may
555 /// appear as dead code if not reachable from the specified entry point.
556 ///
557 /// # Arguments
558 /// * `entry_symbol_id` - Stable symbol ID of the entry point (e.g., main function)
559 ///
560 /// # Returns
561 /// Vector of [`DeadSymbol`] for unreachable symbols, sorted deterministically
562 ///
563 /// # Example
564 ///
565 /// \`\`\`no_run
566 /// # use magellan::CodeGraph;
567 /// # let mut graph = CodeGraph::open("test.db").unwrap();
568 /// // Find all functions unreachable from main
569 /// let dead = graph.dead_symbols("main_symbol_id")?;
570 /// for dead_symbol in dead {
571 /// println!("Dead: {} in {} ({})",
572 /// dead_symbol.symbol.fqn.as_deref().unwrap_or("?"),
573 /// dead_symbol.symbol.file_path,
574 /// dead_symbol.reason);
575 /// }
576 /// \`\`\`
577 pub fn dead_symbols(&self, entry_symbol_id: &str) -> Result<Vec<DeadSymbol>> {
578 let entry_entity = self.resolve_symbol_entity(entry_symbol_id)?;
579 let backend = &self.calls.backend;
580
581 // Get all call graph entities
582 let all_entities = self.all_call_graph_entities()?;
583
584 // Find all entities reachable from the entry point
585 // Note: sqlitegraph 1.3.0 API takes (graph, start)
586 let reachable_ids =
587 algo::reachable_from(get_sqlite_graph(backend)?, entry_entity)?;
588
589 // Dead symbols = all entities - reachable entities
590 let reachable_set: HashSet<i64> = reachable_ids.into_iter().collect();
591 let mut dead_symbols = Vec::new();
592
593 for entity_id in all_entities {
594 // Skip the entry point itself
595 if entity_id == entry_entity {
596 continue;
597 }
598
599 // If not reachable from entry point, it's dead code
600 if !reachable_set.contains(&entity_id) {
601 if let Ok(info) = self.symbol_by_entity_id(entity_id) {
602 dead_symbols.push(DeadSymbol {
603 reason: "unreachable from entry point".to_string(),
604 symbol: info,
605 });
606 }
607 }
608 }
609
610 // Sort deterministically for stable output
611 dead_symbols.sort_by(|a, b| {
612 a.symbol
613 .file_path
614 .cmp(&b.symbol.file_path)
615 .then_with(|| a.symbol.fqn.as_ref().cmp(&b.symbol.fqn.as_ref()))
616 .then_with(|| a.symbol.kind.cmp(&b.symbol.kind))
617 });
618
619 Ok(dead_symbols)
620 }
621
622 /// Detect cycles in the call graph using SCC decomposition
623 ///
624 /// Finds all strongly connected components (SCCs) with more than one member,
625 /// which indicate cycles or mutual recursion in the call graph.
626 ///
627 /// # Graph View
628 ///
629 /// This operates on the **call graph** (CALLS edges only).
630 ///
631 /// # Cycle Detection
632 ///
633 /// Uses Tarjan's SCC algorithm to find strongly connected components.
634 /// Only SCCs with more than one member are reported as cycles (MutualRecursion).
635 /// Single-node SCCs are not cycles (unless they have self-loops).
636 ///
637 /// # Returns
638 /// [`CycleReport`] containing all detected cycles
639 ///
640 /// # Example
641 ///
642 /// \`\`\`no_run
643 /// # use magellan::CodeGraph;
644 /// # let mut graph = CodeGraph::open("test.db").unwrap();
645 /// let report = graph.detect_cycles()?;
646 /// println!("Found {} cycles", report.total_count);
647 /// for cycle in &report.cycles {
648 /// println!("Cycle with {} members:", cycle.members.len());
649 /// for member in &cycle.members {
650 /// println!(" - {}", member.fqn.as_deref().unwrap_or("?"));
651 /// }
652 /// }
653 /// \`\`\`
654 pub fn detect_cycles(&self) -> Result<CycleReport> {
655 let backend = &self.calls.backend;
656
657 // Use sqlitegraph's strongly_connected_components algorithm
658 let scc_result = algo::strongly_connected_components(get_sqlite_graph(backend)?)?;
659
660 // Filter to SCCs with >1 member (mutual recursion)
661 let cycles: Vec<_> = scc_result
662 .components
663 .into_iter()
664 .filter(|scc| scc.len() > 1)
665 .map(|members| {
666 // Convert entity IDs to SymbolInfo
667 let symbol_infos: Vec<_> = members
668 .into_iter()
669 .filter_map(|id| self.symbol_by_entity_id(id).ok())
670 .collect();
671
672 Cycle {
673 members: symbol_infos,
674 kind: CycleKind::MutualRecursion,
675 }
676 })
677 .filter(|cycle| !cycle.members.is_empty())
678 .collect();
679
680 let total_count = cycles.len();
681
682 // Sort cycles deterministically
683 let mut cycles = cycles;
684 cycles.sort_by(|a, b| {
685 match (a.members.first(), b.members.first()) {
686 (Some(am), Some(bm)) => {
687 match (am.fqn.as_ref(), bm.fqn.as_ref()) {
688 (Some(af), Some(bf)) => af.cmp(bf),
689 (Some(_), None) => std::cmp::Ordering::Less,
690 (None, Some(_)) => std::cmp::Ordering::Greater,
691 (None, None) => std::cmp::Ordering::Equal,
692 }
693 }
694 (Some(_), None) => std::cmp::Ordering::Less,
695 (None, Some(_)) => std::cmp::Ordering::Greater,
696 (None, None) => std::cmp::Ordering::Equal,
697 }
698 });
699
700 Ok(CycleReport {
701 cycles,
702 total_count,
703 })
704 }
705
706 /// Find cycles containing a specific symbol
707 ///
708 /// Returns only the cycles that include the specified symbol in their member set.
709 ///
710 /// # Arguments
711 /// * `symbol_id` - Stable symbol ID or FQN to search for
712 ///
713 /// # Returns
714 /// Vector of [`Cycle`] containing the specified symbol
715 ///
716 /// # Example
717 ///
718 /// \`\`\`no_run
719 /// # use magellan::CodeGraph;
720 /// # let mut graph = CodeGraph::open("test.db").unwrap();
721 /// let cycles = graph.find_cycles_containing("problematic_function")?;
722 /// if cycles.is_empty() {
723 /// println!("No cycles found containing this symbol");
724 /// } else {
725 /// println!("Found {} cycles containing this symbol", cycles.len());
726 /// }
727 /// \`\`\`
728 pub fn find_cycles_containing(&self, symbol_id: &str) -> Result<Vec<Cycle>> {
729 let entity_id = self.resolve_symbol_entity(symbol_id)?;
730 let backend = &self.calls.backend;
731
732 // Use sqlitegraph's strongly_connected_components algorithm
733 let scc_result = algo::strongly_connected_components(get_sqlite_graph(backend)?)?;
734
735 // Find which SCC contains this entity
736 let target_component_idx = scc_result.node_to_component.get(&entity_id);
737
738 let target_idx = match target_component_idx {
739 Some(&idx) => idx,
740 None => return Ok(Vec::new()), // Symbol not in any SCC (shouldn't happen)
741 };
742
743 // Check if this SCC is a cycle (has >1 member)
744 let target_component = &scc_result.components[target_idx];
745 if target_component.len() <= 1 {
746 // Single node SCC - not a cycle (unless self-loop, but that's rare)
747 return Ok(Vec::new());
748 }
749
750 // Convert this SCC to a Cycle
751 let symbol_infos: Vec<_> = target_component
752 .iter()
753 .filter_map(|&id| self.symbol_by_entity_id(id).ok())
754 .collect();
755
756 let cycle = Cycle {
757 members: symbol_infos,
758 kind: CycleKind::MutualRecursion,
759 };
760
761 Ok(vec![cycle])
762 }
763
764 /// Condense the call graph by collapsing SCCs into supernodes
765 ///
766 /// Creates a condensation DAG by collapsing each strongly connected component
767 /// into a single "supernode". The resulting graph is always acyclic, making it
768 /// suitable for topological analysis and safe refactoring.
769 ///
770 /// # Graph View
771 ///
772 /// This operates on the **call graph** (CALLS edges only).
773 ///
774 /// # Use Cases
775 ///
776 /// - **Topological Sorting**: Condensation graph is a DAG, enabling topo sort
777 /// - **Mutual Recursion Detection**: Large supernodes indicate tight coupling
778 /// - **Impact Analysis**: Changing one symbol affects its entire SCC
779 ///
780 /// # Returns
781 /// [`CondensationResult`] with the condensed DAG and symbol-to-supernode mapping
782 ///
783 /// # Example
784 ///
785 /// \`\`\`no_run
786 /// # use magellan::CodeGraph;
787 /// # let mut graph = CodeGraph::open("test.db").unwrap();
788 /// let condensed = graph.condense_call_graph()?;
789 ///
790 /// println!("Condensed to {} supernodes", condensed.graph.supernodes.len());
791 /// println!("Condensed graph has {} edges", condensed.graph.edges.len());
792 ///
793 /// // Check which SCC a symbol belongs to
794 /// if let Some(supernode_id) = condensed.original_to_supernode.get("some_symbol_id") {
795 /// println!("Symbol is in SCC {}", supernode_id);
796 /// }
797 /// \`\`\`
798 pub fn condense_call_graph(&self) -> Result<CondensationResult> {
799 let backend = &self.calls.backend;
800
801 // Use sqlitegraph's collapse_sccs algorithm
802 let collapse_result = algo::collapse_sccs(get_sqlite_graph(backend)?)?;
803
804 // Build supernodes with SymbolInfo members
805 let mut supernodes = Vec::new();
806 let mut original_to_supernode = HashMap::new();
807
808 for (&supernode_id, member_ids) in &collapse_result.supernode_members {
809 let symbol_infos: Vec<_> = member_ids
810 .iter()
811 .filter_map(|&id| self.symbol_by_entity_id(id).ok())
812 .collect();
813
814 // Build mapping from symbol_id to supernode
815 for symbol_info in &symbol_infos {
816 if let Some(ref sym_id) = symbol_info.symbol_id {
817 original_to_supernode.insert(sym_id.clone(), supernode_id);
818 }
819 }
820
821 supernodes.push(Supernode {
822 id: supernode_id,
823 members: symbol_infos,
824 });
825 }
826
827 // Sort supernodes deterministically
828 supernodes.sort_by(|a, b| a.id.cmp(&b.id));
829
830 let graph = CondensationGraph {
831 supernodes,
832 edges: collapse_result.supernode_edges,
833 };
834
835 Ok(CondensationResult {
836 graph,
837 original_to_supernode,
838 })
839 }
840
841 /// Compute a backward program slice (what affects this symbol)
842 ///
843 /// Returns all symbols that can affect the target symbol through the call graph.
844 /// This is useful for bug isolation - finding all code that could influence
845 /// a given symbol's behavior.
846 ///
847 /// # Graph View (Call-Graph Fallback)
848 ///
849 /// **Current implementation uses call-graph reachability as a fallback.**
850 /// Full CFG-based program slicing requires control dependence graph (CDG)
851 /// which needs post-dominators and AST CFG integration not yet available.
852 ///
853 /// The fallback implementation uses reverse reachability on the call graph,
854 /// finding all callers that directly or indirectly call this symbol.
855 ///
856 /// # Limitations
857 ///
858 /// - Uses call-graph reachability instead of full CFG-based slicing
859 /// - Does not include data flow dependencies within functions
860 /// - Does not include control flow from conditionals/loops
861 /// - Full slicing will be available when AST CFG edges are integrated
862 ///
863 /// # Arguments
864 /// * `symbol_id` - Stable symbol ID or FQN to slice from
865 ///
866 /// # Returns
867 /// [`SliceResult`] containing the slice and statistics
868 ///
869 /// # Example
870 ///
871 /// \`\`\`no_run
872 /// # use magellan::CodeGraph;
873 /// # let mut graph = CodeGraph::open("test.db").unwrap();
874 /// // Find what affects 'helper_function'
875 /// let slice_result = graph.backward_slice("helper_function")?;
876 /// println!("{} symbols affect this function", slice_result.slice.symbol_count);
877 /// for symbol in &slice_result.slice.included_symbols {
878 /// println!(" - {}", symbol.fqn.as_deref().unwrap_or("?"));
879 /// }
880 /// \`\`\`
881 pub fn backward_slice(&self, symbol_id: &str) -> Result<SliceResult> {
882 let entity_id = self.resolve_symbol_entity(symbol_id)?;
883 let backend = &self.calls.backend;
884
885 // Get target symbol info
886 let target = self.symbol_by_entity_id(entity_id)?;
887
888 // Fallback: Use reverse reachable on call graph
889 // This finds all callers that directly or indirectly call this symbol
890 let caller_entity_ids = algo::reverse_reachable_from(get_sqlite_graph(backend)?, entity_id)?;
891
892 // Convert entity IDs to SymbolInfo
893 let mut included_symbols = Vec::new();
894 for id in caller_entity_ids {
895 // Skip the starting symbol itself
896 if id == entity_id {
897 continue;
898 }
899
900 if let Ok(info) = self.symbol_by_entity_id(id) {
901 included_symbols.push(info);
902 }
903 }
904
905 // Sort deterministically
906 included_symbols.sort_by(|a, b| {
907 a.file_path
908 .cmp(&b.file_path)
909 .then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
910 .then_with(|| a.kind.cmp(&b.kind))
911 });
912
913 let symbol_count = included_symbols.len();
914
915 Ok(SliceResult {
916 slice: ProgramSlice {
917 target,
918 direction: SliceDirection::Backward,
919 included_symbols,
920 symbol_count,
921 },
922 statistics: SliceStatistics {
923 total_symbols: symbol_count,
924 data_dependencies: 0, // Not available in call-graph fallback
925 control_dependencies: symbol_count,
926 },
927 })
928 }
929
930 /// Compute a forward program slice (what this symbol affects)
931 ///
932 /// Returns all symbols that the target symbol can affect through the call graph.
933 /// This is useful for refactoring safety - finding all code that could be
934 /// impacted by changes to this symbol.
935 ///
936 /// # Graph View (Call-Graph Fallback)
937 ///
938 /// **Current implementation uses call-graph reachability as a fallback.**
939 /// Full CFG-based program slicing requires control dependence graph (CDG)
940 /// which needs post-dominators and AST CFG integration not yet available.
941 ///
942 /// The fallback implementation uses forward reachability on the call graph,
943 /// finding all callees that this symbol directly or indirectly calls.
944 ///
945 /// # Limitations
946 ///
947 /// - Uses call-graph reachability instead of full CFG-based slicing
948 /// - Does not include data flow dependencies within functions
949 /// - Does not include control flow from conditionals/loops
950 /// - Full slicing will be available when AST CFG edges are integrated
951 ///
952 /// # Arguments
953 /// * `symbol_id` - Stable symbol ID or FQN to slice from
954 ///
955 /// # Returns
956 /// [`SliceResult`] containing the slice and statistics
957 ///
958 /// # Example
959 ///
960 /// \`\`\`no_run
961 /// # use magellan::CodeGraph;
962 /// # let mut graph = CodeGraph::open("test.db").unwrap();
963 /// // Find what 'main_function' affects
964 /// let slice_result = graph.forward_slice("main_function")?;
965 /// println!("{} symbols are affected by this function", slice_result.slice.symbol_count);
966 /// for symbol in &slice_result.slice.included_symbols {
967 /// println!(" - {}", symbol.fqn.as_deref().unwrap_or("?"));
968 /// }
969 /// \`\`\`
970 pub fn forward_slice(&self, symbol_id: &str) -> Result<SliceResult> {
971 let entity_id = self.resolve_symbol_entity(symbol_id)?;
972 let backend = &self.calls.backend;
973
974 // Get target symbol info
975 let target = self.symbol_by_entity_id(entity_id)?;
976
977 // Fallback: Use forward reachable on call graph
978 // This finds all callees that this symbol directly or indirectly calls
979 let callee_entity_ids = algo::reachable_from(get_sqlite_graph(backend)?, entity_id)?;
980
981 // Convert entity IDs to SymbolInfo
982 let mut included_symbols = Vec::new();
983 for id in callee_entity_ids {
984 // Skip the starting symbol itself
985 if id == entity_id {
986 continue;
987 }
988
989 if let Ok(info) = self.symbol_by_entity_id(id) {
990 included_symbols.push(info);
991 }
992 }
993
994 // Sort deterministically
995 included_symbols.sort_by(|a, b| {
996 a.file_path
997 .cmp(&b.file_path)
998 .then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
999 .then_with(|| a.kind.cmp(&b.kind))
1000 });
1001
1002 let symbol_count = included_symbols.len();
1003
1004 Ok(SliceResult {
1005 slice: ProgramSlice {
1006 target,
1007 direction: SliceDirection::Forward,
1008 included_symbols,
1009 symbol_count,
1010 },
1011 statistics: SliceStatistics {
1012 total_symbols: symbol_count,
1013 data_dependencies: 0, // Not available in call-graph fallback
1014 control_dependencies: symbol_count,
1015 },
1016 })
1017 }
1018
1019 /// Enumerate execution paths from a starting symbol
1020 ///
1021 /// Finds all execution paths from `start_symbol_id` to `end_symbol_id` (if provided)
1022 /// or all paths starting from `start_symbol_id` (if end_symbol_id is None).
1023 ///
1024 /// Path enumeration uses bounded DFS to prevent infinite traversal in cyclic graphs:
1025 /// - `max_depth`: Maximum path length (number of edges)
1026 /// - `max_paths`: Maximum number of paths to return
1027 /// - `revisit_cap`: Maximum number of times a single node can be revisited (prevents infinite loops)
1028 ///
1029 /// # Arguments
1030 ///
1031 /// * `start_symbol_id` - Starting symbol ID or FQN
1032 /// * `end_symbol_id` - Optional ending symbol ID or FQN (if None, enumerates all paths from start)
1033 /// * `max_depth` - Maximum path depth (default: 100)
1034 /// * `max_paths` - Maximum number of paths to return (default: 1000)
1035 ///
1036 /// # Returns
1037 ///
1038 /// Returns a [`PathEnumerationResult`] containing:
1039 /// - All discovered paths
1040 /// - Whether enumeration hit bounds
1041 /// - Statistics about path lengths and unique symbols
1042 ///
1043 /// # Example
1044 ///
1045 /// \`\`\`no_run
1046 /// # use magellan::CodeGraph;
1047 /// # fn main() -> anyhow::Result<()> {
1048 /// let mut graph = CodeGraph::open("codegraph.db")?;
1049 ///
1050 /// // Find all paths from main to any leaf function
1051 /// let result = graph.enumerate_paths("main", None, 50, 100)?;
1052 ///
1053 /// println!("Found {} paths", result.total_enumerated);
1054 /// println!("Average length: {:.2}", result.statistics.avg_length);
1055 /// for (i, path) in result.paths.iter().enumerate() {
1056 /// println!("Path {}: {:?}", path.symbols.iter().map(|s| s.fqn.as_deref().unwrap_or("?")).collect::<Vec<_>>());
1057 /// }
1058 /// # Ok(())
1059 /// # }
1060 /// \`\`\`
1061 pub fn enumerate_paths(
1062 &self,
1063 start_symbol_id: &str,
1064 end_symbol_id: Option<&str>,
1065 max_depth: usize,
1066 max_paths: usize,
1067 ) -> Result<PathEnumerationResult> {
1068 let start_entity_id = self.resolve_symbol_entity(start_symbol_id)?;
1069 let backend = &self.calls.backend;
1070 let graph = get_sqlite_graph(backend)?;
1071
1072 // Build exit_nodes set for target symbol
1073 let exit_nodes: Option<AHashSet<i64>> = if let Some(end_id) = end_symbol_id {
1074 let end_entity_id = self.resolve_symbol_entity(end_id)?;
1075 let mut set = AHashSet::new();
1076 set.insert(end_entity_id);
1077 Some(set)
1078 } else {
1079 None
1080 };
1081
1082 // Use sqlitegraph's path enumeration with bounds
1083 let config = algo::PathEnumerationConfig {
1084 max_depth,
1085 max_paths,
1086 revisit_cap: 100, // Prevent infinite loops in cyclic graphs
1087 exit_nodes,
1088 error_nodes: None,
1089 };
1090
1091 let sqlite_result = algo::enumerate_paths(graph, start_entity_id, &config)?;
1092
1093 // Convert sqlitegraph's path result to our format
1094 let mut paths = Vec::new();
1095 let mut all_symbols = HashSet::new();
1096 let mut min_length = usize::MAX;
1097 let mut max_length = 0;
1098 let mut total_length = 0;
1099
1100 for path in sqlite_result.paths {
1101 let mut symbols = Vec::new();
1102
1103 for entity_id in &path.nodes {
1104 if let Ok(info) = self.symbol_by_entity_id(*entity_id) {
1105 all_symbols.insert(info.symbol_id.clone().unwrap_or_default());
1106 symbols.push(info);
1107 }
1108 }
1109
1110 let length = symbols.len();
1111 if length > 0 {
1112 min_length = min_length.min(length);
1113 max_length = max_length.max(length);
1114 total_length += length;
1115
1116 paths.push(ExecutionPath {
1117 symbols,
1118 length,
1119 });
1120 }
1121 }
1122
1123 // Sort paths: first by starting symbol FQN, then by length
1124 paths.sort_by(|a, b| {
1125 match (
1126 a.symbols.first().and_then(|s| s.fqn.as_ref()),
1127 b.symbols.first().and_then(|s| s.fqn.as_ref()),
1128 ) {
1129 (Some(a_fqn), Some(b_fqn)) => {
1130 a_fqn
1131 .cmp(b_fqn)
1132 .then_with(|| a.length.cmp(&b.length))
1133 }
1134 (Some(_), None) => std::cmp::Ordering::Less,
1135 (None, Some(_)) => std::cmp::Ordering::Greater,
1136 (None, None) => a.length.cmp(&b.length),
1137 }
1138 });
1139
1140 let avg_length = if paths.is_empty() {
1141 0.0
1142 } else {
1143 total_length as f64 / paths.len() as f64
1144 };
1145
1146 // Determine if we hit bounds
1147 let bounded_hit = sqlite_result.paths_pruned_by_bounds > 0;
1148
1149 Ok(PathEnumerationResult {
1150 paths,
1151 total_enumerated: sqlite_result.total_paths_found,
1152 bounded_hit,
1153 statistics: PathStatistics {
1154 avg_length,
1155 min_length: if min_length == usize::MAX {
1156 0
1157 } else {
1158 min_length
1159 },
1160 max_length,
1161 unique_symbols: all_symbols.len(),
1162 },
1163 })
1164 }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170 use crate::CodeGraph;
1171
1172 /// Test helper to create a simple call graph for testing
1173 ///
1174 /// Creates:
1175 /// - main() -> helper_a() -> leaf()
1176 /// - main() -> helper_b() -> leaf()
1177 /// - unused_function() -> leaf()
1178 ///
1179 /// Returns the CodeGraph and symbol IDs for main and unused_function
1180 fn create_test_graph() -> Result<(CodeGraph, String, String)> {
1181 // Use a persistent temp directory that won't be deleted
1182 // This is necessary for V3 backend which needs files to remain accessible
1183 let temp_dir = std::env::temp_dir().join(format!("magellan_test_{}", std::process::id()));
1184 std::fs::create_dir_all(&temp_dir)?;
1185 let db_path = temp_dir.join("test.db");
1186
1187 let source = r#"
1188fn main() {
1189 helper_a();
1190 helper_b();
1191}
1192
1193fn helper_a() {
1194 leaf();
1195}
1196
1197fn helper_b() {
1198 leaf();
1199}
1200
1201fn leaf() {
1202 println!("leaf");
1203}
1204
1205fn unused_function() {
1206 leaf();
1207}
1208"#;
1209
1210 let mut graph = CodeGraph::open(&db_path)?;
1211 // Index the file - use a temporary file path
1212 let test_file = temp_dir.join("test.rs");
1213 std::fs::write(&test_file, source)?;
1214 let path_str = test_file.to_string_lossy().to_string();
1215 let source_bytes = std::fs::read(&test_file)?;
1216
1217 // Index symbols and calls
1218 graph.index_file(&path_str, &source_bytes)?;
1219 graph.index_calls(&path_str, &source_bytes)?;
1220
1221 // Find the symbol IDs for main and unused_function
1222 let symbols = crate::graph::query::symbols_in_file(&mut graph, &path_str)?;
1223 let main_id = symbols
1224 .iter()
1225 .find(|s| s.name.as_deref() == Some("main"))
1226 .and_then(|s| s.fqn.clone())
1227 .unwrap_or_default();
1228
1229 let unused_id = symbols
1230 .iter()
1231 .find(|s| s.name.as_deref() == Some("unused_function"))
1232 .and_then(|s| s.fqn.clone())
1233 .unwrap_or_default();
1234
1235 // For testing, use the symbol's FQN directly
1236 // In a real scenario with proper SymbolId generation, we'd use that
1237 Ok((graph, main_id, unused_id))
1238 }
1239
1240 #[test]
1241 fn test_resolve_symbol_entity_not_found() {
1242 let (graph, _, _) = create_test_graph().unwrap();
1243 let result = graph.resolve_symbol_entity("nonexistent_id_123456789012");
1244 assert!(result.is_err());
1245 }
1246
1247 #[test]
1248 fn test_symbol_by_entity_id() {
1249 let (graph, _, _) = create_test_graph().unwrap();
1250
1251 // Get all entity IDs
1252 let entity_ids = graph.calls.backend.entity_ids().unwrap();
1253 let snapshot = SnapshotId::current();
1254
1255 // Find a Symbol entity
1256 for entity_id in entity_ids {
1257 if let Ok(node) = graph.calls.backend.get_node(snapshot, entity_id) {
1258 if node.kind == "Symbol" {
1259 let info = graph.symbol_by_entity_id(entity_id);
1260 assert!(info.is_ok());
1261 let symbol_info = info.unwrap();
1262 assert!(!symbol_info.file_path.is_empty());
1263 assert!(!symbol_info.kind.is_empty());
1264 return;
1265 }
1266 }
1267 }
1268
1269 panic!("No Symbol entity found in test graph");
1270 }
1271
1272 #[test]
1273 fn test_reachable_symbols_basic() {
1274 let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
1275
1276 // Get all symbols and verify we can query them
1277 let entity_ids = graph.calls.backend.entity_ids().unwrap();
1278 eprintln!("Total entities: {}", entity_ids.len());
1279 let snapshot = SnapshotId::current();
1280 let mut found_symbols = 0;
1281
1282 for entity_id in &entity_ids {
1283 match graph.calls.backend.get_node(snapshot, *entity_id) {
1284 Ok(node) => {
1285 eprintln!(" Entity {}: kind={}", entity_id, node.kind);
1286 if node.kind == "Symbol" {
1287 found_symbols += 1;
1288 }
1289 }
1290 Err(e) => {
1291 eprintln!(" Entity {}: ERROR getting node: {:?}", entity_id, e);
1292 }
1293 }
1294 }
1295
1296 // We should have found at least some symbols
1297 assert!(found_symbols > 0, "Should find Symbol entities in test graph, got {} symbols from {} entities", found_symbols, entity_ids.len());
1298 }
1299
1300 #[test]
1301 fn test_reachable_symbols_max_depth() {
1302 let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
1303
1304 // Get the main function's entity ID
1305 let snapshot = SnapshotId::current();
1306 let entity_ids = graph.calls.backend.entity_ids().unwrap();
1307
1308 let main_entity_id = entity_ids
1309 .into_iter()
1310 .find(|&id| {
1311 if let Ok(node) = graph.calls.backend.get_node(snapshot, id) {
1312 if let Ok(data) = serde_json::from_value::<serde_json::Value>(node.data) {
1313 if let Some(name) = data.get("name").and_then(|v| v.as_str()) {
1314 return name == "main";
1315 }
1316 }
1317 }
1318 false
1319 });
1320
1321 if let Some(entity_id) = main_entity_id {
1322 // Verify we can get the node
1323 let result = graph.calls.backend.get_node(snapshot, entity_id);
1324 assert!(result.is_ok(), "Should be able to get main node");
1325 }
1326 }
1327
1328 #[test]
1329 fn test_dead_symbols() {
1330 let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
1331
1332 // Get all entity IDs
1333 let entity_ids = graph.calls.backend.entity_ids().unwrap();
1334
1335 // We should have some entities in the call graph
1336 assert!(entity_ids.len() > 0, "Should have call graph entities");
1337 }
1338
1339 #[test]
1340 fn test_reverse_reachable_symbols() {
1341 let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
1342
1343 // Get all entity IDs
1344 let entity_ids = graph.calls.backend.entity_ids().unwrap();
1345
1346 // We should have some entities in the call graph
1347 assert!(entity_ids.len() > 0, "Should have call graph entities");
1348 }
1349}