Skip to content

Instantly share code, notes, and snippets.

@blueray453
Last active May 18, 2025 06:51
Show Gist options
  • Save blueray453/489ba63da629e0d8f9f446182f726b7a to your computer and use it in GitHub Desktop.
Save blueray453/489ba63da629e0d8f9f446182f726b7a to your computer and use it in GitHub Desktop.
generate call graph from .js files
#!/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("}");
@blueray453
Copy link
Author

Example usage:

generate_callgraph_js_module data.js domain.js business.js index.js | dot -Tpng -o callgraph.png

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment