From 30ff0ec0a374de1855f4ae49583453a059d3dfdc Mon Sep 17 00:00:00 2001 From: Esorat Date: Sat, 2 Nov 2024 12:45:49 +0700 Subject: [PATCH] fix: Update Callgraph and dumpCallgraph - Add needed comments --- src/internals/ir/callGraph.ts | 151 +++++++++++++++++++++++++--------- src/tools/dumpCallgraph.ts | 67 ++++++++++++--- 2 files changed, 168 insertions(+), 50 deletions(-) diff --git a/src/internals/ir/callGraph.ts b/src/internals/ir/callGraph.ts index 64d15085..2262bb3a 100644 --- a/src/internals/ir/callGraph.ts +++ b/src/internals/ir/callGraph.ts @@ -11,11 +11,21 @@ import { AstMethodCall, } from "@tact-lang/compiler/dist/grammar/ast"; +// Define types for node and edge identifiers export type CGNodeId = number; export type CGEdgeId = number; +/** + * Represents an edge in the call graph, indicating a call from one function to another. + */ export class CGEdge { public idx: CGEdgeId; + + /** + * Creates a new edge in the call graph. + * @param src The source node ID (caller function). + * @param dst The destination node ID (callee function). + */ constructor( public src: CGNodeId, public dst: CGNodeId, @@ -24,10 +34,19 @@ export class CGEdge { } } +/** + * Represents a node in the call graph, corresponding to a function or method. + */ export class CGNode { public idx: CGNodeId; public inEdges: Set = new Set(); public outEdges: Set = new Set(); + + /** + * Creates a new node in the call graph. + * @param astId The AST node ID associated with this function, if any. + * @param name The name of the function or method. + */ constructor( public astId: number | undefined, public name: string, @@ -36,25 +55,75 @@ export class CGNode { } } +/** + * Represents call graph, managing nodes (functions) and edges (calls). + */ export class CallGraph { private nodeMap: Map = new Map(); private edgesMap: Map = new Map(); private nameToNodeId: Map = new Map(); + /** + * Retrieves all nodes in the call graph. + * @returns A map of node IDs to `CGNode` instances. + */ public getNodes(): Map { return this.nodeMap; } + /** + * Retrieves all edges in the call graph. + * @returns A map of edge IDs to `CGEdge` instances. + */ public getEdges(): Map { return this.edgesMap; } - build(astStore: TactASTStore): CallGraph { + /** + * Builds call graph from the provided AST store. + * @param astStore The AST store containing function definitions. + * @returns The constructed `CallGraph` instance. + */ + public build(astStore: TactASTStore): CallGraph { this.addFunctionsToNodes(astStore); this.analyzeFunctionCalls(astStore); return this; } + /** + * Determines whether there is a path from the source node to the destination node. + * @param src The source node ID. + * @param dst The destination node ID. + * @returns `true` if a path exists, `false` otherwise. + */ + public areConnected(src: CGNodeId, dst: CGNodeId): boolean { + const srcNode = this.nodeMap.get(src); + const dstNode = this.nodeMap.get(dst); + if (!srcNode || !dstNode) { + return false; + } + const queue: CGNodeId[] = [src]; + const visited = new Set([src]); + while (queue.length > 0) { + const current = queue.shift()!; + if (current === dst) { + return true; + } + const currentNode = this.nodeMap.get(current); + if (currentNode) { + for (const edgeId of currentNode.outEdges) { + const edge = this.edgesMap.get(edgeId); + if (edge && !visited.has(edge.dst)) { + visited.add(edge.dst); + queue.push(edge.dst); + } + } + } + } + return false; + } + + // Adds functions from the AST store to the call graph as nodes. private addFunctionsToNodes(astStore: TactASTStore) { for (const func of astStore.getFunctions()) { const funcName = this.getFunctionName(func); @@ -62,38 +131,52 @@ export class CallGraph { const node = new CGNode(func.id, funcName); this.nodeMap.set(node.idx, node); this.nameToNodeId.set(funcName, node.idx); + } else { + console.warn( + `Function with id ${func.id} has no name and will be skipped.`, + ); } } } + // Analyzes function bodies to identify function calls and adds edges accordingly. private analyzeFunctionCalls(astStore: TactASTStore) { for (const func of astStore.getFunctions()) { - const callerId = this.getNodeIdByName(this.getFunctionName(func)); - if (callerId !== undefined) { - this.processFunctionBody(func, callerId); + const funcName = this.getFunctionName(func); + if (funcName) { + const callerId = this.getNodeIdByName(funcName); + if (callerId !== undefined) { + this.processFunctionBody(func, callerId); + } else { + console.warn(`Caller function ${funcName} not found in node map.`); + } } } } + // Retrieves function name based on its kind. private getFunctionName( func: AstFunctionDef | AstReceiver | AstContractInit, - ): string { + ): string | undefined { switch (func.kind) { case "function_def": - return func.name.text; + return func.name?.text; case "contract_init": return `contract_init_${func.id}`; case "receiver": return `receiver_${func.id}`; default: unreachable(func); + return undefined; } } + // Gets node ID associated with a given function name. private getNodeIdByName(name: string): CGNodeId | undefined { return this.nameToNodeId.get(name); } + // Processes body of a function to find function calls. private processFunctionBody( func: AstFunctionDef | AstReceiver | AstContractInit, callerId: CGNodeId, @@ -103,20 +186,34 @@ export class CallGraph { }); } + // Processes an expression to identify function calls and adds edges. private processExpression(expr: AstExpression, callerId: CGNodeId) { if (expr.kind === "static_call") { const staticCall = expr as AstStaticCall; - const functionName = staticCall.function.text; - const calleeId = this.findOrAddFunction(functionName); - this.addEdge(callerId, calleeId); + const functionName = staticCall.function?.text; + if (functionName) { + const calleeId = this.findOrAddFunction(functionName); + this.addEdge(callerId, calleeId); + } else { + console.warn( + `Static call expression missing function name at caller ${callerId}`, + ); + } } else if (expr.kind === "method_call") { const methodCall = expr as AstMethodCall; - const methodName = methodCall.method.text; - const calleeId = this.findOrAddFunction(methodName); - this.addEdge(callerId, calleeId); + const methodName = methodCall.method?.text; + if (methodName) { + const calleeId = this.findOrAddFunction(methodName); + this.addEdge(callerId, calleeId); + } else { + console.warn( + `Method call expression missing method name at caller ${callerId}`, + ); + } } } + // Finds an existing function node by name or adds a new one if it doesn't exist. private findOrAddFunction(name: string): CGNodeId { const nodeId = this.nameToNodeId.get(name); if (nodeId !== undefined) { @@ -129,6 +226,7 @@ export class CallGraph { return newNode.idx; } + // Adds an edge between two nodes in the call graph. private addEdge(src: CGNodeId, dst: CGNodeId) { const srcNode = this.nodeMap.get(src); const dstNode = this.nodeMap.get(dst); @@ -137,33 +235,8 @@ export class CallGraph { this.edgesMap.set(edge.idx, edge); srcNode.outEdges.add(edge.idx); dstNode.inEdges.add(edge.idx); + } else { + console.warn(`Cannot add edge from ${src} to ${dst}: node(s) not found.`); } } - - areConnected(src: CGNodeId, dst: CGNodeId): boolean { - const srcNode = this.nodeMap.get(src); - const dstNode = this.nodeMap.get(dst); - if (!srcNode || !dstNode) { - return false; - } - const queue: CGNodeId[] = [src]; - const visited = new Set([src]); - while (queue.length > 0) { - const current = queue.shift()!; - if (current === dst) { - return true; - } - const currentNode = this.nodeMap.get(current); - if (currentNode) { - for (const edgeId of currentNode.outEdges) { - const edge = this.edgesMap.get(edgeId); - if (edge && !visited.has(edge.dst)) { - visited.add(edge.dst); - queue.push(edge.dst); - } - } - } - } - return false; - } } diff --git a/src/tools/dumpCallgraph.ts b/src/tools/dumpCallgraph.ts index 3f63ad75..e051e9ae 100644 --- a/src/tools/dumpCallgraph.ts +++ b/src/tools/dumpCallgraph.ts @@ -15,7 +15,7 @@ import JSONbig from "json-bigint"; import * as path from "path"; /** - * Defines the options for the DumpCallGraph tool. + * Defines options for the `DumpCallGraph` tool. */ interface DumpCallGraphOptions extends Record { formats: Array<"dot" | "json" | "mmd">; @@ -26,11 +26,19 @@ interface DumpCallGraphOptions extends Record { * A tool that dumps the Call Graph (CG) of the given compilation unit in multiple formats. */ export class DumpCallGraph extends Tool { + /** + * Creates a new instance of the `DumpCallGraph` tool. + * @param ctx Context in which the tool operates. + * @param options Options for configuring the tool. + */ constructor(ctx: MistiContext, options: DumpCallGraphOptions) { super(ctx, options); } - get defaultOptions(): DumpCallGraphOptions { + /** + * Provides default options for the tool. + */ + public get defaultOptions(): DumpCallGraphOptions { return { formats: ["dot", "mmd", "json"], outputPath: "./test/all", @@ -38,19 +46,20 @@ export class DumpCallGraph extends Tool { } /** - * Executes the DumpCallGraph tool. - * @param cu The CompilationUnit representing the code to analyze. - * @returns A ToolOutput containing messages about the generated files. + * Executes `DumpCallGraph` tool. + * @param cu `CompilationUnit` representing the code to analyze. + * @returns A `ToolOutput` containing messages about the generated files. */ - run(cu: CompilationUnit): ToolOutput | never { + public run(cu: CompilationUnit): ToolOutput | never { const callGraph = cu.callGraph; const outputPath = this.options.outputPath || "./test/all"; - const baseName = cu.projectName; + const baseName = cu.projectName || "callgraph"; const outputs: string[] = []; const supportedFormats = ["dot", "mmd", "json"] as const; type SupportedFormat = (typeof supportedFormats)[number]; + // Validate requested formats if ( !this.options.formats.every((format) => supportedFormats.includes(format as SupportedFormat), @@ -61,8 +70,10 @@ export class DumpCallGraph extends Tool { ); } + // Ensure output directory exists fs.mkdirSync(outputPath, { recursive: true }); + // Generate and save the call graph in each requested format this.options.formats.forEach((format) => { let outputData: string; let extension: string; @@ -90,7 +101,9 @@ export class DumpCallGraph extends Tool { fs.writeFileSync(fullPath, outputData, "utf-8"); outputs.push(`${extension.toUpperCase()} file saved to ${fullPath}`); } catch (error) { - outputs.push(`Failed to save ${extension} file to ${fullPath}`); + outputs.push( + `Failed to save ${extension} file to ${fullPath}: ${error}`, + ); } }); @@ -98,11 +111,19 @@ export class DumpCallGraph extends Tool { return this.makeOutput(cu, combinedOutput); } - getDescription(): string { + /** + * Provides a description of the tool. + * @returns A string describing the tool. + */ + public getDescription(): string { return "Dumps the Call Graph (CG) in multiple formats: DOT, Mermaid, and JSON."; } - getOptionDescriptions(): Record { + /** + * Provides descriptions for each option. + * @returns An object mapping option names to their descriptions. + */ + public getOptionDescriptions(): Record { return { formats: "The output formats for the call graph dump: . Specify one or more formats.", @@ -112,7 +133,15 @@ export class DumpCallGraph extends Tool { } } +/** + * Utility class to dump the call graph in Mermaid format. + */ class MermaidDumper { + /** + * Generates a Mermaid-formatted string representing the call graph. + * @param callGraph Call graph to dump. + * @returns A string in Mermaid format. + */ public static dumpCallGraph(callGraph: CallGraph): string { if (!callGraph || callGraph.getNodes().size === 0) { return 'graph TD\n empty["Empty Call Graph"]'; @@ -138,7 +167,15 @@ class MermaidDumper { } } +/** + * Utility class to dump the call graph in DOT (Graphviz) format. + */ class GraphvizDumper { + /** + * Generates a DOT-formatted string representing the call graph. + * @param callGraph Call graph to dump. + * @returns A string in DOT format. + */ public static dumpCallGraph(callGraph: CallGraph): string { if (!callGraph || callGraph.getNodes().size === 0) { console.warn("Empty call graph or no nodes available."); @@ -154,7 +191,7 @@ class GraphvizDumper { const srcNode = callGraph.getNodes().get(edge.src as CGNodeId); const dstNode = callGraph.getNodes().get(edge.dst as CGNodeId); if (srcNode && dstNode) { - dot += ` node_${srcNode.idx} -> node_${dstNode.idx} [label="${edge.idx}"];\n`; + dot += ` node_${srcNode.idx} -> node_${dstNode.idx};\n`; } else { console.warn( `Skipping edge due to missing nodes: ${edge.src} -> ${edge.dst}`, @@ -166,7 +203,15 @@ class GraphvizDumper { } } +/** + * Utility class to dump the call graph in JSON format. + */ class JSONDumper { + /** + * Serializes the call graph into a JSON-formatted string. + * @param callGraph Call graph to dump. + * @returns A JSON string representing call graph. + */ public static dumpCallGraph(callGraph: CallGraph): string { if (!callGraph) { return JSONbig.stringify({ nodes: [], edges: [] }, null, 2);