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.
As far as I know, you can't do it this way:
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.