Separate TypeConverters from TypeAdapterStore
This is pre-work for adding better nullability support. I've moved
type converter related logic into another class to ensure isolation
from TypeAdapterStore.
This change should be a no-op.
Bug: 193437407
Test: TypeConverterStoreTest
Change-Id: I3508c65c6b03f3fd99c65de5a9483f6ad987f411
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 6216452..603d5a6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -90,10 +90,8 @@
import androidx.room.solver.types.ByteBufferColumnTypeAdapter
import androidx.room.solver.types.ColumnTypeAdapter
import androidx.room.solver.types.CompositeAdapter
-import androidx.room.solver.types.CompositeTypeConverter
import androidx.room.solver.types.CursorValueReader
import androidx.room.solver.types.EnumColumnTypeAdapter
-import androidx.room.solver.types.NoOpConverter
import androidx.room.solver.types.PrimitiveBooleanToIntConverter
import androidx.room.solver.types.PrimitiveColumnTypeAdapter
import androidx.room.solver.types.StatementValueBinder
@@ -104,11 +102,10 @@
import com.google.common.annotations.VisibleForTesting
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableListMultimap
+import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableMultimap
import com.google.common.collect.ImmutableSetMultimap
-import com.google.common.collect.ImmutableMap
import com.squareup.javapoet.ClassName
-import java.util.LinkedList
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
/**
@@ -121,10 +118,8 @@
* first type adapter has the highest priority
*/
private val columnTypeAdapters: List<ColumnTypeAdapter>,
- /**
- * first converter has the highest priority
- */
- private val typeConverters: List<TypeConverter>
+
+ private val typeConverterStore: TypeConverterStore
) {
companion object {
@@ -132,7 +127,7 @@
return TypeAdapterStore(
context = context,
columnTypeAdapters = store.columnTypeAdapters,
- typeConverters = store.typeConverters
+ typeConverterStore = store.typeConverterStore
)
}
@@ -172,7 +167,7 @@
.forEach(::addTypeConverter)
return TypeAdapterStore(
context = context, columnTypeAdapters = adapters,
- typeConverters = converters
+ typeConverterStore = TypeConverterStore(converters)
)
}
}
@@ -306,20 +301,7 @@
*/
@VisibleForTesting
fun reverse(converter: TypeConverter): TypeConverter? {
- return when (converter) {
- is NoOpConverter -> converter
- is CompositeTypeConverter -> {
- val r1 = reverse(converter.conv1) ?: return null
- val r2 = reverse(converter.conv2) ?: return null
- CompositeTypeConverter(r2, r1)
- }
- else -> {
- typeConverters.firstOrNull {
- it.from.isSameType(converter.to) &&
- it.to.isSameType(converter.from)
- }
- }
- }
+ return typeConverterStore.reverse(converter)
}
/**
@@ -381,7 +363,7 @@
}
fun findTypeConverter(input: XType, output: XType): TypeConverter? {
- return findTypeConverter(listOf(input), listOf(output))
+ return typeConverterStore.findTypeConverter(listOf(input), listOf(output))
}
fun findDeleteOrUpdateMethodBinder(typeMirror: XType): DeleteOrUpdateMethodBinder {
@@ -815,74 +797,11 @@
}
private fun findTypeConverter(input: XType, outputs: List<XType>): TypeConverter? {
- return findTypeConverter(listOf(input), outputs)
+ return typeConverterStore.findTypeConverter(listOf(input), outputs)
}
private fun findTypeConverter(input: List<XType>, output: XType): TypeConverter? {
- return findTypeConverter(input, listOf(output))
- }
-
- private fun findTypeConverter(
- inputs: List<XType>,
- outputs: List<XType>
- ): TypeConverter? {
- if (inputs.isEmpty()) {
- return null
- }
- inputs.forEach { input ->
- if (outputs.any { output -> input.isSameType(output) }) {
- return NoOpConverter(input)
- }
- }
-
- val excludes = arrayListOf<XType>()
-
- val queue = LinkedList<TypeConverter>()
- fun List<TypeConverter>.findMatchingConverter(): TypeConverter? {
- // We prioritize exact match over assignable. To do that, this variable keeps any
- // assignable match and if we cannot find exactly same type match, we'll return the
- // assignable match.
- var assignableMatchFallback: TypeConverter? = null
- this.forEach { converter ->
- outputs.forEach { output ->
- if (output.isSameType(converter.to)) {
- return converter
- } else if (assignableMatchFallback == null &&
- output.isAssignableFrom(converter.to)
- ) {
- // if we don't find exact match, we'll return this.
- assignableMatchFallback = converter
- }
- }
- }
- return assignableMatchFallback
- }
- inputs.forEach { input ->
- val candidates = getAllTypeConverters(input, excludes)
- val match = candidates.findMatchingConverter()
- if (match != null) {
- return match
- }
- candidates.forEach {
- excludes.add(it.to)
- queue.add(it)
- }
- }
- excludes.addAll(inputs)
- while (queue.isNotEmpty()) {
- val prev = queue.pop()
- val from = prev.to
- val candidates = getAllTypeConverters(from, excludes)
- val match = candidates.findMatchingConverter()
- if (match != null) {
- return CompositeTypeConverter(prev, match)
- }
- candidates.forEach {
- excludes.add(it.to)
- queue.add(CompositeTypeConverter(prev, it))
- }
- }
- return null
+ return typeConverterStore.findTypeConverter(input, listOf(output))
}
private fun getAllColumnAdapters(input: XType): List<ColumnTypeAdapter> {
@@ -890,25 +809,4 @@
input.isSameType(it.out)
}
}
-
- /**
- * Returns all type converters that can receive input type and return into another type.
- * The returned list is ordered by priority such that if we have an exact match, it is
- * prioritized.
- */
- private fun getAllTypeConverters(input: XType, excludes: List<XType>): List<TypeConverter> {
- // for input, check assignability because it defines whether we can use the method or not.
- // for excludes, use exact match
- return typeConverters.filter { converter ->
- converter.from.isAssignableFrom(input) &&
- !excludes.any { it.isSameType(converter.to) }
- }.sortedByDescending {
- // if it is the same, prioritize
- if (it.from.isSameType(input)) {
- 2
- } else {
- 1
- }
- }
- }
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStore.kt
new file mode 100644
index 0000000..43ac3e3
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeConverterStore.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2021 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.solver
+
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.types.CompositeTypeConverter
+import androidx.room.solver.types.NoOpConverter
+import androidx.room.solver.types.TypeConverter
+import com.google.common.annotations.VisibleForTesting
+import java.util.LinkedList
+
+/**
+ * Common logic that handles conversion between types either using built in converters or user
+ * provided type converters.
+ */
+class TypeConverterStore(
+ private val typeConverters: List<TypeConverter>
+) {
+ /**
+ * Finds a type converter that can conver one of the input valuese to one of the output values.
+ *
+ * When multiple conversion paths are possible, shortest path (least amount of conversion) is
+ * preferred.
+ */
+ fun findTypeConverter(
+ inputs: List<XType>,
+ outputs: List<XType>
+ ): TypeConverter? {
+ if (inputs.isEmpty()) {
+ return null
+ }
+ inputs.forEach { input ->
+ if (outputs.any { output -> input.isSameType(output) }) {
+ return NoOpConverter(input)
+ }
+ }
+
+ val excludes = arrayListOf<XType>()
+
+ val queue = LinkedList<TypeConverter>()
+ fun List<TypeConverter>.findMatchingConverter(): TypeConverter? {
+ // We prioritize exact match over assignable. To do that, this variable keeps any
+ // assignable match and if we cannot find exactly same type match, we'll return the
+ // assignable match.
+ var assignableMatchFallback: TypeConverter? = null
+ this.forEach { converter ->
+ outputs.forEach { output ->
+ if (output.isSameType(converter.to)) {
+ return converter
+ } else if (assignableMatchFallback == null &&
+ output.isAssignableFrom(converter.to)
+ ) {
+ // if we don't find exact match, we'll return this.
+ assignableMatchFallback = converter
+ }
+ }
+ }
+ return assignableMatchFallback
+ }
+ inputs.forEach { input ->
+ val candidates = getAllTypeConverters(input, excludes)
+ val match = candidates.findMatchingConverter()
+ if (match != null) {
+ return match
+ }
+ candidates.forEach {
+ excludes.add(it.to)
+ queue.add(it)
+ }
+ }
+ excludes.addAll(inputs)
+ while (queue.isNotEmpty()) {
+ val prev = queue.pop()
+ val from = prev.to
+ val candidates = getAllTypeConverters(from, excludes)
+ val match = candidates.findMatchingConverter()
+ if (match != null) {
+ return CompositeTypeConverter(prev, match)
+ }
+ candidates.forEach {
+ excludes.add(it.to)
+ queue.add(CompositeTypeConverter(prev, it))
+ }
+ }
+ return null
+ }
+
+ /**
+ * Returns all type converters that can receive input type and return into another type.
+ * The returned list is ordered by priority such that if we have an exact match, it is
+ * prioritized.
+ */
+ private fun getAllTypeConverters(input: XType, excludes: List<XType>): List<TypeConverter> {
+ // for input, check assignability because it defines whether we can use the method or not.
+ // for excludes, use exact match
+ return typeConverters.filter { converter ->
+ converter.from.isAssignableFrom(input) &&
+ !excludes.any { it.isSameType(converter.to) }
+ }.sortedByDescending {
+ // if it is the same, prioritize
+ if (it.from.isSameType(input)) {
+ 2
+ } else {
+ 1
+ }
+ }
+ }
+ /**
+ * Tries to reverse the converter going through the same nodes, if possible.
+ */
+ @VisibleForTesting
+ fun reverse(converter: TypeConverter): TypeConverter? {
+ return when (converter) {
+ is NoOpConverter -> converter
+ is CompositeTypeConverter -> {
+ val r1 = reverse(converter.conv1) ?: return null
+ val r2 = reverse(converter.conv2) ?: return null
+ CompositeTypeConverter(r2, r1)
+ }
+ else -> {
+ typeConverters.firstOrNull {
+ it.from.isSameType(converter.to) &&
+ it.to.isSameType(converter.from)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 9b0a364..fcd310d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -55,7 +55,6 @@
import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
import androidx.room.solver.types.CompositeAdapter
-import androidx.room.solver.types.CompositeTypeConverter
import androidx.room.solver.types.CustomTypeConverterWrapper
import androidx.room.solver.types.EnumColumnTypeAdapter
import androidx.room.solver.types.PrimitiveColumnTypeAdapter
@@ -346,99 +345,6 @@
}
@Test
- fun multiStepTypeConverters() {
- val source = Source.kotlin(
- "Foo.kt",
- """
- import androidx.room.*
- interface Type1_Super
- interface Type1 : Type1_Super
- interface Type1_Sub : Type1
- interface Type2_Super
- interface Type2 : Type2_Super
- interface Type2_Sub : Type2
- interface JumpType_1
- interface JumpType_2
- interface JumpType_3
- class MyConverters {
- @TypeConverter
- fun t1_jump1(inp : Type1): JumpType_1 = TODO()
- @TypeConverter
- fun jump1_t2_Sub(inp : JumpType_1): Type2_Sub = TODO()
- @TypeConverter
- fun jump1_t2(inp : JumpType_1): Type2 = TODO()
- @TypeConverter
- fun t1_super_jump2(inp : Type1_Super): JumpType_2 = TODO()
- @TypeConverter
- fun jump2_jump3(inp : JumpType_2): JumpType_3 = TODO()
- @TypeConverter
- fun jump2_Type2_Sub(inp : JumpType_3): Type2_Sub = TODO()
- }
- """.trimIndent()
- )
- runProcessorTest(sources = listOf(source)) { invocation ->
- val convertersElm = invocation.processingEnv.requireTypeElement("MyConverters")
- val converters = CustomConverterProcessor(invocation.context, convertersElm)
- .process()
- val store = TypeAdapterStore.create(
- invocation.context,
- converters.map(::CustomTypeConverterWrapper)
- )
-
- fun TypeConverter.toSignature(): String {
- return when (this) {
- is CompositeTypeConverter -> "${conv1.toSignature()} : ${conv2.toSignature()}"
- else -> "${from.typeName} -> ${to.typeName}"
- }
- }
-
- fun findConverter(from: String, to: String): String? {
- val input = invocation.processingEnv.requireType(from)
- val output = invocation.processingEnv.requireType(to)
- return store.findTypeConverter(
- input = input,
- output = output
- )?.also {
- // validate that it makes sense to ensure test is correct
- assertThat(output.isAssignableFrom(it.to)).isTrue()
- assertThat(it.from.isAssignableFrom(input)).isTrue()
- }?.toSignature()
- }
- assertThat(
- findConverter("Type1", "Type2")
- ).isEqualTo(
- "Type1 -> JumpType_1 : JumpType_1 -> Type2"
- )
- assertThat(
- findConverter("Type1", "Type2_Sub")
- ).isEqualTo(
- "Type1 -> JumpType_1 : JumpType_1 -> Type2_Sub"
- )
- assertThat(
- findConverter("Type1_Super", "Type2_Super")
- ).isEqualTo(
- "Type1_Super -> JumpType_2 : JumpType_2 -> JumpType_3 : JumpType_3 -> Type2_Sub"
- )
- assertThat(
- findConverter("Type1", "Type2_Sub")
- ).isEqualTo(
- "Type1 -> JumpType_1 : JumpType_1 -> Type2_Sub"
- )
- assertThat(
- findConverter("Type1_Sub", "Type2_Sub")
- ).isEqualTo(
- "Type1 -> JumpType_1 : JumpType_1 -> Type2_Sub"
- )
- assertThat(
- findConverter("Type2", "Type2_Sub")
- ).isNull()
- assertThat(
- findConverter("Type2", "Type1")
- ).isNull()
- }
- }
-
- @Test
fun testIntList() {
runProcessorTest { invocation ->
val binders = createIntListToStringBinders(invocation)
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterStoreTest.kt
new file mode 100644
index 0000000..c56b944
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeConverterStoreTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2021 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.solver
+
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.processor.CustomConverterProcessor
+import androidx.room.solver.types.CompositeTypeConverter
+import androidx.room.solver.types.CustomTypeConverterWrapper
+import androidx.room.solver.types.TypeConverter
+import androidx.room.testing.context
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class TypeConverterStoreTest {
+ @Test
+ fun multiStepTypeConverters() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.*
+ interface Type1_Super
+ interface Type1 : Type1_Super
+ interface Type1_Sub : Type1
+ interface Type2_Super
+ interface Type2 : Type2_Super
+ interface Type2_Sub : Type2
+ interface JumpType_1
+ interface JumpType_2
+ interface JumpType_3
+ class MyConverters {
+ @TypeConverter
+ fun t1_jump1(inp : Type1): JumpType_1 = TODO()
+ @TypeConverter
+ fun jump1_t2_Sub(inp : JumpType_1): Type2_Sub = TODO()
+ @TypeConverter
+ fun jump1_t2(inp : JumpType_1): Type2 = TODO()
+ @TypeConverter
+ fun t1_super_jump2(inp : Type1_Super): JumpType_2 = TODO()
+ @TypeConverter
+ fun jump2_jump3(inp : JumpType_2): JumpType_3 = TODO()
+ @TypeConverter
+ fun jump2_Type2_Sub(inp : JumpType_3): Type2_Sub = TODO()
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(sources = listOf(source)) { invocation ->
+ val convertersElm = invocation.processingEnv.requireTypeElement("MyConverters")
+ val converters = CustomConverterProcessor(invocation.context, convertersElm)
+ .process()
+ val store = TypeAdapterStore.create(
+ invocation.context,
+ converters.map(::CustomTypeConverterWrapper)
+ )
+
+ fun findConverter(from: String, to: String): String? {
+ val input = invocation.processingEnv.requireType(from)
+ val output = invocation.processingEnv.requireType(to)
+ return store.findTypeConverter(
+ input = input,
+ output = output
+ )?.also {
+ // validate that it makes sense to ensure test is correct
+ assertThat(output.isAssignableFrom(it.to)).isTrue()
+ assertThat(it.from.isAssignableFrom(input)).isTrue()
+ }?.toSignature()
+ }
+ assertThat(
+ findConverter("Type1", "Type2")
+ ).isEqualTo(
+ "Type1 -> JumpType_1 : JumpType_1 -> Type2"
+ )
+ assertThat(
+ findConverter("Type1", "Type2_Sub")
+ ).isEqualTo(
+ "Type1 -> JumpType_1 : JumpType_1 -> Type2_Sub"
+ )
+ assertThat(
+ findConverter("Type1_Super", "Type2_Super")
+ ).isEqualTo(
+ "Type1_Super -> JumpType_2 : JumpType_2 -> JumpType_3 : JumpType_3 -> Type2_Sub"
+ )
+ assertThat(
+ findConverter("Type1", "Type2_Sub")
+ ).isEqualTo(
+ "Type1 -> JumpType_1 : JumpType_1 -> Type2_Sub"
+ )
+ assertThat(
+ findConverter("Type1_Sub", "Type2_Sub")
+ ).isEqualTo(
+ "Type1 -> JumpType_1 : JumpType_1 -> Type2_Sub"
+ )
+ assertThat(
+ findConverter("Type2", "Type2_Sub")
+ ).isNull()
+ assertThat(
+ findConverter("Type2", "Type1")
+ ).isNull()
+ }
+ }
+
+ private fun TypeConverter.toSignature(): String {
+ return when (this) {
+ is CompositeTypeConverter -> "${conv1.toSignature()} : ${conv2.toSignature()}"
+ else -> "${from.typeName} -> ${to.typeName}"
+ }
+ }
+}
\ No newline at end of file