Merge "Support TypeConverters on kotlin objects" into androidx-master-dev am: 826a5d6f25

Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/1323097

Change-Id: I0f0bb79ca964d2dcef8dc6684e8b65a8aaced70c
diff --git a/room/compiler/src/main/kotlin/androidx/room/kotlin/KotlinClassMetadataUtils.kt b/room/compiler/src/main/kotlin/androidx/room/kotlin/KotlinClassMetadataUtils.kt
index 9b2071a..955c5f1 100644
--- a/room/compiler/src/main/kotlin/androidx/room/kotlin/KotlinClassMetadataUtils.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/kotlin/KotlinClassMetadataUtils.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.kotlin
 
+import kotlinx.metadata.ClassName
 import kotlinx.metadata.Flag
 import kotlinx.metadata.Flags
 import kotlinx.metadata.KmClassVisitor
@@ -127,4 +128,16 @@
             }
         }
     }
+}
+
+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/src/main/kotlin/androidx/room/kotlin/KotlinMetadataElement.kt b/room/compiler/src/main/kotlin/androidx/room/kotlin/KotlinMetadataElement.kt
index 91ff154..82eb235 100644
--- a/room/compiler/src/main/kotlin/androidx/room/kotlin/KotlinMetadataElement.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/kotlin/KotlinMetadataElement.kt
@@ -61,6 +61,8 @@
         it.descriptor == method.descriptor
     }?.isSuspend() ?: false
 
