Basic JavaScript Static Analysis With Google Closure

Closure is a JavaScript compiler from Google, that will generate an AST from JavaScript. I’m exploring it in the hopes of using it to do static analyses of JavaScript embedded in Siebel.

To start with I decided to implement a very simple rule as a proof of concept. The rule is: All variables declared must be initialized immediately. For example, this code should trigger a violation:

var x;
x = 8;

Whereas this should not:

var x = 8;

The first thing I needed to do was understand the AST generated by the compiler. To represent the structure output by Closure I turned to DOT:

DOT is a plaintext format for describing a graph. In addition to defining the nodes and edges for a graph, it is also possible to add attributes to the elements of a graph in DOT, which will affect its presentation. – Closure: The Definitive Guide

This is then transformed into a PNGimage using the Graphviz tool.

Step 1: Generate DOT from JavaScript with rule violation

Compiler compiler = new Compiler();
JSSourceFile input = JSSourceFile.fromFile("/home/user/input.js");
 
Node rootNode = compiler.parse(input);
 
File tempFile = new File("/home/user/", "ast.dot");
Files.write(DotFormatter.toDot(rootNode), tempFile, Charsets.US_ASCII);

Step 2: Generate PNG from DOT

dot -T png ast.dot > example.png

Step 3: View the resulting image

Once that was done I could create a rule (CompilerPass) to search for a declaration without an immediate initialization:

package example;
 
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
 
/**
 * @author Michael Williams
 */
public class DeclareAndInitializeVariableRule implements CompilerPass {
 
    private final AbstractCompiler compiler;
 
    public DeclareAndInitializeVariableRule(AbstractCompiler compiler) {
        this.compiler = compiler;
    }
 
    @Override
    public void process(Node externs, Node root) {
        NodeTraversal.traverse(compiler, root, new DeclareAndInitializeVariable());
    }
 
    private class DeclareAndInitializeVariable extends AbstractPostOrderCallback {
        @Override
        public void visit(NodeTraversal t, Node n, Node parent) {
            if(n.getType() == Token.VAR){
                if(n.hasChildren()){
                    if(n.getFirstChild().getType() == Token.NAME){
                        if(!n.getFirstChild().hasChildren()){
                            System.out.println("Rule violation found");
                        }
                    }
                }
            }
        }
    }
}

Then inform the Compiler of the new rule (code from Closure: The Definitive Guide):

package example;
 
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.CommandLineRunner;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.CustomPassExecutionTime;
 
public class MyCommandLineRunner extends CommandLineRunner {
 
    protected MyCommandLineRunner(String[] args) {
        super(args);
    }
 
    private static Multimap<CustomPassExecutionTime, CompilerPass> getCustomPasses(
            CompilerOptions options) {
        Multimap<CustomPassExecutionTime, CompilerPass> customPasses =
                options.customPasses;
        if (customPasses == null) {
            customPasses = HashMultimap.create();
            options.customPasses = customPasses;
        }
        return customPasses;
    }
 
    @Override
    protected CompilerOptions createOptions() {
        CompilerOptions options = super.createOptions();
 
        Multimap<CustomPassExecutionTime, CompilerPass> customPasses =
                getCustomPasses(options);
 
        customPasses.put(CustomPassExecutionTime.BEFORE_CHECKS,
                new DeclareAndInitializeVariableRule(getCompiler()));
 
        return options;
    }
 
    /** Runs the Compiler */
    public static void main(String[] args) {
        MyCommandLineRunner runner = new MyCommandLineRunner(args);
        if (runner.shouldRunCompiler()) {
            runner.run();
        } else {
            System.exit(-1);
        }
    }
}

I’m just starting to understand how everything fits together, so the above is probably a very naive implementation, but it serves it’s purpose. My next challenge will be to implement more complex rules and discover a more idiomatic way of doing these checks or even if a DSL is the way to go.

This entry was posted in Programming and tagged , , , , . Bookmark the permalink.

3 Responses to Basic JavaScript Static Analysis With Google Closure

  1. John Lenz says:

    You are missing two things in your example check:
    1) VAR nodes can have multiple children.
    2) VAR nodes can also exist in FOR-IN loop without children and still be initialized:

    for (var i in x) {}

    produces:

    FOR
    VAR
    NAME
    NAME
    BLOCK

  2. Michael says:

    Thanks John. Shows that even simple rules aren’t simple 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *