I use the following code to create a call graph from javascript file(s). I generate the output in Graphviz DOT language in order to create visualizations of that graph using the Graphviz command line.
#!/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("}");
To generate graph we have to do something like:
generate_callgraph_js_module data.js domain.js business.js index.js | dot -Tpng -o callgraph.png
Here, each file name is considered as layer name.
Please let me know if the code can be simplified. I am ready to change babel with something else if makes things better.