1

Overview

I'm trying to build my first annotation processor and it's going pretty well. I am creating a code generating processor that basically generates SharedPreferences for a defined interface. My current annotations are SharedPrefs and Default. @SharedPrefs informs the processor that the file is an interface and needs a generated prefs file. @Default is what I annotated some properties in the interface as in order to let the processor know what to set the default value to. There can be multiple files defined as @SharedPrefs.

Implementation

I currently use the following code to get a list of files annotated with @SharedPrefs and the @Defaults:

roundEnv.getElementsAnnotatedWith(SharedPrefs::class.java)?.forEach { element ->
  ...
  roundEnv.getElementsAnnotatedWith(Default::class.java)?.forEach {
    ...
  }
}

@SharedPrefs:

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class SharedPrefs(val showTraces: Boolean = false)

@Default:

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.PROPERTY)
annotation class Default(val defaultValue: String = "[null]")

In Use:

@SharedPrefs
interface InstanceSettings {
    var wifiPassword: String
    @Default("20")
    var count: Int
}

Problem

As is, the inner forEach is returning all properties from all files annotated with @Default. The code generation works fine, but this doesn't seem like the best way forward. Is there a way to get just the properties w/in the current @SharedPrefs class that I'm processing? For instance, something like:

roundEnv.getElementsAnnotatedWith(SharedPrefs::class.java)?.forEach { element ->
  ...
  element.methodToGetAnnotatedProperties(Default::class.java)?.forEach {
    ...
  }
}

* EDIT *

I found that, for methods that I annotate

@SomeAnnotation
fun someMethod()

I can loop through the element.enclosingElements and find if it has an annotation using enclosingElement.getAnnotation(<SomeAnnotation::class.java>). Unfortunately, and correct me if I'm wrong here, I can't annotate interface variables with an annotation with AnnotationTarget.FIELD since they don't have a backing field since it's an interface and they're not implemented. Therefore, I'm using AnnotationTarget.PROPERTY. When looping through the enclosing elements, all of the variables show up as getters and setters. For the example above for InstanceSettings, I get getWifiPassword, setWifiPassword, getCount and setCount. I do not get an element for just wifiPassword or count. Calling getAnnotation(Default::class.java) will always return null on these since they are generated.

Also, any other resources on annotation processing that anyone knows of would be a great addition in the comments. Thanks!

James B
  • 447
  • 3
  • 15

2 Answers2

0

Similar question in java was asked here

Here is how you can create an extension function which will do job for you!

roundEnv.getElementsAnnotatedWith(SharedPrefs::class.java)?.forEach { element ->
    ...
    roundEnv.findNestedElements(SharedPrefs::class, Default::class)?.forEach {
        ...
    }
}

fun <T> RoundEnvironment.findNestedElements(parent: KClass<*>, child: KClass<T>): List<Element>? {
    val childs = this.getElementsAnnotatedWith(child.java)
    val list = ArrayList<Element>()
    for (element in childs)
    {
        if (element.getEnclosingElement().getAnnotation(parent.java) != null)
        {
            list.add(element)
        }
    }
    return if(list.isEmpty()) null else list
}
Animesh Sahu
  • 7,445
  • 2
  • 21
  • 49
  • the line `element.getEnclosingElement().getAnnotation(parent.java)` returns `DefaultImpls`. I _believe_ this is because the `getElementsAnnotatedWith(Default::class.java)` is returning the generated getters/setters and not a backing field. See my edit above – James B Mar 18 '20 at 17:16
  • Sorry, minor correction to the comment above : `element.getEnclosingElement()` returns `DefaultImpls`. Calling `getAnnotation(parent.java)` returns null (as expected). – James B Mar 19 '20 at 15:04
0

So I think I've found the solution:

For the example of InterfaceSettings:

@SharedPrefs
interface InstanceSettings {
    var wifiPassword: String
    @Default("20")
    var count: Int
}

The simplified generated Java code is :

public interface InstanceSettings {
   @NotNull
   String getWifiPassword();

   void setWifiPassword(@NotNull String var1);

   int getCount();

   void setCount(int var1);

   public static final class DefaultImpls {
      public static void count$annotations() {
      }
   }
}

The element returned when calling roundEnv.getElementsAnnotatedWith(Default::class.java) is count$annotations. So when I call getEnclosingElement(), DefaultImpls is returned. If I call getEnclosingElement() again, InstanceSettings is returned. I actually call getSimpleName() on that result and compare it with the className from the item annotated with @SharedPrefs to see if it is a child:

roundEnv.getElementsAnnotatedWith(SharedPrefs::class.java)?.forEach { element ->
  val className = element.simpleName.toString()
  val defaults = roundEnv.getElementsAnnotatedWith(Default::class.java)?.filter {
    // it.enclosingElement -> DefaultImpls
    // DefaultImpls.enclosingElement -> com.mypackage.InstanceSettings
    it.enclosingElement.enclosingElement.simpleName == className
  }
}
James B
  • 447
  • 3
  • 15