45

I have seen "https://stackoverflow.com/questions/1385335/how-to-generate-function-call-graphs-for-javascript", and tried it. It works well, if you want to get an abstract syntax tree.

Unfortunately Closure Compiler only seems to offer --print_tree, --print_ast and --print_pass_graph. None of them are useful for me.

I want to see a chart of which function calls which other functions.

Community
  • 1
  • 1
beatak
  • 9,185
  • 10
  • 33
  • 42
  • Why don't you use the dev tools built in support for profiling javascript? – tusharmath Jun 30 '13 at 14:20
  • 1
    It appears the original thread has gone and link is now broken. :-( – Simon East Sep 24 '15 at 00:46
  • 1
    Check this out. https://github.com/cheburakshu/Javascript-Explorer-Callgraph – Sud May 29 '17 at 18:33
  • 1
    The post linked on the question was deleted long ago. A link to its latest archived version can be found [here](https://web.archive.org/web/20120907153927/https://stackoverflow.com/questions/1385335/how-to-generate-function-call-graphs-for-javascript). – OfirD Mar 28 '18 at 11:45
  • Hi beatak, having gone through all the suggestions, what have you settled with or recommend? Would you give a quick summary here at https://stackoverflow.com/questions/62613726/ please? Thx. – xpt Jun 28 '20 at 12:59
  • https://github.com/Persper/js-callgraph seems modern. – Bergi Aug 07 '20 at 16:56

4 Answers4

21

code2flow does exactly this. Full disclosure, I started this project

To run

$ code2flow source1.js source2.js -o out.gv

Then, open out.gv with graphviz

Edit: For now, this project is unmaintained. I would suggest trying out a different solution before using code2flow.

scottmrogowski
  • 2,063
  • 4
  • 23
  • 32
  • That's awesome. If it can run on jQuery, it must be able to handle my projects as well. I'll definitely try it. Thank you!!! – beatak Jun 07 '13 at 07:07
  • 3
    @scottmrogowski, your project worked really well for me. For anyone else using this solution, I'd like to point out [this page](http://dl9obn.darc.de/programming/python/dottoxml/) which converts graphviz to files that yEd can open. Scott, I tweaked your python script to name the nodes based on the function names, and it produced great yEd-readable output. – John Walthour Jan 03 '14 at 18:15
  • Unfortunately, it seems the project went unmaintained. I was unable to make code2flow just working on my Windows and Linux laptop. – Achille Mar 19 '16 at 03:29
  • 1
    @achille You are right, this is unmaintained and I've now updated my answer. I developed this on a Mac and tested it on both Mac and Debian systems. Windows users have had trouble getting this to work. You might want to verify that you are using Python 2.7x instead of 3.x. – scottmrogowski Mar 21 '16 at 18:41
  • 1
    It works on windows: 1. install graphviz: https://graphviz.gitlab.io/_pages/Download/windows/graphviz-2.38.msi 2. ADD below to Windows Environment PATH, i.e. C:\Program Files (x86)\Graphviz2.38\bin 3. cd C:\code2flow-master\code2flow-master python setup.py install python code2flow myfile.js -o myfile.jpeg However it does not understand asyc.parallel, and others in javascript, so its mostly useless for these basic scenarios. – Manohar Reddy Poreddy Mar 21 '18 at 10:51
6

If you filter the output of closure --print_tree you get what you want.

For example take the following file:

var fib = function(n) {
    if (n < 2) {
        return n;
    } else {
        return fib(n - 1) + fib(n - 2);
    }
};

console.log(fib(fib(5)));

Filter the output of closure --print_tree

            NAME fib 1 
                FUNCTION  1 
                                    CALL 5 
                                        NAME fib 5 
                                        SUB 5 
                                            NAME a 5 
                                            NUMBER 1.0 5 
                                    CALL 5 
                                        NAME fib 5 
                                        SUB 5 
                                            NAME a 5 
                                            NUMBER 2.0 5 
        EXPR_RESULT 9 
            CALL 9 
                GETPROP 9 
                    NAME console 9 
                    STRING log 9 
                CALL 9 
                CALL 9 
                    NAME fib 9 
                    CALL 9 
                    CALL 9 
                        NAME fib 9 
                        NUMBER 5.0 9 

And you can see all the call statements.

I wrote the following scripts to do this.

./call_tree

#! /usr/bin/env sh
function make_tree() {
    closure --print_tree $1 | grep $1
}

function parse_tree() {
    gawk -f parse_tree.awk
}

if [[ "$1" = "--tree" ]]; then
    make_tree $2
else
    make_tree $1 | parse_tree
fi

parse_tree.awk

BEGIN {
    lines_c = 0
    indent_width = 4
    indent_offset = 0
    string_offset = ""
    calling = 0
    call_indent = 0
}

{
    sub(/\[source_file.*$/, "")
    sub(/\[free_call.*$/, "")
}

/SCRIPT/ {
    indent_offset = calculate_indent($0)
    root_indent = indent_offset - 1
}

/FUNCTION/ {
    pl  = get_previous_line()
    if (calculate_indent(pl) < calculate_indent($0))
        print pl
    print
}

{
    lines_v[lines_c] = $0
    lines_c += 1
}

{
    indent = calculate_indent($0)
    if (indent <= call_indent) {
        calling = 0
    }
    if (calling) {
        print
    }
}

/CALL/ {
    calling = 1
    call_indent = calculate_indent($0)
    print
}

/EXPR/{
    line_indent = calculate_indent($0)
    if (line_indent == root_indent) {
        if ($0 !~ /(FUNCTION)/) {
            print
        }
    }
}

function calculate_indent(line) {
    match(line, /^ */)
    return int(RLENGTH / indent_width) - indent_offset
}

function get_previous_line() {
    return lines_v[lines_c - 1]
}
Simon East
  • 55,742
  • 17
  • 139
  • 133
4

I finally managed this using UglifyJS2 and Dot/GraphViz, in a sort of combination of the above answer and the answers to the linked question.

The missing part, for me, was how to filter the parsed AST. It turns out that UglifyJS has the TreeWalker object, which basically applys a function to each node of the AST. This is the code I have so far:

//to be run using nodejs
var UglifyJS = require('uglify-js')
var fs = require('fs');
var util = require('util');

var file = 'path/to/file...';
//read in the code
var code = fs.readFileSync(file, "utf8");
//parse it to AST
var toplevel = UglifyJS.parse(code);
//open the output DOT file
var out = fs.openSync('path/to/output/file...', 'w');
//output the start of a directed graph in DOT notation
fs.writeSync(out, 'digraph test{\n');

//use a tree walker to examine each node
var walker = new UglifyJS.TreeWalker(function(node){
    //check for function calls
    if (node instanceof UglifyJS.AST_Call) {
        if(node.expression.name !== undefined)
        {
        //find where the calling function is defined
        var p = walker.find_parent(UglifyJS.AST_Defun);

        if(p !== undefined)
        {
            //filter out unneccessary stuff, eg calls to external libraries or constructors
            if(node.expression.name == "$" || node.expression.name == "Number" || node.expression.name =="Date")
            {
                //NOTE: $ is from jquery, and causes problems if it's in the DOT file.
                //It's also very frequent, so even replacing it with a safe string
                //results in a very cluttered graph
            }
            else
            {

                fs.writeSync(out, p.name.name);
                fs.writeSync(out, " -> ");
                fs.writeSync(out, node.expression.name);
                fs.writeSync(out, "\n");
            }
        }
        else
        {
            //it's a top level function
            fs.writeSync(out, node.expression.name);
            fs.writeSync(out, "\n");
        }

    }
}
if(node instanceof UglifyJS.AST_Defun)
{
    //defined but not called
    fs.writeSync(out, node.name.name);
    fs.writeSync(out, "\n");
}
});
//analyse the AST
toplevel.walk(walker);

//finally, write out the closing bracket
fs.writeSync(out, '}');

I run it with node, and then put the output through

dot -Tpng -o graph_name.png dot_file_name.dot

Notes:

It gives a pretty basic graph - only black and white and no formatting.

It doesn't catch ajax at all, and presumably not stuff like eval or with either, as others have mentioned.

Also, as it stands it includes in the graph: functions called by other functions (and consequently functions that call other functions), functions that are called independantly, AND functions that are defined but not called.

As a result of all this, it may miss things that are relevant, or include things that are not. It's a start though, and appears to accomplish what I was after, and what led me to this question in the first place.

Community
  • 1
  • 1
rorold
  • 326
  • 3
  • 13
  • Interesting. It does make a call graph for a simple javascript. Thanks for your efforts! (side note: Recently I started to dig this area with Esprima http://esprima.org/ and Esprima is so interesting.) – beatak Jan 10 '13 at 21:14
  • @beatak Did you manage to generate similar graphs with esprima? – tomasz Mar 27 '18 at 23:32
1

https://github.com/mishoo/UglifyJS gives access to an ast in javascript.

ast.coffee

util = require 'util'
jsp = require('uglify-js').parser

orig_code = """

var a = function (x) {
  return x * x;
};

function b (x) {
  return a(x)
}

console.log(a(5));
console.log(b(5));

"""

ast = jsp.parse(orig_code)

console.log util.inspect ast, true, null, true