How do I strip annotations from local variables in Java annotation processors?

484 views Asked by At

Due to a specific reason, I would like to use Checker Framework and its subtyping checker. To make this checker work I have to use ElementType.TYPE_PARAMETER and ElementType.TYPE_USE. However, I would like to remove them from local variables before compilation to class files.

For example, let's say I have the following code with custom @FirstName and @LastName (both must retain at the class level with RetentionPolicy.CLASS):

@FirstName String firstName = ...;
@LastName String lastName = ...;
...
firstName = lastName; // illegal, the error is generated by Checker Framework because the first name cannot be assigned to the last name

but for another reason, I would like to remove the annotations from the local variables "at" bytecode level as if the source code is just:

String firstName = ...;
String lastName = ...;
...
firstName = lastName; // totally fine and legal in Java

If I understand the way it can be accomplished, annotation processing is a way to go. So, if it's a right thing to do, then I'd have to chain some annotation processors in the following order:

  • org.checkerframework.common.subtyping.SubtypingChecker.
  • my custom "remove local variables annotations" annotation processor;

Well, diving into how javac works is an extreme challenge to me. What I have implemented so far is:

@SupportedOptions(RemoveLocalVariableAnnotationsProcessor.ANNOTATIONS_OPTION)
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public final class RemoveLocalVariableAnnotationsProcessor
        extends AbstractProcessor {

    private static final Pattern commaPattern = Pattern.compile(",");

    static final String ANNOTATIONS_OPTION = "RemoveLocalVariableAnnotationsProcessor.annotations";

    @Nonnull
    private Predicate<? super Class<? extends Annotation>> annotationClasses = clazz -> false;

    @Override
    public void init(@Nonnull final ProcessingEnvironment environment) {
        super.init(environment);
        final Messager messager = environment.getMessager();
        final Map<String, String> options = environment.getOptions();
        @Nullable
        final String annotationsOption = options.get(ANNOTATIONS_OPTION);
        if ( annotationsOption != null ) {
            annotationClasses = commaPattern.splitAsStream(annotationsOption)
                    .<Class<? extends Annotation>>flatMap(className -> {
                        try {
                            @SuppressWarnings("unchecked")
                            final Class<? extends Annotation> clazz = (Class<? extends Annotation>) Class.forName(className);
                            if ( !clazz.isAnnotation() ) {
                                messager.printMessage(Diagnostic.Kind.WARNING, "Not an annotation: " + className);
                                return Stream.empty();
                            }
                            return Stream.of(clazz);
                        } catch ( final ClassNotFoundException ex ) {
                            messager.printMessage(Diagnostic.Kind.WARNING, "Cannot find " + className);
                            return Stream.empty();
                        }
                    })
                    .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet))
                    ::contains;
        }
        final Trees trees = Trees.instance(environment);
        final JavacTask javacTask = JavacTask.instance(environment);
        javacTask.addTaskListener(new RemoverTaskListener(trees, messager));
    }

    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment environment) {
        // do nothing: ElementType.TYPE_USE and ElementType.TYPE_PARAMETER seem to be unable to be analyzed here
        return false;
    }

    private static final class RemoverTaskListener
            implements TaskListener {

        private final Trees trees;
        private final Messager messager;

        private RemoverTaskListener(final Trees trees, final Messager messager) {
            this.trees = trees;
            this.messager = messager;
        }

        @Override
        public void started(final TaskEvent taskEvent) {
            if ( taskEvent.getKind() == TaskEvent.Kind.ANALYZE ) {
                final TreeScanner<?, ?> remover = new Remover(trees, messager);
                remover.scan(taskEvent.getCompilationUnit(), null);
            }
        }

        @Override
        public void finished(final TaskEvent taskEvent) {
            // do nothing
        }

        private static final class Remover
                extends TreePathScanner<Void, Void> {

            private final Trees trees;
            private final Messager messager;

            private Remover(final Trees trees, final Messager messager) {
                this.trees = trees;
                this.messager = messager;
            }

            @Override
            public Void visitVariable(final VariableTree variableTree, final Void nothing) {
                super.visitVariable(variableTree, nothing);
                final Symbol symbol = (Symbol) trees.getElement(trees.getPath(getCurrentPath().getCompilationUnit(), variableTree));
                if ( !symbol.hasTypeAnnotations() || symbol.getKind() != ElementKind.LOCAL_VARIABLE ) {
                    return nothing;
                }
                final List<? extends AnnotationTree> annotationTrees = variableTree.getModifiers().getAnnotations();
                if ( annotationTrees.isEmpty() ) {
                    return nothing;
                }
                messager.printMessage(Diagnostic.Kind.WARNING, "TODO: " + symbol);
                for ( final AnnotationTree annotationTree : annotationTrees ) {
                    // TODO how to align AnnotationTree and java.lang.annotation.Annotation?
                    // TODO how to remove the annotation from the local variable?
                }
                return nothing;
            }

        }

    }

}

As you can see, it does not work as it's supposed to do.

What is a proper way of removing the annotations from local variables? I mean, how do I accomplish it? If it's possible, I would like to stick to javac annotation processors due to the Maven build integration specifics.

1

There are 1 answers

3
Jeffset On

As far as I know, you can't do it this way:

  • javac annotation processors (JSR-269) can't modify code. They can only observe it and generate new code that will be compiled together with the hand-written code. Thus, annotation processing is done in multiple rounds to allow compiler and other annotation processors see the newly generated code. The processing stops basically when no new code is generated at the end of the round.
  • This way the order of annotation processor invocations is not defined, and that's okay, because of multi-round compilation - it helps solving cyclic dependencies.

What you need is a bytecode rewriter (ASM library would do well). Such tools operate on resulting .class files after compilation is done. Yet again, AFAIK, annotation processing is embedded into compilation itself, so you won't be able to rewrite bytecode before Checker annotation processor sees it.

So, sadly, I don't see any solution, but to try and fork the Checker Framework and make it ignore annotations you want, if of course it doesn't already have options to turn certain validations off.