Friday, 18 January 2013

JavaScript: AMD module dependency analysis with the Java Rhino engine

The Asynchronous Module Definition (AMD) API provides powerful modularization options to JavaScript developers. But this introduces its own problems when it comes to dependency management. As this post demonstrates, Mozilla's Rhino engine offers developers a means to analyze these dependencies.

Correctness and efficiency

Interdependency graphs can be used to enforce or report on adherence to packaging principles. For example, it is possible to prohibit cyclic dependencies or provide unstable dependency metrics.

When designing for efficient code reuse across web applications there are competing goals: minimize the number of file transfers to reduce network latency; prevent parsing of code that won't be executed until it's needed and reduce payload size. Dependency data can be used to determine which packages can be layered together into single files without duplication.

The Rhino AST package

Rhino 1.7R3 introduces a package for producing an abstract syntax tree.

Here's a sample JavaScript module:

// hideElements.js
define([ "dojo/_base/array", "dojo/dom-class"], function(array, domClass) {
  "use strict";

  // usage: hideElements(element1, element2, ... elementN);
  return function() {
    array.forEach(arguments, function(element) {
      domClass.add(element, "hide");
    });
  };
});

This (completely untested) module provides a function that adds a CSS class to the specified nodes, but that isn't important. The important part is that it depends on two Dojo modules.

Using Rhino's API the following Java code parses and emits the elements of the code:

import java.io.*;
import org.mozilla.javascript.Parser;
import org.mozilla.javascript.ast.*;

public class Dump {
  public static void main(String[] args) throws IOException {
    class Printer implements NodeVisitor {
      @Override
      public boolean visit(AstNode node) {
        String indent = "%1$Xs".replace("X", String.valueOf(node.depth() + 1));
        System.out.format(indent, "").println(node.getClass());
        return true;
      }
    }
    String file = "hideElements.js";
    Reader reader = new FileReader(file);
    try {
      AstNode node = new Parser().parse(reader, file, 1);
      node.visit(new Printer());
    } finally {
      reader.close();
    }
  }
}

The tree and its elements:

 class org.mozilla.javascript.ast.AstRoot
  class org.mozilla.javascript.ast.ExpressionStatement
   class org.mozilla.javascript.ast.FunctionCall
    class org.mozilla.javascript.ast.Name
    class org.mozilla.javascript.ast.ArrayLiteral
     class org.mozilla.javascript.ast.StringLiteral
     class org.mozilla.javascript.ast.StringLiteral
    class org.mozilla.javascript.ast.FunctionNode
     class org.mozilla.javascript.ast.Name
     class org.mozilla.javascript.ast.Name
     class org.mozilla.javascript.ast.Block
      class org.mozilla.javascript.ast.ExpressionStatement
       class org.mozilla.javascript.ast.StringLiteral
      class org.mozilla.javascript.ast.ReturnStatement
       class org.mozilla.javascript.ast.FunctionNode
        class org.mozilla.javascript.ast.Block
         class org.mozilla.javascript.ast.ExpressionStatement
          class org.mozilla.javascript.ast.FunctionCall
           class org.mozilla.javascript.ast.PropertyGet
            class org.mozilla.javascript.ast.Name
            class org.mozilla.javascript.ast.Name
           class org.mozilla.javascript.ast.Name
           class org.mozilla.javascript.ast.FunctionNode
            class org.mozilla.javascript.ast.Name
            class org.mozilla.javascript.ast.Block
             class org.mozilla.javascript.ast.ExpressionStatement
              class org.mozilla.javascript.ast.FunctionCall
               class org.mozilla.javascript.ast.PropertyGet
                class org.mozilla.javascript.ast.Name
                class org.mozilla.javascript.ast.Name
               class org.mozilla.javascript.ast.Name
               class org.mozilla.javascript.ast.StringLiteral

Finding the dependencies

This code looks for string literals in the first array in a define function call:

import java.io.*;
import java.util.*;
import org.mozilla.javascript.Parser;
import org.mozilla.javascript.ast.*;

public class Dependencies {

  public static void main(String[] args) throws IOException {
    final List<String> dependencies = new ArrayList<String>();

    class Printer implements NodeVisitor {
      @Override
      public boolean visit(AstNode node) {
        if (isDefineCall(node)) {
          addDependencies(dependencies, node.getParent());
        }
        return true;
      }
    }

    String file = "hideElements.js";
    Reader reader = new FileReader(file);
    try {
      AstNode node = new Parser().parse(reader, file, 1);
      node.visit(new Printer());
    } finally {
      reader.close();
    }

    System.out.println(dependencies);
  }

  private static boolean isDefineCall(AstNode node) {
    if (node instanceof Name) {
      Name name = (Name) node;
      if ("define".equals(name.getIdentifier())
          && name.getParent() instanceof FunctionCall) {
        return true;
      }
    }
    return false;
  }

  private static void addDependencies(final List<String> dependencies,
      final AstNode define) {
    class ArrayFinder implements NodeVisitor {
      @Override
      public boolean visit(AstNode node) {
        if (node.depth() == define.depth() + 1 && node instanceof ArrayLiteral) {
          collectStrings(dependencies, node);
          return false;
        }
        return true;
      }
    }

    define.visit(new ArrayFinder());
  }

  private static void collectStrings(final List<String> dependencies,
      final AstNode array) {
    class StringCollector implements NodeVisitor {
      @Override
      public boolean visit(AstNode node) {
        if (node.depth() == array.depth() + 1 && node instanceof StringLiteral) {
          StringLiteral str = (StringLiteral) node;
          dependencies.add(str.getValue());
        }
        return true;
      }
    }

    array.visit(new StringCollector());
  }
}

The application output:

[dojo/_base/array, dojo/dom-class]

Note the limitations in the posted code. For example, it doesn't try to evaluate literal expressions like "dojo/" + "dom-class" or handle the redefinition or scoping of the define reference.

End notes

Here are the Java compiler options expressed as an Apache Maven pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>demo</groupId>
  <artifactId>jsdep</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.5</version>
        <configuration>
          <encoding>US-ASCII</encoding>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>org.mozilla</groupId>
      <artifactId>rhino</artifactId>
      <version>1.7R4</version>
    </dependency>
  </dependencies>
</project>

Note that this is quick'n'dirty demo code and shouldn't be reused verbatim (e.g. it uses FileReader instead of specifying a file encoding.)

No comments:

Post a Comment

All comments are moderated