+    fun isObject(): Boolean = classMetadata.isObject()
+
     companion object {
 
         /**
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
index 16f4736..9be4658 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
@@ -22,6 +22,7 @@
 import androidx.room.ext.hasAnyOf
 import androidx.room.ext.toAnnotationBox
 import androidx.room.ext.typeName
+import androidx.room.kotlin.KotlinMetadataElement
 import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_BAD_RETURN_TYPE
 import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_EMPTY_CLASS
 import androidx.room.processor.ProcessorErrors.TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR
@@ -68,8 +69,10 @@
                 .filterValues { it.size > 1 }
                 .values.forEach {
                     it.forEach { converter ->
-                        context.logger.e(converter.method, ProcessorErrors
-                                .duplicateTypeConverters(it.minus(converter)))
+                        context.logger.e(
+                            converter.method, ProcessorErrors
+                                .duplicateTypeConverters(it.minus(converter))
+                        )
                     }
                 }
         }
@@ -78,7 +81,7 @@
     fun process(): List<CustomTypeConverter> {
         // using element utils instead of MoreElements to include statics.
         val methods = ElementFilter
-                .methodsIn(context.processingEnv.elementUtils.getAllMembers(element))
+            .methodsIn(context.processingEnv.elementUtils.getAllMembers(element))
         val declaredType = MoreTypes.asDeclared(element.asType())
         val converterMethods = methods.filter {
             it.hasAnnotation(TypeConverter::class)
@@ -86,30 +89,47 @@
         context.checker.check(converterMethods.isNotEmpty(), element, TYPE_CONVERTER_EMPTY_CLASS)
         val allStatic = converterMethods.all { it.modifiers.contains(Modifier.STATIC) }
         val constructors = ElementFilter.constructorsIn(
-                context.processingEnv.elementUtils.getAllMembers(element))
-        context.checker.check(allStatic || constructors.isEmpty() || constructors.any {
-            it.parameters.isEmpty()
-        }, element, TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR)
-        return converterMethods.mapNotNull { processMethod(declaredType, it) }
+            context.processingEnv.elementUtils.getAllMembers(element)
+        )
+        val kotlinMetadata = KotlinMetadataElement.createFor(context, element)
+        val isKotlinObjectDeclaration = kotlinMetadata?.isObject() == true
+        context.checker.check(
+            isKotlinObjectDeclaration || allStatic || constructors.isEmpty() ||
+                    constructors.any {
+                        it.parameters.isEmpty()
+                    }, element, TYPE_CONVERTER_MISSING_NOARG_CONSTRUCTOR
+        )
+        return converterMethods.mapNotNull {
+            processMethod(
+                container = declaredType,
+                isContainerKotlinObject = isKotlinObjectDeclaration,
+                methodElement = it
+            )
+        }
     }
 
     private fun processMethod(
         container: DeclaredType,
-        methodElement: ExecutableElement
+        methodElement: ExecutableElement,
+        isContainerKotlinObject: Boolean
     ): CustomTypeConverter? {
         val asMember = context.processingEnv.typeUtils.asMemberOf(container, methodElement)
         val executableType = MoreTypes.asExecutable(asMember)
         val returnType = executableType.returnType
         val invalidReturnType = INVALID_RETURN_TYPES.contains(returnType.kind)
-        context.checker.check(methodElement.hasAnyOf(Modifier.PUBLIC), methodElement,
-                TYPE_CONVERTER_MUST_BE_PUBLIC)
+        context.checker.check(
+            methodElement.hasAnyOf(Modifier.PUBLIC), methodElement,
+            TYPE_CONVERTER_MUST_BE_PUBLIC
+        )
         if (invalidReturnType) {
             context.logger.e(methodElement, TYPE_CONVERTER_BAD_RETURN_TYPE)
             return null
         }
         val returnTypeName = returnType.typeName()
-        context.checker.notUnbound(returnTypeName, methodElement,
-                TYPE_CONVERTER_UNBOUND_GENERIC)
+        context.checker.notUnbound(
+            returnTypeName, methodElement,
+            TYPE_CONVERTER_UNBOUND_GENERIC
+        )
         val params = methodElement.parameters
         if (params.size != 1) {
             context.logger.e(methodElement, TYPE_CONVERTER_MUST_RECEIVE_1_PARAM)
@@ -119,7 +139,13 @@
             MoreTypes.asMemberOf(context.processingEnv.typeUtils, container, it)
         }.first()
         context.checker.notUnbound(param.typeName(), params[0], TYPE_CONVERTER_UNBOUND_GENERIC)
-        return CustomTypeConverter(container, methodElement, param, returnType)
+        return CustomTypeConverter(
+            enclosingClass = container,
+            isEnclosingClassKotlinObject = isContainerKotlinObject,
+            method = methodElement,
+            from = param,
+            to = returnType
+        )
     }
 
     /**
@@ -130,6 +156,7 @@
         val converters: List<CustomTypeConverterWrapper>
     ) {
         object EMPTY : ProcessResult(LinkedHashSet(), emptyList())
+
         operator fun plus(other: ProcessResult): ProcessResult {
             val newClasses = LinkedHashSet<TypeMirror>()
             newClasses.addAll(classes)
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
index 6196041..90bef99 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
@@ -34,7 +34,11 @@
 
     override fun convert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
         scope.builder().apply {
-            if (custom.isStatic) {
+            if (custom.isEnclosingClassKotlinObject) {
+                addStatement("$L = $T.INSTANCE.$L($L)",
+                    outputVarName, custom.typeName,
+                    custom.methodName, inputVarName)
+            } else if (custom.isStatic) {
                 addStatement("$L = $T.$L($L)",
                         outputVarName, custom.typeName,
                         custom.methodName, inputVarName)
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/CustomTypeConverter.kt b/room/compiler/src/main/kotlin/androidx/room/vo/CustomTypeConverter.kt
index 1686b1c..3d214d5 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/CustomTypeConverter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/CustomTypeConverter.kt
@@ -27,12 +27,13 @@
  * Generated when we parse a method annotated with TypeConverter.
  */
 data class CustomTypeConverter(
-    val type: TypeMirror,
+    val enclosingClass: TypeMirror,
+    val isEnclosingClassKotlinObject: Boolean,
     val method: ExecutableElement,
     val from: TypeMirror,
     val to: TypeMirror
 ) {
-    val typeName: TypeName by lazy { type.typeName() }
+    val typeName: TypeName by lazy { enclosingClass.typeName() }
     val fromTypeName: TypeName by lazy { from.typeName() }
     val toTypeName: TypeName by lazy { to.typeName() }
     val methodName by lazy { method.simpleName.toString() }
diff --git a/room/compiler/src/test/kotlin/androidx/room/kotlin/KotlinMetadataElementTest.kt b/room/compiler/src/test/kotlin/androidx/room/kotlin/KotlinMetadataElementTest.kt
index d261941..0d49c95 100644
--- a/room/compiler/src/test/kotlin/androidx/room/kotlin/KotlinMetadataElementTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/kotlin/KotlinMetadataElementTest.kt
@@ -25,6 +25,7 @@
 import org.junit.runners.JUnit4
 import simpleRun
 import javax.lang.model.util.ElementFilter
+import kotlin.reflect.KClass
 
 @RunWith(JUnit4::class)
 class KotlinMetadataElementTest {
@@ -32,56 +33,71 @@
     @Test
     fun getParameterNames() {
         simpleRun { invocation ->
-            val (testClassElement, metadataElement) = getMetadataElement(invocation)
+            val (testClassElement, metadataElement) = getMetadataElement(
+                invocation,
+                TestData::class
+            )
             assertThat(ElementFilter.methodsIn(testClassElement.enclosedElements)
                 .first { it.simpleName.toString() == "functionWithParams" }
                 .let { metadataElement.getParameterNames(MoreElements.asExecutable(it)) }
             ).isEqualTo(
                 listOf("param1", "yesOrNo", "number")
             )
-        }
+        }.compilesWithoutError()
     }
 
     @Test
     fun findPrimaryConstructorSignature() {
         simpleRun { invocation ->
-            val (testClassElement, metadataElement) = getMetadataElement(invocation)
+            val (testClassElement, metadataElement) = getMetadataElement(
+                invocation,
+                TestData::class
+            )
             assertThat(
                 ElementFilter.constructorsIn(testClassElement.enclosedElements).map {
                     val desc = MoreElements.asExecutable(it).descriptor(invocation.typeUtils)
                     desc to (desc == metadataElement.findPrimaryConstructorSignature())
-                }.toSet()
-            ).isEqualTo(
-                setOf(
-                    "TestData(Ljava/lang/String;)Landroidx/room/kotlin/" +
-                            "KotlinMetadataElementTest\$TestData" to true,
-                    "TestData(Landroidx/room/kotlin/KotlinMetadataElementTest\$TestData" to false
-                )
+                }
+            ).containsExactly(
+                "<init>(Ljava/lang/String;)V" to true,
+                "<init>()V" to false
             )
-        }
+        }.compilesWithoutError()
     }
 
     @Test
     fun isSuspendFunction() {
         simpleRun { invocation ->
-            val (testClassElement, metadataElement) = getMetadataElement(invocation)
-            assertThat(ElementFilter.constructorsIn(testClassElement.enclosedElements).map {
+            val (testClassElement, metadataElement) = getMetadataElement(
+                invocation,
+                TestData::class
+            )
+            assertThat(ElementFilter.methodsIn(testClassElement.enclosedElements).map {
                 val executableElement = MoreElements.asExecutable(it)
                 executableElement.simpleName.toString() to metadataElement.isSuspendFunction(
                     executableElement
                 )
-            }.toSet()).isEqualTo(
-                setOf(
-                    "emptyFunction" to false,
-                    "suspendFunction" to true,
-                    "functionWithParams" to false
-                )
+            }).containsExactly(
+                "emptyFunction" to false,
+                "suspendFunction" to true,
+                "functionWithParams" to false,
+                "getConstructorParam" to false
             )
-        }
+        }.compilesWithoutError()
     }
 
