Java Compiler Plug-ins in Java 8 (Use a new plug-in mechanism to extend the Java compiler with new behavior)

Java Compiler Plug-ins in Java 8

(Use a new plug-in mechanism to extend the Java compiler with new behavior)


Java 8 will bring a new mechanism that allows you to write plug-ins for the Java compiler (javac). A compiler
plug-in lets you add new phases to javac without making changes to its code base. New behavior can be encapsulated
in a plug-in and distributed for other people to use. For example, javac plug-ins could be used to do the following:

¦¦ Add extra compile-time checks
¦¦ Add code transformations
¦¦ Perform customized analysis of source code

Note: The API for creating

javac plug-ins is still experimental for JDK 8; it is scheduled to ship in 2013.

In this article, we show how you can write a simple, customized source code analysis tool so you can learn how to
leverage the plug-in mechanisms for your own applications. We find code patterns that check whether the result
of calling get() on an object that is a subtype of Map is null. In other words we are looking for patterns such as
the following, where expr is a subtype of java.util.Map.

expr.get(key) == null

This pattern could be contained within a conditional expression, a return statement, and so on. It should be
reported in all cases. How Do Plug-ins Differ from Annotation Processors? Here are some ways that plug-ins differ from annotation
processors:
¦¦ Plug-ins are more flexible. They can run at various points in the compilation pipeline through a TaskListener.
¦¦ Plug-ins do not use the model of processing rounds, which incurs additional overhead. Plug-ins have a simpler interface.
¦¦ Annotation processors are defined by a standard (JSR 269). Plug-ins are javac-specific.
¦¦ Plug-ins require the use of a ServiceLoader. Java Compiler Plug-in Architecture A javac plug-in supports two methods:
¦¦ First, a getName() method that returns the name of the plug-in for identification purposes
¦¦ Second, a call() method that is invoked by the javac with the current environment it is processing The call() method gives
access to the compiler functionalities through a JavacTask object, which allows you to perform parsing, type checking, and compilation.

In addition, the JavacTask object lets you add observers (which are instances of TaskListener) to various events generated during compilation.
This is done through the method addTaskListener(), which accepts an object TaskListener. Listing 1 shows com.sun.source.util.Plugin, which
uses the getName() and call() methods. Listing 2 shows some methods available in com.sun.source.util.JavacTask, and Listing 3 shows the
methods available in com.sun.source.util.TaskListener.

But there are obvious questions that remain unanswered, for example, how do you start writing the compiler plug-in and how do you
run it? Let’s Build a Compiler Plug-In

The first step is to download and build the current release of the Java 8 compiler, which supports compiler plug-ins.
Toward this end, download(http://hg.openjdk.java.net/jdk8/jdk8/langtools)the latest version and follow the build instructions in the
README file.

Once the compiler is built, include the generated dist/lib/classes.jar file in your project. Alternatively, you can download
a ready-made binary from http://jdk8.java.net.Next, there are several steps we need to follow to build our plug-in.
Here is a summary of the steps:
1. Implement the com.sun.source.util.Plugin interface shown in Listing 1.
2. Add a TaskListener to perform additional behavior after the type-checking phase.
3. Create an abstract syntax tree(AST) visitor to locate binary expressions:
a. Evaluate whether the left side is a method call expression with a receiver’s type that is a subtype of java.util.Map.
b. Evaluate whether the right side is a null expression.

Step 1: Implement the com.sun.source.util.Plugin interface. The first step is to create the main class, which implements com.sun.source.util.Plugin.
We return the name of our plug-in via the getName() method, as shown in Listing 4.

Step 2: Add a TaskListener. We are looking for a code pattern that checks whether the receiver of the method call get() is a subtype of java.util.Map.
To get the type of the receiver, we need information about the types of expressions that are resolved during the type-checking phase. We therefore
need to insert a new phase after type checking. Toward this end, we first create a TaskListener and add it to the current JavacTask, as shown in Listing 5.

We now need to create the class CodePatternTaskListener, which implements a TaskListener. A TaskListener has two methods,started() and finished(), which are
called, respectively, before and after certain events. These events are encapsulated in a TaskEvent object. In our case, all we need to do is implement
the finished() method and check for an event mentioning the Analyze phase (type checking), as shown in Listing 6.

Step 3: Create the AST visitor. Next, we need to write the logic to locate the code pattern and report it. How do we do that? Thankfully, a task
event provides us with a CompilationUnitTree,which represents the current source file analyzed in a tree structure. It can be accessed with the getCompilationUnit() method.

Our code will need to traverse this tree, locate a binary node,and evaluate the node’s left andright children. This sounds like a visitor pattern job. The Compiler
Tree API provides us with a readymade visitor class designed for such tasks: com.sun.source.util.TreeScanner<R, P>. It visits all the nodes in the AST.

First, we initialize our visitor object and then visit the CompilationUnitTree:, as shown in Listing 7.

All we have left to do is to override visitBinary(BinaryTree node,P p) and write the logic to verify the code pattern. The full code of the visitor class with inlined
comments is provided in Listings 8. Let’s Run Our Compiler Plug-In

We are almost finished. The final step is to set up a file called com.sun.source.util.Plugin located in META-INF/services/. This file must contain the name of our
plug-in, CodePatternPlugin, which allows javac to load the appropriate plug-in.

Next, using your favorite IDE or the command line, create a Java archive (JAR) file from your project containing the META-INF directory and compiled class files:

$ jar –cvf codePatternPlugin.jar META-INF *.class

Finally, you can run the plug-in as shown in Listing 9, where
¦¦ -processorpath indicates the path where the plug-in JAR file is located
¦¦ -Xplugin indicates the name of the plug-in to run, which is CodePatternPlugin, in this case

Conclusion
The new plug-in mechanism provides a simple hook to javac. You can use it to extend javac with new behavior. In addition, you can distribute a plug-in without
making modifications to the javac code base. In this article, we showed how you can use this mechanism to easily write a customized source code analysis tool
for your applications.

Listing will be follows

Code listings for “Java Compiler Plug-ins in Java 8,”

[Listing 1]
public interface Plugin {
public String getName();
public void call(JavacTask task, String[] pluginArgs);
}

[Listing 2]
public abstract class JavacTask implements CompilationTask {

public abstract Iterable<? extends Element> analyze() throws IOException;
public abstract Iterable<? extends JavaFileObject> generate throws IOException;
public abstract void addTaskListener(TaskListener taskListener);

}

[Listing 3]
public interface TaskListener
{
public void started(TaskEvent e);
public void finished(TaskEvent e);
}

[Listing 4]
public class CodePatternPlugin implements Plugin{

@Override
public void call(JavacTask task, String[] args) {
// See Step 2
}

@Override
public String getName() {
return “CodePatternPlugin”;
}
}

[Listing 5]
public void call(JavacTask task, String[] args) {
System.out.println(“Running!”);
task.addTaskListener(new CodePatternTaskListener(task));
}

[Listing 6]
public class CodePatternTaskListener implements TaskListener {
private final CodePatternTreeVisitor visitor;

CodePatternTaskListener(JavacTask task) {
visitor = new CodePatternTreeVisitor(task);
}
@Override
public void finished(TaskEvent taskEvent) {
if(taskEvent.getKind() == TaskEvent.Kind.ANALYZE)
{
// See Step 3
}
}

@Override
public void started(TaskEvent taskEvent) {

}
}

[Listing 7]
if(taskEvent.getKind().equals(TaskEvent.Kind.ANALYZE))
{
CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
new CodePatternTreeVisitor().scan(compilationUnit, null);
}

[Listing 8]
import javax.lang.model.element.Name;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import com.sun.source.tree.*;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;

public class CodePatternTreeVisitor extends TreePathScanner<Void, Void> {
// offsets of AST nodes in source file
private final SourcePositions sourcePositions;
// bridges Compiler api, Annotation Processing API and Tree API
private final Trees trees;
// utility to operate on types
private final Types types;
private final TypeMirror mapType;
private final Name getName;

private CompilationUnitTree currCompUnit;

CodePatternTreeVisitor(JavacTask task) {
types = task.getTypes();
trees = Trees.instance(task);
sourcePositions = trees.getSourcePositions();

// utility to operate on program elements
Elements elements = task.getElements();
// create the type element to match against
mapType = elements.getTypeElement(“java.util.Map”).asType();
// create a Name object representing the method name to match against
getName = elements.getName(“get”);
}

@Override
public Void visitCompilationUnit(CompilationUnitTree tree, Void p) {
currCompUnit = tree;
return super.visitCompilationUnit(tree, p);
}

@Override
public Void visitBinary(BinaryTree tree, Void p) {
// unpack the kind of expression, left and right hand side
ExpressionTree left = tree.getLeftOperand();
ExpressionTree right = tree.getRightOperand();
Kind kind = tree.getKind();

// apply our code pattern logic
if (isGetCallOnMap(new TreePath(getCurrentPath(), left))
&& kind == Kind.EQUAL_TO
&& isNullLiteral(right)) {
System.out.println(“Found Match at line: ”
+ getLineNumber(tree) + ” in ”
+ currCompUnit.getSourceFile().getName());
}
return super.visitBinary(tree, p);
}

private boolean isNullLiteral(ExpressionTree node) {
// is this expression representing “null”?
return (node.getKind() == Kind.NULL_LITERAL);
}

private boolean isGetCallOnMap(TreePath path) {
switch (path.getLeaf().getKind()) {
// is it a Method Invocation?
case METHOD_INVOCATION:
MethodInvocationTree methodInvocation = (MethodInvocationTree) path.getLeaf();
// extract the identifier and receiver (methodSelectTree)
ExpressionTree methodSelect = methodInvocation.getMethodSelect();
switch (methodSelect.getKind()) {
case MEMBER_SELECT:
// extract receiver
MemberSelectTree mst = (MemberSelectTree) methodSelect;
ExpressionTree expr = mst.getExpression();
// get type of extracted receiver
TypeMirror type = trees.getTypeMirror(new TreePath(path, expr));
// extract method name
Name name = mst.getIdentifier();
// 1) check if receiver’s type is subtype of java.util.Map
// 2) check if the extracted method name is exactly “get”
if (types.isAssignable(types.erasure(type), types.erasure(mapType))
&& name == getName) {
return true;
}
}
}
return false;
}

private long getLineNumber(Tree tree) {
// map offsets to line numbers in source file
LineMap lineMap = currCompUnit.getLineMap();
if (lineMap == null)
return -1;
// find offset of the specified AST node
long position = sourcePositions.getStartPosition(currCompUnit, tree);
return lineMap.getLineNumber(position);
}
}

[Listing 9]
$ javac -processorpath codePatternPlugin.jar -Xplugin:CodePatternPlugin Test.java

Copyright 2013, Oracle Corporation