Reputation: 2031
I have several checks that are performed using java annotation processors but I would also like to do checks for types that are not annotated.
For example, if we suppose I have an annotation like @Responsible(name="xyz")
what is the best approach to hook into the compilation process to enforce that the annotation is present for all top level types.
With my current implementation I rely on two annotations, the expected one (Responsible) and a package level one. The later is used to 'fire' the annotation processor even if no expected annotation is present. Inside the fired annotation processor I then search & filter java files on disk (using passed arguments to compiler) to gather all the files that I'd like to be processed and filter them when the java file correspond to an annotated type the processor is handling. Doing so if someone commits a new file without specifying the annotation the build fail.
Isn't there a cleaner way to find the 'non annotated' types?
Upvotes: 8
Views: 1029
Reputation: 2031
To process all elements, I have missed that RoundEnvironment#getRootElements()
was the list of elements I was looking for if the Processor was declared to process everything, using *
.
So in order to check that all types were annotated with a @Responsible
I ended up with
@AutoService(Processor.class)
public class ResponsibleAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
List<ElementKind> typeKinds = Arrays.asList(ElementKind.ENUM, ElementKind.INTERFACE, ElementKind.CLASS);
// let's gather all types we are interrested in
Set<String> allElements = env.getRootElements()
.stream()
.filter(e -> typeKinds.contains(e.getKind())) // keep only interesting elements
.map(e -> e.asType().toString()) // get their full name
.collect(Collectors.toCollection(() -> new HashSet<>()));
Set<String> typesWithResponsible = new HashSet<>();
annotations.forEach(te -> {
if (Responsible.class.getName().equals(te.asType().toString())) {
// We collect elements with an already declared ownership
env.getElementsAnnotatedWith(te).forEach(e -> typesWithResponsible.add(e.asType().toString()));
}
});
allElements.removeAll(typesWithResponsible);
allElements.forEach(cname -> processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, cname + " must be annotated with @" + Responsible.class.getName() + " to declare a ownership"));
return false;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
// We want to process all elements
return Collections.singleton("*");
}
}
Upvotes: 4
Reputation: 4192
You don't have to rely on annotations to have your processor run. As explained in the documentation:
If there are no annotation types present, annotation processing still occurs but only universal processors which support processing "*" can claim the (empty) set of annotation types.
Your approach to finding classes is a bit clumsy. Instead you can rely on parent-child relationships between packages and classes: find out the name of top level package element, that contains interesting classes, and descend into that package (and/or it's subpackages) using Element#getEnclosedElements
. Alternatively you can locate a single class inside that package — then ascend to the topmost package using Element#getEnclosingElement
. A package object can be obtained by name using Elements#getPackageElement and class objects can be obtained by name using Elements#getTypeElement.
A lot less brittle, compared to manual interaction with files and directories, and won't break if source files are moved around or split between directories.
Note, that containment order is "a single package" -> "class" -> "method" -> ...
, the parent of every package is a package itself, not it's "parent" package (net.example
is not contained in net
).
Upvotes: 6