Last active
May 18, 2025 06:51
-
-
Save blueray453/489ba63da629e0d8f9f446182f726b7a to your computer and use it in GitHub Desktop.
generate call graph from .js files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
const fs = require('fs'); | |
const path = require('path'); | |
const parser = require('@babel/parser'); | |
const traverse = require('@babel/traverse').default; | |
// Input files from command-line arguments | |
const inputFiles = process.argv.slice(2); | |
// Data structures | |
const functionsByFile = {}; | |
const calls = []; | |
const topLevelCallsByFile = {}; | |
function getFunctionName(node, parent) { | |
if (node.id && node.id.name) return node.id.name; | |
if (parent.type === 'VariableDeclarator' && parent.id.name) return parent.id.name; | |
return null; | |
} | |
function parseFile(filePath) { | |
const code = fs.readFileSync(filePath, 'utf8'); | |
const ast = parser.parse(code, { | |
sourceType: 'module', | |
plugins: ['jsx', 'typescript'] | |
}); | |
const fileName = path.basename(filePath); | |
functionsByFile[fileName] = []; | |
topLevelCallsByFile[fileName] = []; | |
traverse(ast, { | |
enter(pathNode) { | |
const node = pathNode.node; | |
// Function declaration or arrow function assignment | |
if ( | |
node.type === 'FunctionDeclaration' || | |
(node.type === 'VariableDeclarator' && | |
(node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression')) | |
) { | |
const funcName = getFunctionName(node, pathNode.parent); | |
if (funcName) { | |
functionsByFile[fileName].push(funcName); | |
} | |
} | |
// Function call expression | |
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') { | |
const calleeName = node.callee.name; | |
const enclosingFunc = pathNode.getFunctionParent(); | |
if (enclosingFunc) { | |
const enclosingId = enclosingFunc.node.id || enclosingFunc.parent?.id; | |
const enclosingName = enclosingId ? enclosingId.name : null; | |
if (enclosingName) { | |
calls.push({ | |
from: enclosingName, | |
to: calleeName, | |
file: fileName | |
}); | |
} | |
} else { | |
// Track top-level calls | |
topLevelCallsByFile[fileName].push({ | |
caller: fileName, | |
callee: calleeName | |
}); | |
} | |
} | |
} | |
}); | |
} | |
// Parse each file | |
inputFiles.forEach(parseFile); | |
// Generate DOT file | |
console.log("digraph CallGraph {"); | |
console.log(" rankdir=LR;"); | |
console.log(" node [shape=box, style=filled, fillcolor=\"#f9f9f9\"];"); | |
// Create clusters for each file | |
for (const [file, funcs] of Object.entries(functionsByFile)) { | |
const clusterId = file.replace(/[^\w]/g, '_'); | |
console.log(` subgraph cluster_${clusterId} {`); | |
console.log(` label = "${file}";`); | |
console.log(" style=filled;"); | |
console.log(" color=lightgrey;"); | |
funcs.forEach(fn => { | |
console.log(` "${fn}";`); | |
}); | |
console.log(" }"); | |
} | |
// Add call relationships between functions | |
calls.forEach(({ from, to, file }) => { | |
const targetExists = Object.values(functionsByFile).some(funcs => funcs.includes(to)); | |
if (targetExists) { | |
console.log(` "${from}" -> "${to}";`); | |
} | |
}); | |
// Add top-level call relationships | |
for (const [file, topCalls] of Object.entries(topLevelCallsByFile)) { | |
if (topCalls.length > 0) { | |
const clusterId = file.replace(/[^\w]/g, '_'); | |
console.log(` subgraph cluster_${clusterId}_top {`); | |
console.log(` label = "${file} (top-level)";`); | |
console.log(" style=filled;"); | |
console.log(" color=lightblue;"); | |
console.log(` "${file}" [shape=ellipse];`); | |
console.log(" }"); | |
topCalls.forEach(({ callee }) => { | |
const targetExists = Object.values(functionsByFile).some(funcs => funcs.includes(callee)); | |
if (targetExists) { | |
console.log(` "${file}" -> "${callee}";`); | |
} | |
}); | |
} | |
} | |
console.log("}"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example usage: