Copy JvmDescriptorUtils and KotlinMetadata utils
This is an almost 1-1 copy of JvmDescriptorUtils and kotlin metadata utils from
room:compiler to xprocessing.
Once room is migrated, they'll be deleted from Room.
Right now these classes are not used but they'll be used in a followup CL when I
add support for executable elements.
Bug: 160322705
Bug: 160323720
Test: compiler-xprocessing tests
Change-Id: I12c3468ac7a69ba0b964700bc8f8343dcd263552
diff --git a/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/JvmDescriptorUtils.kt b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/JvmDescriptorUtils.kt
new file mode 100644
index 0000000..cabec27
--- /dev/null
+++ b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/JvmDescriptorUtils.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.processing.javac.kotlin
+
+import com.google.auto.common.MoreTypes
+import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.NestingKind
+import javax.lang.model.element.QualifiedNameable
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.ArrayType
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.ErrorType
+import javax.lang.model.type.ExecutableType
+import javax.lang.model.type.IntersectionType
+import javax.lang.model.type.NoType
+import javax.lang.model.type.NullType
+import javax.lang.model.type.PrimitiveType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.type.TypeVariable
+import javax.lang.model.type.UnionType
+import javax.lang.model.type.WildcardType
+import javax.lang.model.util.AbstractTypeVisitor8
+
+/**
+ * Returns the method descriptor of this [ExecutableElement].
+ *
+ * For reference, see the [JVM specification, section 4.3.2](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.2)
+ */
+internal fun VariableElement.descriptor() = "$simpleName:${asType().descriptor()}"
+
+/**
+ * Returns the method descriptor of this [ExecutableElement].
+ *
+ * For reference, see the [JVM specification, section 4.3.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.3)
+ */
+internal fun ExecutableElement.descriptor() =
+ "$simpleName${MoreTypes.asExecutable(asType()).descriptor()}"
+
+/**
+ * Returns the name of this [TypeElement] in its "internal form".
+ *
+ * For reference, see the [JVM specification, section 4.2](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.2).
+ */
+internal val Element.internalName: String
+ get() = when (this) {
+ is TypeElement ->
+ when (nestingKind) {
+ NestingKind.TOP_LEVEL ->
+ qualifiedName.toString().replace('.', '/')
+ NestingKind.MEMBER ->
+ enclosingElement.internalName + "$" + simpleName
+ NestingKind.LOCAL, NestingKind.ANONYMOUS ->
+ error("Unsupported nesting $nestingKind")
+ else ->
+ error("Unsupported, nestingKind == null")
+ }
+ is QualifiedNameable -> qualifiedName.toString().replace('.', '/')
+ else -> simpleName.toString()
+ }
+
+@Suppress("unused")
+internal val NoType.descriptor: String
+ get() = "V"
+
+internal val DeclaredType.descriptor: String
+ get() = "L" + asElement().internalName + ";"
+
+internal val PrimitiveType.descriptor: String
+ get() = when (this.kind) {
+ TypeKind.BYTE -> "B"
+ TypeKind.CHAR -> "C"
+ TypeKind.DOUBLE -> "D"
+ TypeKind.FLOAT -> "F"
+ TypeKind.INT -> "I"
+ TypeKind.LONG -> "J"
+ TypeKind.SHORT -> "S"
+ TypeKind.BOOLEAN -> "Z"
+ else -> error("Unknown primitive type $this")
+ }
+
+internal fun TypeMirror.descriptor(): String = accept(JvmDescriptorTypeVisitor, Unit)
+
+@Suppress("unused")
+internal fun WildcardType.descriptor(): String = ""
+
+// The erasure of a type variable is the erasure of its leftmost bound. - JVM Spec Sec 4.6
+internal fun TypeVariable.descriptor(): String = this.upperBound.descriptor()
+
+// For a type variable with multiple bounds: "the erasure of a type variable is determined by
+// the first type in its bound" - JVM Spec Sec 4.4
+internal fun IntersectionType.descriptor(): String =
+ this.bounds[0].descriptor()
+
+internal fun ArrayType.descriptor(): String =
+ "[" + componentType.descriptor()
+
+internal fun ExecutableType.descriptor(): String {
+ val parameterDescriptors =
+ parameterTypes.joinToString(separator = "") { it.descriptor() }
+ val returnDescriptor = returnType.descriptor()
+ return "($parameterDescriptors)$returnDescriptor"
+}
+
+/**
+ * When applied over a type, it returns either:
+ * + a "field descriptor", for example: `Ljava/lang/Object;`
+ * + a "method descriptor", for example: `(Ljava/lang/Object;)Z`
+ *
+ * The easiest way to use this is through [TypeMirror.descriptor]
+ *
+ * For reference, see the [JVM specification, section 4.3](http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3).
+ */
+@Suppress("DEPRECATION")
+internal object JvmDescriptorTypeVisitor : AbstractTypeVisitor8<String, Unit>() {
+
+ override fun visitNoType(t: NoType, u: Unit): String = t.descriptor
+
+ override fun visitDeclared(t: DeclaredType, u: Unit): String = t.descriptor
+
+ override fun visitPrimitive(t: PrimitiveType, u: Unit): String = t.descriptor
+
+ override fun visitArray(t: ArrayType, u: Unit): String = t.descriptor()
+
+ override fun visitWildcard(t: WildcardType, u: Unit): String = t.descriptor()
+
+ override fun visitExecutable(t: ExecutableType, u: Unit): String = t.descriptor()
+
+ override fun visitTypeVariable(t: TypeVariable, u: Unit): String = t.descriptor()
+
+ override fun visitNull(t: NullType, u: Unit): String = visitUnknown(t, u)
+
+ override fun visitError(t: ErrorType, u: Unit): String = visitUnknown(t, u)
+
+ override fun visitIntersection(t: IntersectionType, u: Unit) = t.descriptor()
+
+ override fun visitUnion(t: UnionType, u: Unit) = visitUnknown(t, u)
+
+ override fun visitUnknown(t: TypeMirror, u: Unit): String = error("Unsupported type $t")
+}
\ No newline at end of file
diff --git a/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/KotlinClassMetadataUtils.kt b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/KotlinClassMetadataUtils.kt
new file mode 100644
index 0000000..fefd8e0
--- /dev/null
+++ b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/KotlinClassMetadataUtils.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.processing.javac.kotlin
+
+import kotlinx.metadata.ClassName
+import kotlinx.metadata.Flag
+import kotlinx.metadata.Flags
+import kotlinx.metadata.KmClassVisitor
+import kotlinx.metadata.KmConstructorExtensionVisitor
+import kotlinx.metadata.KmConstructorVisitor
+import kotlinx.metadata.KmExtensionType
+import kotlinx.metadata.KmFunctionExtensionVisitor
+import kotlinx.metadata.KmFunctionVisitor
+import kotlinx.metadata.KmValueParameterVisitor
+import kotlinx.metadata.jvm.JvmConstructorExtensionVisitor
+import kotlinx.metadata.jvm.JvmFunctionExtensionVisitor
+import kotlinx.metadata.jvm.JvmMethodSignature
+import kotlinx.metadata.jvm.KotlinClassMetadata
+
+/**
+ * Represents the kotlin metadata of a function
+ */
+internal data class KmFunction(
+ val descriptor: String,
+ private val flags: Flags,
+ val parameters: List<KmValueParameter>
+) {
+ fun isSuspend() = Flag.Function.IS_SUSPEND(flags)
+}
+
+/**
+ * Represents the kotlin metadata of a constructor
+ */
+internal data class KmConstructor(
+ val descriptor: String,
+ private val flags: Flags,
+ val parameters: List<KmValueParameter>
+) {
+ fun isPrimary() = Flag.Constructor.IS_PRIMARY(flags)
+}
+
+/**
+ * Represents the kotlin metadata of a parameter
+ */
+internal data class KmValueParameter(val name: String, private val flags: Flags)
+
+internal fun KotlinClassMetadata.Class.readFunctions(): List<KmFunction> =
+ mutableListOf<KmFunction>().apply { accept(FunctionReader(this)) }
+
+private class FunctionReader(val result: MutableList<KmFunction>) : KmClassVisitor() {
+ override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor? {
+ return object : KmFunctionVisitor() {
+
+ lateinit var descriptor: String
+ val parameters = mutableListOf<KmValueParameter>()
+
+ override fun visitValueParameter(
+ flags: Flags,
+ name: String
+ ): KmValueParameterVisitor? {
+ parameters.add(KmValueParameter(name, flags))
+ return super.visitValueParameter(flags, name)
+ }
+
+ override fun visitExtensions(type: KmExtensionType): KmFunctionExtensionVisitor? {
+ if (type != JvmFunctionExtensionVisitor.TYPE) {
+ error("Unsupported extension type: $type")
+ }
+ return object : JvmFunctionExtensionVisitor() {
+ override fun visit(signature: JvmMethodSignature?) {
+ descriptor = signature!!.asString()
+ }
+ }
+ }
+
+ override fun visitEnd() {
+ result.add(KmFunction(descriptor, flags, parameters))
+ }
+ }
+ }
+}
+
+internal fun KotlinClassMetadata.Class.readConstructors(): List<KmConstructor> =
+ mutableListOf<KmConstructor>().apply { accept(ConstructorReader(this)) }
+
+private class ConstructorReader(val result: MutableList<KmConstructor>) : KmClassVisitor() {
+ override fun visitConstructor(flags: Flags): KmConstructorVisitor? {
+ return object : KmConstructorVisitor() {
+
+ lateinit var descriptor: String
+ val parameters = mutableListOf<KmValueParameter>()
+
+ override fun visitValueParameter(
+ flags: Flags,
+ name: String
+ ): KmValueParameterVisitor? {
+ parameters.add(KmValueParameter(name, flags))
+ return super.visitValueParameter(flags, name)
+ }
+
+ override fun visitExtensions(type: KmExtensionType): KmConstructorExtensionVisitor? {
+ if (type != JvmConstructorExtensionVisitor.TYPE) {
+ error("Unsupported extension type: $type")
+ }
+ return object : JvmConstructorExtensionVisitor() {
+ override fun visit(signature: JvmMethodSignature?) {
+ descriptor = signature!!.asString()
+ }
+ }
+ }
+
+ override fun visitEnd() {
+ result.add(KmConstructor(descriptor, flags, parameters))
+ }
+ }
+ }
+}
+
+internal fun KotlinClassMetadata.Class.isObject(): Boolean = ObjectReader().let {
+ this@isObject.accept(it)
+ it.isObject
+}
+
+private class ObjectReader() : KmClassVisitor() {
+ var isObject: Boolean = false
+ override fun visit(flags: Flags, name: ClassName) {
+ isObject = Flag.Class.IS_OBJECT(flags)
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/KotlinMetadataElement.kt b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/KotlinMetadataElement.kt
new file mode 100644
index 0000000..5e822a2
--- /dev/null
+++ b/room/compiler-xprocessing/src/main/java/androidx/room/processing/javac/kotlin/KotlinMetadataElement.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.processing.javac.kotlin
+
+import kotlinx.metadata.jvm.KotlinClassHeader
+import kotlinx.metadata.jvm.KotlinClassMetadata
+import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
+
+/**
+ * Utility class for processors that wants to run kotlin specific code.
+ */
+internal class KotlinMetadataElement(
+ val element: Element,
+ private val classMetadata: KotlinClassMetadata.Class
+) {
+
+ private val functionList: List<KmFunction> by lazy { classMetadata.readFunctions() }
+ private val constructorList: List<KmConstructor> by lazy { classMetadata.readConstructors() }
+
+ private val ExecutableElement.descriptor: String
+ get() = descriptor()
+
+ /**
+ * Returns the parameter names of the function or constructor if all have names embedded in the
+ * metadata.
+ */
+ fun getParameterNames(method: ExecutableElement): List<String>? {
+ val methodSignature = method.descriptor
+ val paramList =
+ functionList.firstOrNull { it.descriptor == methodSignature }?.parameters
+ ?: constructorList.firstOrNull { it.descriptor == methodSignature }?.parameters
+ return paramList?.map { it.name }
+ }
+
+ /**
+ * Finds the primary constructor descriptor of the class.
+ */
+ fun findPrimaryConstructorSignature() = constructorList.first { it.isPrimary() }.descriptor
+
+ /**
+ * Checks if a method is a suspend function.
+ */
+ fun isSuspendFunction(method: ExecutableElement) = functionList.firstOrNull {
+ it.descriptor == method.descriptor
+ }?.isSuspend() ?: false
+
+ fun isObject(): Boolean = classMetadata.isObject()
+
+ companion object {
+ /**
+ * Creates a [KotlinMetadataElement] for the given element if it contains Kotlin metadata,
+ * otherwise this method returns null.
+ *
+ * Usually the [element] passed must represent a class. For example, if kotlin metadata is
+ * desired for a method, then the containing class should be used as parameter.
+ */
+ fun createFor(element: Element): KotlinMetadataElement? {
+ val metadata = getMetadataAnnotation(element)?.run {
+ KotlinClassHeader(
+ kind = kind,
+ metadataVersion = metadataVersion,
+ bytecodeVersion = bytecodeVersion,
+ data1 = data1,
+ data2 = data2,
+ extraString = extraString,
+ packageName = packageName,
+ extraInt = extraInt
+ ).let {
+ // TODO: Support more metadata kind (file facade, synthetic class, etc...)
+ KotlinClassMetadata.read(it) as? KotlinClassMetadata.Class
+ }
+ }
+ return if (metadata != null) {
+ KotlinMetadataElement(element, metadata)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Search for Kotlin's Metadata annotation across the element's hierarchy.
+ */
+ private fun getMetadataAnnotation(element: Element?): Metadata? =
+ if (element != null) {
+ element.getAnnotation(Metadata::class.java)
+ ?: getMetadataAnnotation(element.enclosingElement)
+ } else {
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-xprocessing/src/test/java/androidx/room/processing/javac/kotlin/JvmDescriptorUtilsTest.kt b/room/compiler-xprocessing/src/test/java/androidx/room/processing/javac/kotlin/JvmDescriptorUtilsTest.kt
new file mode 100644
index 0000000..4bb6c67
--- /dev/null
+++ b/room/compiler-xprocessing/src/test/java/androidx/room/processing/javac/kotlin/JvmDescriptorUtilsTest.kt
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.processing.javac.kotlin
+
+import com.google.auto.common.MoreElements
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.element.ElementKind.CONSTRUCTOR
+import javax.lang.model.element.ElementKind.FIELD
+import javax.lang.model.element.ElementKind.METHOD
+import javax.lang.model.element.TypeElement
+import javax.tools.JavaFileObject
+
+@RunWith(JUnit4::class)
+class JvmDescriptorUtilsTest {
+
+ private val describeAnnotation =
+ """
+ package androidx.room.test;
+
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Target;
+
+ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+ public @interface Describe { }
+ """.toJFO("androidx.room.test.Describe")
+
+ @Test
+ fun descriptor_method_simple() {
+ singleRun(
+ """
+ package androidx.room.test;
+
+ public class DummyClass {
+ @Describe
+ public void emptyMethod() {
+ }
+ }
+ """.toJFO("androidx.room.test.DummyClass")
+ ) { descriptors ->
+ assertThat(descriptors.first())
+ .isEqualTo("emptyMethod()V")
+ }
+ }
+
+ @Test
+ fun descriptor_field() {
+ singleRun(
+ """
+ package androidx.room.test;
+
+ import java.util.List;
+
+ class DummyClass<T> {
+ @Describe
+ int field1;
+
+ @Describe
+ String field2;
+
+ @Describe
+ T field3;
+
+ @Describe
+ List<String> field4;
+ }
+ """.toJFO("androidx.room.test.DummyClass")
+ ) { descriptors ->
+ assertThat(descriptors).isEqualTo(
+ setOf(
+ "field1:I",
+ "field2:Ljava/lang/String;",
+ "field3:Ljava/lang/Object;",
+ "field4:Ljava/util/List;"
+ )
+ )
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun descriptor_method_erasured() {
+ singleRun(
+ """
+ package androidx.room.test;
+
+ import java.util.ArrayList;
+ import java.util.Collection;
+ import java.util.List;
+ import java.util.Map;
+
+ class DummyClass<T> {
+ @Describe
+ void method1(T something) { }
+
+ @Describe
+ T method2() { return null; }
+
+ @Describe
+ List<? extends String> method3() { return null; }
+
+ @Describe
+ Map<T, String> method4() { return null; }
+
+ @Describe
+ ArrayList<Map<T, String>> method5() { return null; }
+
+ @Describe
+ static <I, O extends I> O method6(I input) { return null; }
+
+ @Describe
+ static <I, O extends String> O method7(I input) { return null; }
+
+ @Describe
+ static <P extends Collection & Comparable> P method8() { return null; }
+
+ @Describe
+ static <P extends String & List<Character>> P method9() { return null; }
+ }
+ """.toJFO("androidx.room.test.DummyClass")
+ ) { descriptors ->
+ assertThat(descriptors).isEqualTo(
+ setOf(
+ "method1(Ljava/lang/Object;)V",
+ "method2()Ljava/lang/Object;",
+ "method3()Ljava/util/List;",
+ "method4()Ljava/util/Map;",
+ "method5()Ljava/util/ArrayList;",
+ "method6(Ljava/lang/Object;)Ljava/lang/Object;",
+ "method7(Ljava/lang/Object;)Ljava/lang/String;",
+ "method8()Ljava/util/Collection;",
+ "method9()Ljava/lang/String;"
+ )
+ )
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun descriptor_method_primitiveParams() {
+ singleRun(
+ """
+ package androidx.room.test;
+
+ class DummyClass {
+ @Describe
+ void method1(boolean yesOrNo, int number) { }
+
+ @Describe
+ byte method2(char letter) { return 0; }
+
+ @Describe
+ void method3(double realNumber1, float realNummber2) { }
+
+ @Describe
+ void method4(long bigNumber, short littlerNumber) { }
+ }
+ """.toJFO("androidx.room.test.DummyClass")
+ ) { descriptors ->
+ assertThat(descriptors)
+ .isEqualTo(setOf("method1(ZI)V", "method2(C)B", "method3(DF)V", "method4(JS)V"))
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun descriptor_method_classParam_javaTypes() {
+ singleRun(
+ """
+ package androidx.room.test;
+
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.Map;
+
+ class DummyClass {
+ @Describe
+ void method1(Object something) { }
+
+ @Describe
+ Object method2() { return null; }
+
+ @Describe
+ List<String> method3(ArrayList<Integer> list) { return null; }
+
+ @Describe
+ Map<String, Object> method4() { return null; }
+ }
+ """.toJFO("androidx.room.test.DummyClass")
+ ) { descriptors ->
+ assertThat(descriptors).isEqualTo(
+ setOf(
+ "method1(Ljava/lang/Object;)V",
+ "method2()Ljava/lang/Object;",
+ "method3(Ljava/util/ArrayList;)Ljava/util/List;",
+ "method4()Ljava/util/Map;"
+ )
+ )
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun descriptor_method_classParam_testClass() {
+ val extraJfo =
+ """
+ package androidx.room.test;
+
+ class DataClass { }
+ """.toJFO("androidx.room.test.DataClass")
+
+ singleRun(
+ """
+ package androidx.room.test;
+
+ class DummyClass {
+ @Describe
+ void method1(DataClass data) { }
+
+ @Describe
+ DataClass method2() { return null; }
+ }
+ """.toJFO("androidx.room.test.DummyClass"), extraJfo
+ ) { descriptors ->
+ assertThat(descriptors).isEqualTo(
+ setOf(
+ "method1(Landroidx/room/test/DataClass;)V",
+ "method2()Landroidx/room/test/DataClass;"
+ )
+ )
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun descriptor_method_classParam_innerTestClass() {
+ val extraJfo =
+ """
+ package androidx.room.test;
+
+ class DataClass {
+
+ class MemberInnerData { }
+
+ static class StaticInnerData { }
+
+ enum EnumData {
+ VALUE1, VALUE2
+ }
+ }
+ """.toJFO("androidx.room.test.DataClass")
+
+ singleRun(
+ """
+ package androidx.room.test;
+
+ class DummyClass {
+ @Describe
+ void method1(DataClass.MemberInnerData data) { }
+
+ @Describe
+ void method2(DataClass.StaticInnerData data) { }
+
+ @Describe
+ void method3(DataClass.EnumData enumData) { }
+
+ @Describe
+ DataClass.StaticInnerData method4() { return null; }
+ }
+ """.toJFO("androidx.room.test.DummyClass"), extraJfo
+ ) { descriptors ->
+ assertThat(descriptors).isEqualTo(
+ setOf(
+ "method1(Landroidx/room/test/DataClass\$MemberInnerData;)V",
+ "method2(Landroidx/room/test/DataClass\$StaticInnerData;)V",
+ "method3(Landroidx/room/test/DataClass\$EnumData;)V",
+ "method4()Landroidx/room/test/DataClass\$StaticInnerData;"
+ )
+ )
+ }.compilesWithoutError()
+ }
+
+ @Test
+ fun descriptor_method_arrayParams() {
+ val extraJfo =
+ """
+ package androidx.room.test;
+
+ class DataClass { }
+ """.toJFO("androidx.room.test.DataClass")
+
+ singleRun(
+ """
+ package androidx.room.test;
+
+ class DummyClass {
+ @Describe
+ void method1(DataClass[] data) { }
+
+ @Describe
+ DataClass[] method2() { return null; }
+
+ @Describe
+ void method3(int[] array) { }
+
+ @Describe
+ void method4(int... array) { }
+ }
+ """.toJFO("androidx.room.test.DummyClass"), extraJfo
+ ) { descriptors ->
+ assertThat(descriptors).isEqualTo(
+ setOf(
+ "method1([Landroidx/room/test/DataClass;)V",
+ "method2()[Landroidx/room/test/DataClass;",
+ "method3([I)V",
+ "method4([I)V"
+ )
+ )
+ }.compilesWithoutError()
+ }
+
+ private fun String.toJFO(qName: String): JavaFileObject =
+ JavaFileObjects.forSourceLines(qName, this)
+
+ @Suppress("UnstableApiUsage")
+ private fun singleRun(
+ vararg jfo: JavaFileObject,
+ handler: (Set<String>) -> Unit
+ ): CompileTester {
+ return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+ .that(listOf(describeAnnotation) + jfo)
+ .processedWith(object : AbstractProcessor() {
+ override fun process(
+ annotations: Set<TypeElement>,
+ roundEnv: RoundEnvironment
+ ): Boolean {
+ roundEnv.getElementsAnnotatedWith(annotations.first()).map { element ->
+ when (element.kind) {
+ FIELD -> MoreElements.asVariable(element).descriptor()
+ METHOD, CONSTRUCTOR -> MoreElements.asExecutable(element).descriptor()
+ else -> error("Unsupported element to describe.")
+ }
+ }.toSet().let(handler)
+ return true
+ }
+
+ override fun getSupportedOptions(): Set<String> {
+ return setOf("androidx.room.test.Describe")
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-xprocessing/src/test/java/androidx/room/processing/javac/kotlin/KotlinMetadataElementTest.kt b/room/compiler-xprocessing/src/test/java/androidx/room/processing/javac/kotlin/KotlinMetadataElementTest.kt
new file mode 100644
index 0000000..d60932e
--- /dev/null
+++ b/room/compiler-xprocessing/src/test/java/androidx/room/processing/javac/kotlin/KotlinMetadataElementTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.processing.javac.kotlin
+
+import androidx.room.processing.javac.JavacProcessingEnv
+import androidx.room.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.AssumptionViolatedException
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.TypeElement
+import javax.lang.model.util.ElementFilter
+import kotlin.reflect.KClass
+
+@RunWith(JUnit4::class)
+class KotlinMetadataElementTest {
+
+ @Test
+ fun getParameterNames() {
+ simpleRun { processingEnv ->
+ val (testClassElement, metadataElement) = getMetadataElement(
+ processingEnv,
+ TestData::class
+ )
+ assertThat(testClassElement.getDeclaredMethods()
+ .first { it.simpleName.toString() == "functionWithParams" }
+ .let { metadataElement.getParameterNames(it) }
+ ).isEqualTo(
+ listOf("param1", "yesOrNo", "number")
+ )
+ }
+ }
+
+ @Test
+ fun findPrimaryConstructorSignature() {
+ simpleRun { invocation ->
+ val (testClassElement, metadataElement) = getMetadataElement(
+ invocation,
+ TestData::class
+ )
+ assertThat(
+ testClassElement.getConstructors().map {
+ val desc = it.descriptor()
+ desc to (desc == metadataElement.findPrimaryConstructorSignature())
+ }
+ ).containsExactly(
+ "<init>(Ljava/lang/String;)V" to true,
+ "<init>()V" to false
+ )
+ }
+ }
+
+ @Test
+ fun isSuspendFunction() {
+ simpleRun { invocation ->
+ val (testClassElement, metadataElement) = getMetadataElement(
+ invocation,
+ TestData::class
+ )
+ assertThat(testClassElement.getDeclaredMethods().map {
+ it.simpleName.toString() to metadataElement.isSuspendFunction(it)
+ }).containsExactly(
+ "emptyFunction" to false,
+ "suspendFunction" to true,
+ "functionWithParams" to false,
+ "getConstructorParam" to false
+ )
+ }
+ }
+
+ @Test
+ fun isObject() {
+ simpleRun { invocation ->
+ val (_, objectTypeMetadata) = getMetadataElement(invocation, ObjectType::class)
+ assertThat(objectTypeMetadata.isObject()).isTrue()
+ val (_, testDataMetadata) = getMetadataElement(invocation, TestData::class)
+ assertThat(testDataMetadata.isObject()).isFalse()
+ }
+ }
+
+ private fun TypeElement.getDeclaredMethods() = ElementFilter.methodsIn(enclosedElements)
+
+ private fun TypeElement.getConstructors() = ElementFilter.constructorsIn(enclosedElements)
+
+ private fun simpleRun(
+ handler: (ProcessingEnvironment) -> Unit
+ ) {
+ runProcessorTest {
+ if (it.processingEnv !is JavacProcessingEnv) {
+ throw AssumptionViolatedException("This test only works for java/kapt compilation")
+ }
+ handler(it.processingEnv.delegate)
+ }
+ }
+
+ private fun getMetadataElement(processingEnv: ProcessingEnvironment, klass: KClass<*>) =
+ processingEnv.elementUtils.getTypeElement(klass.java.canonicalName).let {
+ it to KotlinMetadataElement.createFor(it)!!
+ }
+
+ @Suppress("unused")
+ private class TestData(val constructorParam: String) {
+
+ constructor() : this("anything")
+
+ fun emptyFunction() {}
+
+ @Suppress("RedundantSuspendModifier")
+ suspend fun suspendFunction() {
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun functionWithParams(param1: String, yesOrNo: Boolean, number: Int) {
+ }
+ }
+
+ object ObjectType {
+ val foo: String = ""
+ }
+}
\ No newline at end of file