Matthieu BROUILLARD
Matthieu BROUILLARD

Reputation: 2031

process java types without annotation

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

Answers (2)

Matthieu BROUILLARD
Matthieu BROUILLARD

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

user1643723
user1643723

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

Related Questions