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)