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"))
+ }
+ }
}