14

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?

Community
  • 1
  • 1
greghmerrill
  • 695
  • 7
  • 13
  • Are you sure that's entirely possible at compile time? It's not always possible to know what method is being referred to. Suppose your `Foo` was implementing an interface, with the `@Unsafe` annotation on just the `Foo` implementation. Then a client using the interface wouldn't show up. – sisyphus Dec 29 '15 at 22:30
  • You want to print warning regarding the line of the code that is calling the method, so you definitely can't avoid byte-code analysis. – user3707125 Dec 29 '15 at 22:39
  • @sisyphus, if a call to the interface method can be unsafe, then the interface method should be annotated `@Unsafe`. Thus, the compile-time processing is complete and sound. – mernst Dec 30 '15 at 16:15
  • @user3707125, byte-code analysis is not required because an annotation processor can do source-code analysis even within a method body. – mernst Dec 30 '15 at 16:17
  • @mernst that may or may not be the case. It very much depends on the annotation's meaning. There may be nothing wrong with the interface, only a specific implementation of it. – sisyphus Dec 30 '15 at 16:22
  • @sisyphus, if you want sound checking, then the meaning must be as I noted. If your point is that checking is not sound if the semantics of the annotation are broken, or the library codebase is incorrectly annotated, then I agree. – mernst Dec 30 '15 at 16:37
  • @mernst I think we're agreeing ;) My question is really how far `@Unsafe` is expected to be like `@Deprecated`. I would expect something like 'unsafe' to refer to an implementation whereas I would expect something like 'deprecated' to refer to an API. It seems strange to me to suggest that an API method could be 'unsafe' - there's nothing inherently unsafe about a method declaration on an interface. Perhaps that's what @greghmerrill wants and perhaps not. If not, if 'unsafe' is meant to be on implementations then I suspect it's not possible to detect all references statically. – sisyphus Dec 30 '15 at 16:49
  • 1
    For the purposes of this question, we may assume that `@Unsafe` behaves *exactly* like `@Deprecated`. I see your point @sisyphus, you are saying `@Unsafe` should act kind of like the "synchronized" modifier, i.e. you really must have the implementation in-hand to know whether it's synchronized or not. But to simplify, let's assume that this works exactly like `@Deprecated`, i.e. I can just look at the compiled type and not worry about runtime substitutions. – greghmerrill Jan 02 '16 at 15:11

3 Answers3

5

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
Community
  • 1
  • 1
greghmerrill
  • 695
  • 7
  • 13
  • 2
    Regarding licensing: Just as compiling your code with javac does not infect your code with the GPL, type-checking your code with the Checker Framework does not infect your code with the GPL. Running the Checker Framework during development does not affect your intellectual property or licensing. *However*, if you want to ship the Checker Framework as part of your product, then your product would have to be licensed under the GPL. Therefore, you might want to avoid the Checker Framework in that case. Anyway, your plugin is simple enough not to need the power of the whole Checker Framework. – mernst Jan 03 '16 at 20:00
  • 2
    Excellent, I see that you found the javac plugin mechanism before I managed to suggest it. :-) Unfortunately it's not documented very well, but it looks like you were able to use it to accomplish what you wanted. Note that the plugin interface is "non-standard." That is, it is not part of Java SE, but it is specific to Oracle JDK and OpenJDK. Other Java implementations might not have it. That's why the plugin option is prefixed with `-X`. – Stuart Marks Jan 08 '16 at 17:08
3

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.

mernst
  • 7,437
  • 30
  • 45
  • Thank you for the pointer to the Checker Framework. If I understand this correctly, then it is *not* possible for me to do this with the standard javax.annotation.processing.Processor and the JDK javac, as I see that Checker achieves its processing by providing its own substitue for javac (which I assume delegates to the "real" javac internally)? – greghmerrill Jan 02 '16 at 15:14
  • The Checker Framework can use the standard javac. Its javac has two effects: (1) add the Checker Framework .jar file to the classpath, and (2) recognize annotations in comments. If you don't care about annotations in comments and are willing to pass an extra command-line argument, then you don't need the Checker Framework compiler. – mernst Jan 03 '16 at 19:55
1

The AbstractProcessor below processes greghmerrill's @Unsafe annotation and emits warnings on method calls to @Unsafe annotated methods.

It is a slight modification of greghmerrills own answer, which was great, but I had some problems getting my IDEs incremental compiler (I am using Netbeans) to detect the warnings/errors etc emitted from the plugin - only those I printed from the processor was shown, though the behaviour was as expected when I ran 'mvn clean compile' ( I am using Maven). Whether this is due to some problem from my hand, or a points to difference between Plugins and AbstractProcessors/the phases of the compilation process, I do not know.

Anyway:

package com.hervian.annotationutils.target;

import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.util.*;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeInfo;
import java.util.Set;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;

@SupportedAnnotationTypes({"com.hervian.annotationutils.target.Unsafe"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class UnsafeAnnotationProcessor extends AbstractProcessor implements TaskListener {
Trees trees;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    trees = Trees.instance(processingEnv);
    JavacTask.instance(processingEnv).setTaskListener(this);
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //Process @Unsafe annotated methods if needed
    return true;
}

@Override public void finished(TaskEvent taskEvt) {
    if (taskEvt.getKind() == TaskEvent.Kind.ANALYZE) {
        taskEvt.getCompilationUnit().accept(new TreeScanner<Void, Void>() {
            @Override
            public Void visitMethodInvocation(MethodInvocationTree methodInv, Void v) {
                Element method = TreeInfo.symbol((JCTree) methodInv.getMethodSelect());
                Unsafe unsafe = method.getAnnotation(Unsafe.class);
                if (unsafe != null) {
                    JCTree jcTree = (JCTree) methodInv.getMethodSelect();
                    trees.printMessage(Diagnostic.Kind.WARNING, "Call to unsafe method.", jcTree, taskEvt.getCompilationUnit());
                }
                return super.visitMethodInvocation(methodInv, v);
            }
        }, null);
    }
}

@Override public void started(TaskEvent taskEvt) { } }

When using the annotation and making calls to the annotated method it will look like this: enter image description here

One needs to remember to add the fully qualified class name of the annotation processor to a META-INF/service file named javax.annotation.processing.Processor. This makes it available to the ServiceLoader framework.

Maven users having trouble with the com.sun** imports may find this answer from AnimeshSharma helpful.

I keep my annotation + annotation processor in a separate project. I had to disable annotation processing by adding the following to the pom:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <compilerArgument>-proc:none</compilerArgument>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

Using the annotation and having the processor do its work was simple: In my other project (the one where the screenshot of method foo() is from) I simply added a dependency to the project containing the annotation and processor.

Lastly it should be mentioned that I am new to AbstractProcessors and TaskListeners. I do, fx, not have an overview of the performance or robustness of the code. The goal was simply to "get it to work" and provide a stub for similar projects.

Community
  • 1
  • 1
Hervian
  • 1,066
  • 1
  • 12
  • 20