David Leppik
David Leppik

Reputation: 3304

detecting singletons in kapt

I'm writing an annotation processor for Kotlin which needs to know how to call a method, i.e. whether or not a class needs to be constructed.

The following works for Java code, for values, and for @JvmStatic-labeled code, but not for Kotlin object Foo {} singletons:

import javax.lang.model.element.Element
import javax.lang.model.element.Modifier

// Fails if el is in a singleton
fun isStatic(el: Element) = el.modifiers.contains(Modifier.STATIC)

What's the best way to detect if a method can be called without constructing a class?

Upvotes: 2

Views: 181

Answers (2)

David Leppik
David Leppik

Reputation: 3304

This isn't a perfect solution, but this is what I came up with. It checks for kotlin.Metadata in the binary data to see if it's a Kotlin class, and then uses heuristics to tease out whether or not it is static.

A similar technique is required if you want to find a main file, i.e. a file which can be run from the command line.

fun isKotlinClass(el: TypeElement)
        = el.annotationMirrors.any { it.annotationType.toString() == "kotlin.Metadata" }

/** Check for Java static or Kotlin singleton.
 * An imperfect heuristic: if not static, checks for a static INSTANCE field. */
private fun isStatic(element: Element): Boolean {
    if (element.modifiers.contains(Modifier.STATIC)) return true
    else {
        val parent = element.enclosingElement
        if (parent is TypeElement && isKotlinClass(parent)) {
            val instances = parent.enclosedElements
                    .filter { "INSTANCE" == it.simpleName.toString() }
                    .filter { it.modifiers.contains(Modifier.STATIC) }
                    .filter { it.kind.isField }
            return instances.isNotEmpty()
        }
        return false
    }
}

Upvotes: 1

Demigod
Demigod

Reputation: 5635

So internally object in Kotlin is a simple Singleton. The difference is that it is managed by the Kotlin language itself. Such class still needs to be instantiated in order to call its functions. Just this instantiation is made by Kotlin since objects have private constructor and a static INSTANCE field that holds its single instance. Let's take a look at this example. I have object A defined like this:

object A {

    fun a() {
    }
}

If we will look at the java bytecode and will convert it to java we will have this:

import kotlin.Metadata;

@Metadata(
   mv = {1, 1, 10},
   bv = {1, 0, 2},
   k = 1,
   d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\bÆ\u0002\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
   d2 = {"Lcom/telenor/self_service/app/A;", "", "()V", "a", "", "production sources for module app"}
)
public final class A {
   public static final A INSTANCE;

   public final void a() {
   }

   static {
      A var0 = new A();
      INSTANCE = var0;
   }
}

As you can see reading only class body we can't understand if it is an object or not, cause if we will create similar class using Java only it will behave a class (not object).

public final class B {
    public static final B INSTANCE;

    public final void b() {
    }

    static {
        B var0 = new B();
        INSTANCE = var0;
    }
}

But, when we try to create a new instance from Java like this, it will say that A has private constructor and cannot be created.

    new A();
    new B();

So the difference here is somehow made with Kotlin @Metadata annotation which defines this difference.

As a solution you can either check the INSTANCE static field, or you can read Kotlin metadata somehow :)

Upvotes: 0

Related Questions