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