0

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 Answers1

0

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.

Jeffset
  • 841
  • 7
  • 15
  • Thanks for the answer. Regarding JSR-269, the code in my question uses the `javac` infrastructure heavily, not plain code modelling annotations from the JSR-269 only. Therefore I _would_ think I'm on a right way somehow with something like `((JCTree.JCVariableDecl) variableTree).mods.annotations = List.nil()` to remove annotations from any variable (no `if`s for brevity). Unfortunately, it still produces the code as if the annotation processor does nothing (at least what I can see in the `javap` output with the RuntimeVisibleAnnotations block in the end). – terrorrussia-keeps-killing Sep 03 '20 at 12:11
  • Now regarding bytecode processing. I had a quick look on those tools as well, but it requires more bytecode understanding, and that's what I would like to avoid at this stage. However, this _might_ be a good way, but unpacking each library before use (suppose no bytecode processing before `mvn install`) in the target dependency is pain in ass and complicates any Maven build extremely. I still need to use CF with verification enabled, but I would like it not to modify the code (or whatever it does?) so that further pre-Java-8 build tools would be compatible with what CF generates. – terrorrussia-keeps-killing Sep 03 '20 at 12:17
  • Now I'm thinking of why CF would generate anything... To be honest, I have no clue. But once I enable the subtyping processor, the downstream pre-Java-8 bytecode tools fail because they cannot handle what CF emits. I would have had peeked into the `javap` outputs before and after applying CF before claiming that, though, but several days later I'm really far from that. – terrorrussia-keeps-killing Sep 03 '20 at 12:20