-    private fun getMetadataElement(invocation: TestInvocation) =
-        invocation.typeElement(TestData::class.java.canonicalName!!).let {
+    @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()
+        }.compilesWithoutError()
+    }
+
+    private fun getMetadataElement(invocation: TestInvocation, klass: KClass<*>) =
+        invocation.typeElement(klass.java.canonicalName!!).let {
             it to KotlinMetadataElement.createFor(Context(invocation.processingEnv), it)!!
         }
 
@@ -95,6 +111,11 @@
         suspend fun suspendFunction() {}
 
         @Suppress("UNUSED_PARAMETER")
-        fun functionWithParams(param1: String, yesOrNo: Boolean, number: Int) {}
+        fun functionWithParams(param1: String, yesOrNo: Boolean, number: Int) {
+        }
+    }
+
+    object ObjectType {
+        val foo: String = ""
     }
 }
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/DateConverter.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/DateConverter.kt
index 9c43efc..3d1e23c 100755
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/DateConverter.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/DateConverter.kt
@@ -19,7 +19,7 @@
 import androidx.room.TypeConverter
 import java.util.Date
 
-class DateConverter {
+object DateConverter {
     @TypeConverter
     fun toDate(timestamp: Long?): Date? {
         return if (timestamp == null) null else Date(timestamp)