How Code-Review-Graph Handles Circular Dependencies in the Call Hierarchy
TLDR: The graph stores circular dependencies as standard directed edges in SQLite and prevents infinite loops during traversal by using a recursive Common Table Expression (CTE) that tracks visited nodes, ensuring finite results for impact analysis and caller/callee queries.
The tirth8205/code-review-graph extension analyzes code relationships by building a directed graph of function calls in a local database. When developers need to understand how circular dependencies in the call hierarchy affect their codebase, the tool must safely traverse these cycles without entering infinite recursion. The implementation achieves this through database-level cycle detection and graph-walking algorithms that respect a visited set.
Storing CALLS Relationships as Directed Edges
In src/backend/sqlite.ts, the graph persists CALLS relationships as directed edges between nodes. When function A invokes function B, the system creates an edge A → B in the SQLite database. Circular dependencies—such as A → B → A—are treated as normal edges that happen to form a cycle, requiring no special schema modifications or edge-case handling at the storage layer.
Cycle-Safe Traversal with Recursive CTEs
The traversal logic prevents infinite recursion by maintaining a visited set during graph walks. When the extension queries for callers or callees, the underlying SQLite query uses a recursive Common Table Expression (CTE) that includes a WHERE node NOT IN visited clause. This ensures that once a node has been processed, it is skipped on subsequent encounters, automatically terminating the traversal when a cycle would be revisited.
Implementation in the SQLite Backend
According to the source code in src/backend/sqlite.ts, the recursive CTE implements the cycle-detection logic at the database level. This approach guarantees that queries such as callers_of and callees_of always terminate regardless of how many circular dependencies exist in the call graph. The CTE returns every reachable node exactly once, even when the underlying structure contains complex interdependent cycles.
Practical Code Examples for Caller and Callee Analysis
The VS Code extension commands defined in src/extension.ts and the navigation interface in src/features/navigation.ts leverage this cycle-safe backend when displaying results. Both use the same edge-filtering logic to ensure consistent behavior across the UI.
Finding Callers with Automatic Cycle Handling
The "Find callers" command filters edges by the CALLS relationship and maps each result to its source node. Because sqliteReader.getNode() relies on the recursive CTE with visited-set tracking, the operation completes safely even when the caller chain loops back on itself:
// In src/extension.ts – "Find callers" command
const callerEdges = edges.filter(e => e.kind === "CALLS");
if (callerEdges.length === 0) {
vscode.window.showInformationMessage(
`Code Graph: No callers found for "${qualifiedName}".`
);
return;
}
// Build QuickPick items, each call resolves the full node
const items = callerEdges.map(e => {
const callerNode = sqliteReader!.getNode(e.sourceQualified);
return {
label: callerNode?.name ?? e.sourceQualified,
description: callerNode?.filePath ?? e.filePath,
detail: `Line ${callerNode?.lineStart ?? e.line}`,
node: callerNode,
};
});
Finding Callees Safely
Similarly, the "Find callees" command processes outgoing edges while the backend safeguards against infinite recursion:
// In src/extension.ts – "Find callees" command
const calleeEdges = edges.filter(e => e.kind === "CALLS");
if (calleeEdges.length === 0) {
vscode.window.showInformationMessage(
`Code Graph: No callees found for "${qualifiedName}".`
);
return;
}
const items = calleeEdges.map(e => {
const calleeNode = sqliteReader!.getNode(e.targetQualified);
return {
label: calleeNode?.name ?? e.targetQualified,
description: calleeNode?.filePath ?? e.filePath,
detail: `Line ${calleeNode?.lineStart ?? e.line}`,
node: calleeNode,
};
});
Both commands ultimately invoke sqliteReader.getNode(), which executes the recursive CTE that enforces the visited list constraint.
Impact Analysis and Blast Radius Calculations
The blast-radius calculation—used to determine how changes propagate through the codebase—walks the call graph using the same cycle-aware logic implemented in src/backend/sqlite.ts. Because the traversal skips already-visited nodes, a circular dependency contributes only once to the impact set rather than triggering repeated analysis of the same functions. This design ensures accurate impact analysis without inflating results due to cyclic relationships.
The visualization component in src/webview/graph.ts renders these cycles as standard edges. While the UI displays the raw graph structure, it relies on the backend query logic to prevent infinite loops during data retrieval.
Summary
- Circular dependencies are stored as normal directed edges in the SQLite database without requiring special schema handling.
- Recursive CTEs in
src/backend/sqlite.tstrack avisitedset to prevent infinite loops during graph traversal. - The VS Code extension commands in
src/extension.tsautomatically benefit from cycle-safe queries when finding callers and callees. - Blast-radius calculations correctly count cyclic dependencies only once, ensuring finite and accurate impact analysis.
Frequently Asked Questions
What happens if two functions call each other recursively?
When function A calls B and B calls A, the graph stores two directed edges (A → B and B → A). During traversal, the recursive CTE tracks visited nodes and skips B when it would be revisited from A, preventing infinite recursion while still returning both functions in the result set.
Does the graph visualization show circular dependencies differently?
According to src/webview/graph.ts, the visualization component renders cycles as standard edges without special styling or filtering. The UI displays the raw graph structure while relying on the backend query logic to prevent infinite loops during data retrieval.
Can the recursive CTE handle deeply nested circular chains?
Yes, the recursive CTE handles arbitrarily deep circular chains because it tracks the complete visited set across all recursion levels. Whether the cycle involves two functions or twenty, the WHERE node NOT IN visited clause ensures termination after each node is processed exactly once.
Is there a performance penalty for cycle detection?
The cycle detection adds minimal overhead because it leverages SQLite's built-in recursive CTE optimization. The visited set check operates as a simple exclusion filter within the database engine, making it more efficient than application-level traversal that would require multiple round-trips between the extension and the SQLite backend.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →