Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make the java compiler warn when an annotated method is used (like @deprecated)

Let's say I define a custom annotation called @Unsafe.

I'd like to provide an annotation processor which will detect references to methods annotated with @Unsafe and print a warning.

For example, given this code ...

public class Foo {
  @Unsafe
  public void doSomething() { ... }
}

public class Bar {
  public static void main(String[] args) {
    new Foo().doSomething();
  }
}

... I want the compiler to print something like:

WARN > Bar.java, line 3 : Call to Unsafe API - Foo.doSomething()

It is very similar in spirit to @Deprecated, but my annotation is communicating something different, so I can't use @Deprecated directly. Is there a way to achieve this with an annotation processor? The annotation processor API seems to be more focused on the entities applying the annotations (Foo.java in my example) than entities which reference annotated members.

This question provides a technique to achieve it as a separate build step using ASM. But I'm wondering if I can do it in a more natural way with javac & annotation processing?

like image 842
greghmerrill Avatar asked Dec 29 '15 22:12

greghmerrill


2 Answers

I think I could have technically achieved my goal using the response from @mernst, so I appreciate the suggestion. However, I found another route that worked better for me as I'm working on a commercial product and cannot incoporate the Checker Framework (its GPL license is incompatible with ours).

In my solution, I use my own "standard" java annotation processor to build a listing of all the methods annotated with @Unsafe.

Then, I developed a javac plugin. The Plugin API makes it easy to find every invocation of any method in the AST. By using some tips from this question, I was able to determine the class and method name from the MethodInvocationTree AST node. Then I compare those method invocations with the earlier "listing" I created containing methods annotated with @Unsafe and issue warnings where required.

Here is an abbreviated version of my javac Plugin.

import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.Plugin;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeInfo;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreeScanner;

public class UnsafePlugin implements Plugin, TaskListener {

  @Override
  public String getName() {
    return "UnsafePlugin";
  }

  @Override
  public void init(JavacTask task, String... args) {
    task.addTaskListener(this);
  }

  @Override
  public void finished(TaskEvent taskEvt) {
    if (taskEvt.getKind() == Kind.ANALYZE) {
      taskEvt.getCompilationUnit().accept(new TreeScanner<Void, Void>() {
        @Override
        public Void visitMethodInvocation(MethodInvocationTree methodInv, Void v) {
          Element method = TreeInfo.symbol((JCTree) methodInv.getMethodSelect());
          TypeElement invokedClass = (TypeElement) method.getEnclosingElement();
          String className = invokedClass.toString();
          String methodName = methodInv.getMethodSelect().toString().replaceAll(".*\\.", "");
          System.out.println("Method Invocation: " + className + " : " + methodName);
          return super.visitMethodInvocation(methodInv, v);
        }
      }, null);
    }
  }

  @Override
  public void started(TaskEvent taskEvt) {
  }

}

Note - in order for the javac plugin to be invoked, you must provide arguments on the command line:

javac -processorpath build/unsafe-plugin.jar -Xplugin:UnsafePlugin

Also, you must have a file META-INF/services/com.sun.source.util.Plugin in unsafe-plugin.jar containing the fully qualified name of the plugin:

com.unsafetest.javac.UnsafePlugin
like image 68
greghmerrill Avatar answered Sep 19 '22 12:09

greghmerrill


Yes, this is possible using annotation processing.

One complication is that a standard annotation processor does not descend into method bodies (it only examines the method declaration). You want an annotation processor that examines every line of code.

The Checker Framework is designed to build such annotation processors. You just need to define a callback that, given a method call and issues a javac warning if the call is not acceptable. (In your case, it's simply whether the method's declaration has an @Unsafe annotation.) The Checker Framework runs that callback on every method call in the program.

like image 45
mernst Avatar answered Sep 17 '22 12:09

mernst