Merge "Support Kotlin concrete functions in DAO interfaces." into androidx-master-dev
diff --git a/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt b/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
index 1e02fad..b817e36 100644
--- a/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/ext/element_ext.kt
@@ -354,7 +354,7 @@
 /**
  * Finds the default implementation method corresponding to this Kotlin interface method.
  */
-fun Element.findKotlinDefaultImpl(typeUtils: Types): Element? {
+fun ExecutableElement.findKotlinDefaultImpl(typeUtils: Types): ExecutableElement? {
     fun paramsMatch(ourParams: List<VariableElement>, theirParams: List<VariableElement>): Boolean {
         if (ourParams.size != theirParams.size - 1) {
             return false
@@ -372,10 +372,8 @@
     val innerClass = parent.enclosedElements.find {
         it.kind == ElementKind.CLASS && it.simpleName.contentEquals(DEFAULT_IMPLS_CLASS_NAME)
     } ?: return null
-    return innerClass.enclosedElements.find {
-        it.kind == ElementKind.METHOD && it.simpleName == this.simpleName &&
-                paramsMatch(MoreElements.asExecutable(this).parameters,
-                        MoreElements.asExecutable(it).parameters)
+    return ElementFilter.methodsIn(innerClass.enclosedElements).find {
+        it.simpleName == this.simpleName && paramsMatch(this.parameters, it.parameters)
     }
 }
 
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
index 228f6fe..be15aaf 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DaoProcessor.kt
@@ -29,7 +29,7 @@
 import androidx.room.ext.typeName
 import androidx.room.verifier.DatabaseVerifier
 import androidx.room.vo.Dao
-import com.google.auto.common.MoreElements
+import androidx.room.vo.KotlinDefaultMethodDelegate
 import com.google.auto.common.MoreTypes
 import com.squareup.javapoet.TypeName
 import javax.lang.model.element.ElementKind
@@ -37,6 +37,7 @@
 import javax.lang.model.element.Modifier.ABSTRACT
 import javax.lang.model.element.TypeElement
 import javax.lang.model.type.DeclaredType
+import javax.lang.model.util.ElementFilter
 
 class DaoProcessor(
     baseContext: Context,
@@ -60,12 +61,10 @@
 
         val declaredType = MoreTypes.asDeclared(element.asType())
         val allMembers = context.processingEnv.elementUtils.getAllMembers(element)
-        val methods = allMembers
+        val methods = ElementFilter.methodsIn(allMembers)
             .filter {
-                it.hasAnyOf(ABSTRACT) && it.kind == ElementKind.METHOD &&
+                it.hasAnyOf(ABSTRACT) &&
                         it.findKotlinDefaultImpl(context.processingEnv.typeUtils) == null
-            }.map {
-                MoreElements.asExecutable(it)
             }.groupBy { method ->
                 context.checker.check(
                         PROCESSED_ANNOTATIONS.count { method.hasAnnotation(it) } == 1, method,
@@ -130,20 +129,34 @@
                     executableElement = it).process()
         } ?: emptyList()
 
-        val transactionMethods = allMembers.filter { member ->
+        val transactionMethods = ElementFilter.methodsIn(allMembers).filter { member ->
             member.hasAnnotation(Transaction::class) &&
-                    member.kind == ElementKind.METHOD &&
                     PROCESSED_ANNOTATIONS.none { member.hasAnnotation(it) }
         }.map {
             TransactionMethodProcessor(
                     baseContext = context,
                     containing = declaredType,
-                    executableElement = MoreElements.asExecutable(it)).process()
+                    executableElement = it).process()
         }
 
-        val constructors = allMembers
-                .filter { it.kind == ElementKind.CONSTRUCTOR }
-                .map { MoreElements.asExecutable(it) }
+        val kotlinDefaultMethodDelegates = if (element.kind == ElementKind.INTERFACE) {
+            val allProcessedMethods =
+                methods.values.flatten() + transactionMethods.map { it.element }
+            ElementFilter.methodsIn(allMembers).filterNot {
+                allProcessedMethods.contains(it)
+            }.mapNotNull { method ->
+                method.findKotlinDefaultImpl(context.processingEnv.typeUtils)?.let { delegate ->
+                    KotlinDefaultMethodDelegate(
+                        element = method,
+                        delegateElement = delegate
+                    )
+                }
+            }
+        } else {
+            emptyList()
+        }
+
+        val constructors = ElementFilter.constructorsIn(allMembers)
         val typeUtils = context.processingEnv.typeUtils
         val goodConstructor = constructors.firstOrNull {
             it.parameters.size == 1 &&
@@ -171,6 +184,7 @@
                 deletionMethods = deletionMethods,
                 updateMethods = updateMethods,
                 transactionMethods = transactionMethods,
+                kotlinDefaultMethodDelegates = kotlinDefaultMethodDelegates,
                 constructorParamType = constructorParamType)
     }
 
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/KotlinDefaultMethodDelegateBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/KotlinDefaultMethodDelegateBinder.kt
new file mode 100644
index 0000000..139d491
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/KotlinDefaultMethodDelegateBinder.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.solver
+
+import androidx.room.ext.DEFAULT_IMPLS_CLASS_NAME
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.T
+import com.squareup.javapoet.ClassName
+import isVoid
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Method binder that delegates to concrete DAO function in a Kotlin interface.
+ */
+object KotlinDefaultMethodDelegateBinder {
+    fun executeAndReturn(
+        daoName: ClassName,
+        daoImplName: ClassName,
+        methodName: String,
+        returnType: TypeMirror,
+        parameterNames: List<String>,
+        scope: CodeGenScope
+    ) {
+        scope.builder().apply {
+            val params: MutableList<Any> = mutableListOf()
+            val format = buildString {
+                if (!returnType.isVoid()) {
+                    append("return ")
+                }
+                append("$T.$N.$N($T.this")
+                params.add(daoName)
+                params.add(DEFAULT_IMPLS_CLASS_NAME)
+                params.add(methodName)
+                params.add(daoImplName)
+                parameterNames.forEach {
+                    append(", ")
+                    append(L)
+                    params.add(it)
+                }
+                append(")")
+            }
+            addStatement(format, *params.toTypedArray())
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Dao.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Dao.kt
index 4558040..dbdfdaf 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Dao.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Dao.kt
@@ -30,6 +30,7 @@
     val deletionMethods: List<DeletionMethod>,
     val updateMethods: List<UpdateMethod>,
     val transactionMethods: List<TransactionMethod>,
+    val kotlinDefaultMethodDelegates: List<KotlinDefaultMethodDelegate>,
     val constructorParamType: TypeName?
 ) {
     // parsed dao might have a suffix if it is used in multiple databases.
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/KotlinDefaultMethodDelegate.kt b/room/compiler/src/main/kotlin/androidx/room/vo/KotlinDefaultMethodDelegate.kt
new file mode 100644
index 0000000..219c954
--- /dev/null
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/KotlinDefaultMethodDelegate.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.vo
+
+import javax.lang.model.element.ExecutableElement
+
+/**
+ * Represents a DAO method that delegates to a concrete implementation, such as a concrete function
+ * in a Kotlin interface.
+ */
+data class KotlinDefaultMethodDelegate(
+    val element: ExecutableElement,
+    val delegateElement: ExecutableElement
+)
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index 5d9dc90..cd2a0ad 100644
--- a/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -23,7 +23,9 @@
 import androidx.room.ext.T
 import androidx.room.processor.OnConflictProcessor
 import androidx.room.solver.CodeGenScope
+import androidx.room.solver.KotlinDefaultMethodDelegateBinder
 import androidx.room.vo.Dao
+import androidx.room.vo.KotlinDefaultMethodDelegate
 import androidx.room.vo.InsertionMethod
 import androidx.room.vo.QueryMethod
 import androidx.room.vo.RawQueryMethod
@@ -132,6 +134,9 @@
             dao.rawQueryMethods.forEach {
                 addMethod(createRawQueryMethod(it))
             }
+            dao.kotlinDefaultMethodDelegates.forEach {
+                addMethod(createDefaultMethodDelegate(it))
+            }
         }
         return builder
     }
@@ -428,6 +433,20 @@
         return scope.builder().build()
     }
 
+    private fun createDefaultMethodDelegate(method: KotlinDefaultMethodDelegate): MethodSpec {
+        val scope = CodeGenScope(this)
+        return overrideWithoutAnnotations(method.element, declaredDao).apply {
+            KotlinDefaultMethodDelegateBinder.executeAndReturn(
+                daoName = dao.typeName,
+                daoImplName = dao.implTypeName,
+                methodName = method.delegateElement.simpleName.toString(),
+                returnType = method.element.returnType,
+                parameterNames = method.element.parameters.map { it.simpleName.toString() },
+                scope = scope)
+            addCode(scope.builder().build())
+        }.build()
+    }
+
     private fun overrideWithoutAnnotations(
         elm: ExecutableElement,
         owner: DeclaredType
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index c40c87a..6d91da7 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -401,4 +401,26 @@
 
     @Query("SELECT * FROM book WHERE bookId = :id")
     fun getOneBooksFlow(id: String): Flow<Book?>
+
+    fun addAndRemovePublisher(thePublisher: Publisher) {
+        addPublishers(thePublisher)
+        deletePublishers(thePublisher)
+    }
+
+    fun concreteFunction() = ""
+
+    fun concreteVoidFunction() {
+    }
+
+    fun concreteUnitFunction() {
+    }
+
+    fun concreteFunctionWithParams(num: Int, text: String) = "$num - $text"
+
+    suspend fun concreteSuspendFunction() = ""
+
+    suspend fun concreteVoidSuspendFunction() {
+    }
+
+    suspend fun concreteSuspendFunctionWithParams(num: Int, text: String) = "$num - $text"
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
index 6228828..6921f2b 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/BooksDaoTest.kt
@@ -36,7 +36,9 @@
 import org.hamcrest.CoreMatchers.instanceOf
 import org.hamcrest.CoreMatchers.notNullValue
 import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
 import org.junit.Assert.fail
 import org.junit.Test
 import java.util.Date
@@ -358,4 +360,18 @@
             assertThat(booksDao.getBooksSuspend().isEmpty(), `is`(true))
         }
     }
+
+    @Test
+    fun kotlinDefaultFunction() {
+        booksDao.addAndRemovePublisher(TestUtil.PUBLISHER)
+        assertNull(booksDao.getPublisher(TestUtil.PUBLISHER.publisherId))
+
+        assertEquals("", booksDao.concreteFunction())
+        assertEquals("1 - hello", booksDao.concreteFunctionWithParams(1, "hello"))
+
+        runBlocking {
+            assertEquals("", booksDao.concreteSuspendFunction())
+            assertEquals("2 - hi", booksDao.concreteSuspendFunctionWithParams(2, "hi"))
+        }
+    }
 }