diff --git a/src/checker/callgraph/callgraph-checker.ts b/src/checker/callgraph/callgraph-checker.ts index 5cac34ae..b489df66 100644 --- a/src/checker/callgraph/callgraph-checker.ts +++ b/src/checker/callgraph/callgraph-checker.ts @@ -73,6 +73,36 @@ class CallgraphChecker extends CheckerCallgraph { this.triggerAtFunctionCallBefore(analyzer, scope, node, state, info) } + /** + * Returns whether this call target should be skipped (i.e., not emitted into the call graph). + * + * This is used to filter out internal/closure artifacts that show up as symbols with + * meaningless or implementation-specific paths. + * + * @param fclos - Callee closure/symbol. + * @param fclosName - Callee name (best-effort). + * @returns True if the call should be skipped. + */ + shouldSkipCall(fclos: any, fclosName: string): boolean { + // 1) Skip paths with internal markers (scopes/blocks/instances, etc.) + const fullPath = fclos?.qid || fclos?.sid || fclos?.id || '' + if (typeof fullPath === 'string') { + if (fullPath.includes('') || + fullPath.includes('_scope') || + fullPath.includes('')) { + return true + } + } + + // 2) Skip malformed/anonymous path prefixes + if (fclosName.startsWith('...') || fclosName.startsWith(':')) { + return true + } + + return false + } + /** * * @param analyzer @@ -84,9 +114,24 @@ class CallgraphChecker extends CheckerCallgraph { triggerAtFunctionCallBefore(analyzer: any, scope: any, node: any, state: any, info: any): void { const { fclos, argvalues, ainfo } = info const fdecl = fclos.fdef - if (fclos === undefined || fdecl?.type !== 'FunctionDefinition') { + + if (fclos === undefined) { return } + + // Best-effort callee name resolution. + const fclosName = fclos?.name || fclos?.id || fclos?.sid || '' + + // External call: no resolvable FunctionDefinition in the analysis scope. + const isExternalCall = fdecl?.type !== 'FunctionDefinition' + + // For external calls, filter out noisy/internal symbol paths. + if (isExternalCall) { + if (this.shouldSkipCall(fclos, fclosName)) { + return + } + } + const stack = state.callstack const to = fclos const toAST = fclos && fclos.fdef @@ -98,14 +143,54 @@ class CallgraphChecker extends CheckerCallgraph { return } const callgraph = (ainfo.callgraph = ainfo.callgraph || new this.kit.Graph()) - const fromNode = callgraph.addNode(this.prettyPrint(from, fromAST, call_site_node), { + const fromNodeLabel = this.prettyPrint(from, fromAST, call_site_node) + + // Compute the callee node label. For external calls, use a simplified `` label. + let toNodeLabel: string + if (isExternalCall) { + toNodeLabel = this.generateExternalLabel(fclos, fclosName) + } else { + toNodeLabel = this.prettyPrint(to, toAST, call_site_node) + } + + const fromNode = callgraph.addNode(fromNodeLabel, { funcDef: fromAST, funcSymbol: from, }) - const toNode = callgraph.addNode(this.prettyPrint(to, toAST, call_site_node), { funcDef: toAST, funcSymbol: to }) + const toNode = callgraph.addNode(toNodeLabel, { funcDef: toAST, funcSymbol: to }) callgraph.addEdge(fromNode, toNode, { callSite: call_site_node }) } + /** + * Builds a readable label for an external call target. + * + * We derive a best-effort module path from the parent chain and emit: + * ` module.submodule.function`. + */ + generateExternalLabel(fclos: any, fclosName: string): string { + // Build a best-effort module path from the parent chain. + const pathParts: string[] = [] + let current = fclos.parent + while (current) { + const partName = current.name || current.id || current.sid + if (partName && typeof partName === 'string') { + // Filter out internal markers and local implementation details. + if (!partName.includes('<') && !partName.includes('>') && + !partName.includes('./') && !partName.includes('_scope') && + !partName.includes(' 0) { + return ` ${pathParts.join('.')}.${fclosName}` + } else { + return ` ${fclosName}` + } + } + /** * * @param analyzer diff --git a/src/engine/analyzer/common/analyzer.ts b/src/engine/analyzer/common/analyzer.ts index 924607dd..3d2fb650 100644 --- a/src/engine/analyzer/common/analyzer.ts +++ b/src/engine/analyzer/common/analyzer.ts @@ -2023,6 +2023,12 @@ class Analyzer extends MemSpace { */ executeCall(node: any, fclos: any, argvalues: any, state: any, scope: any): any { if (Config.makeAllCG && fclos?.fdef?.type === 'FunctionDefinition' && this.ainfo?.callgraph?.nodes) { + const funcKey = `${fclos.fdef?.loc?.sourcefile}:${fclos.fdef?.loc?.start?.line}-${fclos.fdef?.loc?.end?.line}` + + if (!this.ainfo.analyzedFunctionBodies) { + this.ainfo.analyzedFunctionBodies = new Set() + } + for (const callgraphnode of this.ainfo?.callgraph?.nodes.values()) { if ( callgraphnode.opts?.funcDef?.loc?.start?.line && @@ -2031,6 +2037,9 @@ class Analyzer extends MemSpace { callgraphnode.opts?.funcDef?.loc?.start?.line === fclos.fdef?.loc?.start?.line && callgraphnode.opts?.funcDef?.loc?.end?.line === fclos.fdef?.loc?.end?.line ) { + const alreadyAnalyzed = this.ainfo.analyzedFunctionBodies.has(funcKey) + + // add call edge this.checkerManager.checkAtFunctionCallBefore(this, scope, node, state, { argvalues, fclos, @@ -2041,12 +2050,18 @@ class Analyzer extends MemSpace { analyzer: this, ainfo: this.ainfo, }) - return SymbolValue({ - type: 'FunctionCall', - expression: fclos, - arguments: argvalues, - ast: node, - }) + + if (alreadyAnalyzed) { + return SymbolValue({ + type: 'FunctionCall', + expression: fclos, + arguments: argvalues, + ast: node, + }) + } else { + this.ainfo.analyzedFunctionBodies.add(funcKey) + break + } } } } @@ -2504,13 +2519,14 @@ class Analyzer extends MemSpace { if (logger.isTraceEnabled()) logger.trace(`\nprocessCall: function: ${this.formatScope(fdecl?.id?.name)}`) - // avoid infinite loops,the re-entry should only less than 3 - if ( - fdecl && - state.callstack.reduce((previousValue: any, currentValue: any) => { - return currentValue.fdef === fdecl ? previousValue + 1 : previousValue - }, 0) > 0 - ) { + // avoid infinite loops, the re-entry should only less than 3 + const recursionCount = fdecl + ? state.callstack.reduce((previousValue: any, currentValue: any) => { + return currentValue.fdef === fdecl ? previousValue + 1 : previousValue + }, 0) + : 0 + + if (fdecl && recursionCount > 0) { return SymbolValue({ type: 'FunctionCall', expression: fclos,