/**
* Call graph parsing and formatting utilities
*/
interface Edge {
from: string;
to: string;
}
interface Graph {
edges?: Edge[];
}
interface HierarchicalGraphNode {
edges?: Edge[];
children?: HierarchicalGraphNode[];
}
interface JumpInfo {
def?: string;
decl?: string;
}
interface SymbolMeta {
parentsym?: string;
}
interface SymbolInfo {
pretty?: string;
sym?: string;
jumps?: JumpInfo;
meta?: SymbolMeta;
}
interface Jumprefs {
[key: string]: SymbolInfo;
}
interface CallGraphData {
graphs?: Graph[];
hierarchicalGraphs?: HierarchicalGraphNode[];
jumprefs?: Jumprefs;
}
interface CallInfo {
prettyName: string;
mangled: string;
location: string;
declLocation?: string;
defLocation?: string;
}
/**
* Collect all edges from hierarchical graph (for calls-between)
*/
function collectEdgesFromHierarchical(
node: HierarchicalGraphNode,
edges: Edge[]
): void {
if (node.edges) {
edges.push(...node.edges);
}
if (node.children) {
for (const child of node.children) {
collectEdgesFromHierarchical(child, edges);
}
}
}
/**
* Format location string with optional declaration
*/
function formatLocation(defLocation?: string, declLocation?: string): string {
if (defLocation && declLocation && defLocation !== declLocation) {
return `${defLocation} (decl: ${declLocation})`;
}
return defLocation || declLocation || "";
}
/**
* Group overloaded functions by pretty name
*/
function groupOverloads(
callInfos: CallInfo[]
): Array<{ prettyName: string; overloads: CallInfo[] }> {
const grouped: Array<{ prettyName: string; overloads: CallInfo[] }> = [];
for (const info of callInfos) {
const last = grouped[grouped.length - 1];
if (last && last.prettyName === info.prettyName) {
last.overloads.push(info);
} else {
grouped.push({
prettyName: info.prettyName,
overloads: [info],
});
}
}
return grouped;
}
/**
* Parse calls-between graph data
*/
export function parseCallsBetween(data: CallGraphData): {
calls: Array<{ from: CallInfo; to: CallInfo }>;
isEmpty: boolean;
} {
const calls: Array<{ from: CallInfo; to: CallInfo }> = [];
if (!data.hierarchicalGraphs || data.hierarchicalGraphs.length === 0) {
return { calls, isEmpty: true };
}
const allEdges: Edge[] = [];
for (const graph of data.hierarchicalGraphs) {
collectEdgesFromHierarchical(graph, allEdges);
}
if (allEdges.length === 0) {
return { calls, isEmpty: true };
}
const jumprefs = data.jumprefs || {};
for (const edge of allEdges) {
const fromInfo = jumprefs[edge.from];
const toInfo = jumprefs[edge.to];
if (!fromInfo || !toInfo) {
continue;
}
const fromDefLocation = fromInfo.jumps?.def || "";
const fromDeclLocation = fromInfo.jumps?.decl || "";
const toDefLocation = toInfo.jumps?.def || "";
const toDeclLocation = toInfo.jumps?.decl || "";
calls.push({
from: {
prettyName: fromInfo.pretty || edge.from,
mangled: fromInfo.sym || edge.from,
location: formatLocation(fromDefLocation, fromDeclLocation),
defLocation: fromDefLocation,
declLocation: fromDeclLocation,
},
to: {
prettyName: toInfo.pretty || edge.to,
mangled: toInfo.sym || edge.to,
location: formatLocation(toDefLocation, toDeclLocation),
defLocation: toDefLocation,
declLocation: toDeclLocation,
},
});
}
return { calls, isEmpty: calls.length === 0 };
}
/**
* Parse calls-from or calls-to graph data
*/
export function parseCallGraph(
data: CallGraphData,
isCallsTo: boolean
): Map<string, CallInfo[]> {
const callInfos: CallInfo[] = [];
const jumprefs = data.jumprefs || {};
if (!data.graphs || data.graphs.length === 0) {
return new Map();
}
for (const graph of data.graphs) {
if (!graph.edges) {
continue;
}
for (const edge of graph.edges) {
// For calls-to, we want functions that call the target (edge.from)
// For calls-from, we want functions called by the source (edge.to)
const targetSym = isCallsTo ? edge.from : edge.to;
const symbolInfo = jumprefs[targetSym];
if (!symbolInfo) {
continue;
}
const prettyName = symbolInfo.pretty || "";
const mangled = symbolInfo.sym || targetSym;
const defLocation = symbolInfo.jumps?.def || "";
const declLocation = symbolInfo.jumps?.decl || "";
const location = formatLocation(defLocation, declLocation);
if (prettyName && location) {
callInfos.push({
prettyName,
mangled,
location,
defLocation,
declLocation,
});
}
}
}
// Group by parent symbol
const parentSymMap = new Map<string, CallInfo[]>();
for (const info of callInfos) {
const targetSym = isCallsTo
? Object.keys(jumprefs).find(
(k) => jumprefs[k].pretty === info.prettyName
)
: null;
const symbolInfo = targetSym ? jumprefs[targetSym] : null;
const parentSym = symbolInfo?.meta?.parentsym || "Free functions";
const parentClean = parentSym.startsWith("T_")
? parentSym.substring(2)
: parentSym;
if (!parentSymMap.has(parentClean)) {
parentSymMap.set(parentClean, []);
}
parentSymMap.get(parentClean)!.push(info);
}
return parentSymMap;
}
/**
* Format call graph results as markdown
*/
export function formatCallGraphMarkdown(
queryText: string,
data: CallGraphData
): string {
let output = `# ${queryText}\n\n`;
const isCallsBetween = queryText.includes("calls-between");
const isCallsTo = queryText.includes("calls-to");
if (isCallsBetween) {
const { calls, isEmpty } = parseCallsBetween(data);
if (isEmpty) {
output += "No direct calls found between source and target.\n";
return output;
}
output += "## Direct calls from source to target\n\n";
for (const { from, to } of calls) {
output += `- **${from.prettyName}** (${from.location}) calls **${to.prettyName}** (${to.location})\n`;
output += ` - From: \`${from.mangled}\`\n`;
output += ` - To: \`${to.mangled}\`\n`;
}
return output;
}
// Parse calls-from or calls-to
const parentSymMap = parseCallGraph(data, isCallsTo);
if (parentSymMap.size === 0) {
output += "No calls found.\n";
return output;
}
// Sort parent symbols for consistent output
const sortedParents = Array.from(parentSymMap.entries()).sort((a, b) =>
a[0].localeCompare(b[0])
);
for (const [parentSym, callInfos] of sortedParents) {
output += `## ${parentSym}\n\n`;
// Group overloaded functions
const grouped = groupOverloads(callInfos);
for (const { prettyName, overloads } of grouped) {
if (overloads.length === 1) {
const info = overloads[0];
output += `- ${prettyName} (\`${info.mangled}\`, ${info.location})\n`;
} else {
output += `- ${prettyName} (${overloads.length} overloads)\n`;
for (const info of overloads) {
output += ` - \`${info.mangled}\`, ${info.location}\n`;
}
}
}
output += "\n";
}
return output;
}