[GH] Improve invalid Dao method errors
Proposed Changes
This change improves invalid Dao method error messages. In this case an invalid Dao method is a method which is suspending and returns one of the following types:
- any of RxJava2 types
- any of RxJava3 types
- LiveData
- ComputableLiveData
- ListenableFuture
- ReactiveStreams Publisher
- Kotlin Flow
Testing
Test: Added unit tests for Update, Insertion, Deletion and Query methods
Issues Fixed
Fixes: https://issuetracker.google.com/issues/151029087
This is an imported pull request from https://github.com/androidx/androidx/pull/299.
Resolves #299
Github-Pr-Head-Sha: d16de9ba9411fb7747a71d49e0dea587cd545a17
GitOrigin-RevId: 9aeb583b294cb58becf335dc33532bed1afeeef8
Change-Id: I980a099517609f39a37c267951d90b963f48e1d8
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
index b024579..1d21c1a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
@@ -212,6 +212,24 @@
val FLOW = ClassName.get("kotlinx.coroutines.flow", "Flow")
}
+val DEFERRED_TYPES = listOf(
+ LifecyclesTypeNames.LIVE_DATA,
+ LifecyclesTypeNames.COMPUTABLE_LIVE_DATA,
+ RxJava2TypeNames.FLOWABLE,
+ RxJava2TypeNames.OBSERVABLE,
+ RxJava2TypeNames.MAYBE,
+ RxJava2TypeNames.SINGLE,
+ RxJava2TypeNames.COMPLETABLE,
+ RxJava3TypeNames.FLOWABLE,
+ RxJava3TypeNames.OBSERVABLE,
+ RxJava3TypeNames.MAYBE,
+ RxJava3TypeNames.SINGLE,
+ RxJava3TypeNames.COMPLETABLE,
+ GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE,
+ KotlinTypeNames.FLOW,
+ ReactiveStreamsTypeNames.PUBLISHER
+)
+
fun TypeName.defaultValue(): String {
return if (!isPrimitive) {
"null"
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index 2301cf4e..d75b580 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -22,6 +22,7 @@
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XVariableElement
import androidx.room.compiler.processing.isSuspendFunction
+import androidx.room.ext.DEFERRED_TYPES
import androidx.room.ext.KotlinTypeNames
import androidx.room.ext.L
import androidx.room.ext.N
@@ -125,6 +126,21 @@
}
}
+fun MethodProcessorDelegate.isSuspendAndReturnsDeferredType(): Boolean {
+ if (!executableElement.isSuspendFunction()) {
+ return false
+ }
+
+ val deferredTypes = DEFERRED_TYPES.mapNotNull { context.processingEnv.findType(it) }
+
+ val returnType = extractReturnType()
+ val hasDeferredReturnType = deferredTypes.any { deferredType ->
+ deferredType.rawType.isAssignableFrom(returnType.rawType)
+ }
+
+ return hasDeferredReturnType
+}
+
/**
* Default delegate for DAO methods.
*/
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 5af3062..283c92d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -175,6 +175,10 @@
val CANNOT_FIND_UPDATE_RESULT_ADAPTER = "Not sure how to handle update method's " +
"return type. Currently the supported return types are void, int or Int."
+ fun suspendReturnsDeferredType(returnTypeName: String) = "Dao functions that have a suspend " +
+ "modifier must not return a deferred/async type ($returnTypeName). Most probably this " +
+ "is an error. Consider changing the return type or removing the suspend modifier."
+
val CANNOT_FIND_INSERT_RESULT_ADAPTER = "Not sure how to handle insert method's return type."
val UPDATE_MISSING_PARAMS = "Method annotated with" +
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
index ab71220..d9c5a0f 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
@@ -107,6 +107,12 @@
val delegate = MethodProcessorDelegate.createFor(context, containing, executableElement)
val returnType = delegate.extractReturnType()
+ context.checker.check(
+ !delegate.isSuspendAndReturnsDeferredType(),
+ executableElement,
+ ProcessorErrors.suspendReturnsDeferredType(returnType.rawType.typeName.toString())
+ )
+
val query = if (input != null) {
val query = SqlParser.parse(input)
context.checker.check(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
index d2bfc80..7e59ed5 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
@@ -50,6 +50,13 @@
returnTypeName, executableElement,
ProcessorErrors.CANNOT_USE_UNBOUND_GENERICS_IN_QUERY_METHODS
)
+
+ context.checker.check(
+ !delegate.isSuspendAndReturnsDeferredType(),
+ executableElement,
+ ProcessorErrors.suspendReturnsDeferredType(returnType.rawType.typeName.toString())
+ )
+
val observedTableNames = processObservedTables()
val query = SqlParser.rawQueryForTables(observedTableNames)
// build the query but don't calculate result info since we just guessed it.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index 59adf60..2f1aff8 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -44,7 +44,15 @@
return annotation
}
- fun extractReturnType() = delegate.extractReturnType()
+ fun extractReturnType(): XType {
+ val returnType = delegate.extractReturnType()
+ context.checker.check(
+ !delegate.isSuspendAndReturnsDeferredType(),
+ executableElement,
+ ProcessorErrors.suspendReturnsDeferredType(returnType.rawType.typeName.toString())
+ )
+ return returnType
+ }
fun extractParams(
targetEntityType: XType?,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
index acf8ec6..404f9fb 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/TransactionMethodProcessor.kt
@@ -16,14 +16,10 @@
package androidx.room.processor
-import androidx.room.ext.GuavaUtilConcurrentTypeNames
-import androidx.room.ext.LifecyclesTypeNames
-import androidx.room.ext.RxJava2TypeNames
-import androidx.room.ext.RxJava3TypeNames
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
-import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.DEFERRED_TYPES
import androidx.room.vo.TransactionMethod
class TransactionMethodProcessor(
@@ -49,7 +45,7 @@
DEFERRED_TYPES.firstOrNull { className ->
context.processingEnv.findType(className)?.let {
- rawReturnType.isAssignableFrom(it)
+ it.rawType.isAssignableFrom(rawReturnType)
} ?: false
}?.let { returnTypeName ->
context.logger.e(
@@ -81,22 +77,4 @@
methodBinder = delegate.findTransactionMethodBinder(callType)
)
}
-
- companion object {
- val DEFERRED_TYPES = listOf(
- LifecyclesTypeNames.LIVE_DATA,
- RxJava2TypeNames.FLOWABLE,
- RxJava2TypeNames.OBSERVABLE,
- RxJava2TypeNames.MAYBE,
- RxJava2TypeNames.SINGLE,
- RxJava2TypeNames.COMPLETABLE,
- RxJava3TypeNames.FLOWABLE,
- RxJava3TypeNames.OBSERVABLE,
- RxJava3TypeNames.MAYBE,
- RxJava3TypeNames.SINGLE,
- RxJava3TypeNames.COMPLETABLE,
- GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE,
- KotlinTypeNames.FLOW
- )
- }
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
index 0a60819..733575d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
@@ -25,6 +25,10 @@
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.GuavaUtilConcurrentTypeNames
+import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.LifecyclesTypeNames
+import androidx.room.ext.ReactiveStreamsTypeNames
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
import androidx.room.solver.shortcut.result.InsertMethodAdapter
@@ -53,6 +57,20 @@
@Dao
abstract class MyClass {
"""
+ const val DAO_PREFIX_KT = """
+ package foo.bar
+ import androidx.room.*
+ import java.util.*
+ import io.reactivex.*
+ io.reactivex.rxjava3.core.*
+ androidx.lifecycle.*
+ com.google.common.util.concurrent.*
+ org.reactivestreams.*
+ kotlinx.coroutines.flow.*
+
+ @Dao
+ abstract class MyClass {
+ """
const val DAO_SUFFIX = "}"
val USER_TYPE_NAME: TypeName = COMMON.USER_TYPE_NAME
val USERNAME_TYPE_NAME: TypeName = ClassName.get("foo.bar", "Username")
@@ -920,6 +938,39 @@
}
}
+ @Test
+ fun suspendReturnsDeferredType() {
+ listOf(
+ "${RxJava2TypeNames.FLOWABLE}<Int>",
+ "${RxJava2TypeNames.OBSERVABLE}<Int>",
+ "${RxJava2TypeNames.MAYBE}<Int>",
+ "${RxJava2TypeNames.SINGLE}<Int>",
+ "${RxJava2TypeNames.COMPLETABLE}",
+ "${RxJava3TypeNames.FLOWABLE}<Int>",
+ "${RxJava3TypeNames.OBSERVABLE}<Int>",
+ "${RxJava3TypeNames.MAYBE}<Int>",
+ "${RxJava3TypeNames.SINGLE}<Int>",
+ "${RxJava3TypeNames.COMPLETABLE}",
+ "${LifecyclesTypeNames.LIVE_DATA}<Int>",
+ "${LifecyclesTypeNames.COMPUTABLE_LIVE_DATA}<Int>",
+ "${GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE}<Int>",
+ "${ReactiveStreamsTypeNames.PUBLISHER}<Int>",
+ "${KotlinTypeNames.FLOW}<Int>"
+ ).forEach { type ->
+ singleInsertMethodKotlin(
+ """
+ @Insert
+ abstract suspend fun foo(user: User): $type
+ """
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ val rawTypeName = type.substringBefore("<")
+ hasErrorContaining(ProcessorErrors.suspendReturnsDeferredType(rawTypeName))
+ }
+ }
+ }
+ }
+
fun singleInsertMethod(
vararg input: String,
additionalSources: List<Source> = emptyList(),
@@ -958,4 +1009,45 @@
handler(processed, invocation)
}
}
+
+ fun singleInsertMethodKotlin(
+ vararg input: String,
+ additionalSources: List<Source> = emptyList(),
+ handler: (InsertionMethod, XTestInvocation) -> Unit
+ ) {
+ val inputSource = Source.kotlin(
+ "MyClass.kt",
+ DAO_PREFIX_KT + input.joinToString("\n") + DAO_SUFFIX
+ )
+ val commonSources = listOf(
+ COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
+ COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX2_FLOWABLE, COMMON.RX2_OBSERVABLE,
+ COMMON.RX3_COMPLETABLE, COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.RX3_FLOWABLE,
+ COMMON.RX3_OBSERVABLE, COMMON.LISTENABLE_FUTURE, COMMON.LIVE_DATA,
+ COMMON.COMPUTABLE_LIVE_DATA, COMMON.PUBLISHER, COMMON.FLOW, COMMON.GUAVA_ROOM
+ )
+
+ runProcessorTest(
+ sources = commonSources + additionalSources + inputSource
+ ) { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+ .filterIsInstance<XTypeElement>()
+ .map {
+ Pair(
+ it,
+ it.getAllMethods().filter {
+ it.hasAnnotation(Insert::class)
+ }.toList()
+ )
+ }.first { it.second.isNotEmpty() }
+ val processor = InsertionMethodProcessor(
+ baseContext = invocation.context,
+ containing = owner.type,
+ executableElement = methods.first()
+ )
+ val processed = processor.process()
+ handler(processed, invocation)
+ }
+ }
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index 0223913..35285cf 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -25,9 +25,13 @@
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.GuavaUtilConcurrentTypeNames
import androidx.room.ext.KotlinTypeNames
import androidx.room.ext.LifecyclesTypeNames
import androidx.room.ext.PagingTypeNames
+import androidx.room.ext.ReactiveStreamsTypeNames
+import androidx.room.ext.RxJava2TypeNames
+import androidx.room.ext.RxJava3TypeNames
import androidx.room.ext.typeName
import androidx.room.parser.QueryType
import androidx.room.parser.Table
@@ -82,6 +86,20 @@
@Dao
abstract class MyClass {
"""
+ const val DAO_PREFIX_KT = """
+ package foo.bar
+ import androidx.room.*
+ import java.util.*
+ import io.reactivex.*
+ io.reactivex.rxjava3.core.*
+ androidx.lifecycle.*
+ com.google.common.util.concurrent.*
+ org.reactivestreams.*
+ kotlinx.coroutines.flow.*
+
+ @Dao
+ abstract class MyClass {
+ """
const val DAO_SUFFIX = "}"
val POJO: ClassName = ClassName.get("foo.bar", "MyClass.Pojo")
@Parameterized.Parameters(name = "enableDbVerification={0}")
@@ -1344,6 +1362,58 @@
}
}
+ private fun <T : QueryMethod> singleQueryMethodKotlin(
+ vararg input: String,
+ additionalSources: Iterable<Source> = emptyList(),
+ options: Map<String, String> = emptyMap(),
+ handler: (T, XTestInvocation) -> Unit
+ ) {
+ val inputSource = Source.kotlin(
+ "MyClass.kt",
+ DAO_PREFIX_KT + input.joinToString("\n") + DAO_SUFFIX
+ )
+ val commonSources = listOf(
+ COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
+ COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX2_FLOWABLE, COMMON.RX2_OBSERVABLE,
+ COMMON.RX3_COMPLETABLE, COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.RX3_FLOWABLE,
+ COMMON.RX3_OBSERVABLE, COMMON.LISTENABLE_FUTURE, COMMON.LIVE_DATA,
+ COMMON.COMPUTABLE_LIVE_DATA, COMMON.PUBLISHER, COMMON.FLOW, COMMON.GUAVA_ROOM
+ )
+
+ runProcessorTest(
+ sources = additionalSources + commonSources + inputSource,
+ options = options
+ ) { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+ .filterIsInstance<XTypeElement>()
+ .map { typeElement ->
+ Pair(
+ typeElement,
+ typeElement.getAllMethods().filter { method ->
+ method.hasAnnotation(Query::class)
+ }.toList()
+ )
+ }.first { it.second.isNotEmpty() }
+ val verifier = if (enableVerification) {
+ createVerifierFromEntitiesAndViews(invocation).also(
+ invocation.context::attachDatabaseVerifier
+ )
+ } else {
+ null
+ }
+ val parser = QueryMethodProcessor(
+ baseContext = invocation.context,
+ containing = owner.type,
+ executableElement = methods.first(),
+ dbVerifier = verifier
+ )
+ val parsedQuery = parser.process()
+ @Suppress("UNCHECKED_CAST")
+ handler(parsedQuery as T, invocation)
+ }
+ }
+
@Test
fun testInvalidLinkedListCollectionInMultimapJoin() {
singleQueryMethod<ReadQueryMethod>(
@@ -1611,4 +1681,37 @@
}
}
}
+
+ @Test
+ fun suspendReturnsDeferredType() {
+ listOf(
+ "${RxJava2TypeNames.FLOWABLE}<Int>",
+ "${RxJava2TypeNames.OBSERVABLE}<Int>",
+ "${RxJava2TypeNames.MAYBE}<Int>",
+ "${RxJava2TypeNames.SINGLE}<Int>",
+ "${RxJava2TypeNames.COMPLETABLE}",
+ "${RxJava3TypeNames.FLOWABLE}<Int>",
+ "${RxJava3TypeNames.OBSERVABLE}<Int>",
+ "${RxJava3TypeNames.MAYBE}<Int>",
+ "${RxJava3TypeNames.SINGLE}<Int>",
+ "${RxJava3TypeNames.COMPLETABLE}",
+ "${LifecyclesTypeNames.LIVE_DATA}<Int>",
+ "${LifecyclesTypeNames.COMPUTABLE_LIVE_DATA}<Int>",
+ "${GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE}<Int>",
+ "${ReactiveStreamsTypeNames.PUBLISHER}<Int>",
+ "${KotlinTypeNames.FLOW}<Int>"
+ ).forEach { type ->
+ singleQueryMethodKotlin<WriteQueryMethod>(
+ """
+ @Query("DELETE from User where uid = :id")
+ abstract suspend fun foo(id: Int): $type
+ """
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ val rawTypeName = type.substringBefore("<")
+ hasErrorContaining(ProcessorErrors.suspendReturnsDeferredType(rawTypeName))
+ }
+ }
+ }
+ }
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
index 9acb444..78d8ba7 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -23,7 +23,13 @@
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.ext.GuavaUtilConcurrentTypeNames
+import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.LifecyclesTypeNames
import androidx.room.ext.PagingTypeNames
+import androidx.room.ext.ReactiveStreamsTypeNames
+import androidx.room.ext.RxJava2TypeNames
+import androidx.room.ext.RxJava3TypeNames
import androidx.room.ext.SupportDbTypeNames
import androidx.room.processor.ProcessorErrors.RAW_QUERY_STRING_PARAMETER_REMOVED
import androidx.room.testing.context
@@ -572,6 +578,39 @@
}
}
+ @Test
+ fun suspendReturnsDeferredType() {
+ listOf(
+ "${RxJava2TypeNames.FLOWABLE}<Int>",
+ "${RxJava2TypeNames.OBSERVABLE}<Int>",
+ "${RxJava2TypeNames.MAYBE}<Int>",
+ "${RxJava2TypeNames.SINGLE}<Int>",
+ "${RxJava2TypeNames.COMPLETABLE}",
+ "${RxJava3TypeNames.FLOWABLE}<Int>",
+ "${RxJava3TypeNames.OBSERVABLE}<Int>",
+ "${RxJava3TypeNames.MAYBE}<Int>",
+ "${RxJava3TypeNames.SINGLE}<Int>",
+ "${RxJava3TypeNames.COMPLETABLE}",
+ "${LifecyclesTypeNames.LIVE_DATA}<Int>",
+ "${LifecyclesTypeNames.COMPUTABLE_LIVE_DATA}<Int>",
+ "${GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE}<Int>",
+ "${ReactiveStreamsTypeNames.PUBLISHER}<Int>",
+ "${KotlinTypeNames.FLOW}<Int>"
+ ).forEach { type ->
+ singleQueryMethodKotlin(
+ """
+ @RawQuery
+ abstract suspend fun foo(query: SupportSQLiteQuery): $type
+ """
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ val rawTypeName = type.substringBefore("<")
+ hasErrorContaining(ProcessorErrors.suspendReturnsDeferredType(rawTypeName))
+ }
+ }
+ }
+ }
+
private fun singleQueryMethod(
vararg input: String,
handler: (RawQueryMethod, XTestInvocation) -> Unit
@@ -612,6 +651,47 @@
}
}
+ private fun singleQueryMethodKotlin(
+ vararg input: String,
+ handler: (RawQueryMethod, XTestInvocation) -> Unit
+ ) {
+ val inputSource = Source.kotlin(
+ "MyClass.kt",
+ DAO_PREFIX_KT +
+ input.joinToString("\n") +
+ DAO_SUFFIX
+ )
+ val commonSources = listOf(
+ COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
+ COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX2_FLOWABLE, COMMON.RX2_OBSERVABLE,
+ COMMON.RX3_COMPLETABLE, COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.RX3_FLOWABLE,
+ COMMON.RX3_OBSERVABLE, COMMON.LISTENABLE_FUTURE, COMMON.LIVE_DATA,
+ COMMON.COMPUTABLE_LIVE_DATA, COMMON.PUBLISHER, COMMON.FLOW, COMMON.GUAVA_ROOM
+ )
+ runProcessorTest(
+ sources = commonSources + inputSource
+ ) { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+ .filterIsInstance<XTypeElement>()
+ .map {
+ Pair(
+ it,
+ it.getAllMethods().filter {
+ it.hasAnnotation(RawQuery::class)
+ }.toList()
+ )
+ }.first { it.second.isNotEmpty() }
+ val parser = RawQueryMethodProcessor(
+ baseContext = invocation.context,
+ containing = owner.type,
+ executableElement = methods.first()
+ )
+ val parsedQuery = parser.process()
+ handler(parsedQuery, invocation)
+ }
+ }
+
companion object {
private const val DAO_PREFIX = """
package foo.bar;
@@ -624,6 +704,20 @@
@Dao
abstract class MyClass {
"""
+ const val DAO_PREFIX_KT = """
+ package foo.bar
+ import androidx.room.*
+ import java.util.*
+ import io.reactivex.*
+ io.reactivex.rxjava3.core.*
+ androidx.lifecycle.*
+ com.google.common.util.concurrent.*
+ org.reactivestreams.*
+ kotlinx.coroutines.flow.*
+
+ @Dao
+ abstract class MyClass {
+ """
private const val DAO_SUFFIX = "}"
}
}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
index 72e4704..bbf5cbe 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
@@ -26,6 +26,9 @@
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.GuavaUtilConcurrentTypeNames
+import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.LifecyclesTypeNames
+import androidx.room.ext.ReactiveStreamsTypeNames
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
import androidx.room.testing.context
@@ -53,6 +56,21 @@
@Dao
abstract class MyClass {
"""
+ const val DAO_PREFIX_KT = """
+ package foo.bar
+ import androidx.room.*
+ import java.util.*
+ import io.reactivex.*
+ io.reactivex.rxjava3.core.*
+ androidx.lifecycle.*
+ com.google.common.util.concurrent.*
+ org.reactivestreams.*
+ kotlinx.coroutines.flow.*
+
+ @Dao
+ abstract class MyClass {
+ """
+
const val DAO_SUFFIX = "}"
val USER_TYPE_NAME: TypeName = COMMON.USER_TYPE_NAME
val USERNAME_TYPE_NAME: TypeName = ClassName.get("foo.bar", "Username")
@@ -342,6 +360,39 @@
}
@Test
+ fun suspendReturnsDeferredType() {
+ listOf(
+ "${RxJava2TypeNames.FLOWABLE}<Int>",
+ "${RxJava2TypeNames.OBSERVABLE}<Int>",
+ "${RxJava2TypeNames.MAYBE}<Int>",
+ "${RxJava2TypeNames.SINGLE}<Int>",
+ "${RxJava2TypeNames.COMPLETABLE}",
+ "${RxJava3TypeNames.FLOWABLE}<Int>",
+ "${RxJava3TypeNames.OBSERVABLE}<Int>",
+ "${RxJava3TypeNames.MAYBE}<Int>",
+ "${RxJava3TypeNames.SINGLE}<Int>",
+ "${RxJava3TypeNames.COMPLETABLE}",
+ "${LifecyclesTypeNames.LIVE_DATA}<Int>",
+ "${LifecyclesTypeNames.COMPUTABLE_LIVE_DATA}<Int>",
+ "${GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE}<Int>",
+ "${ReactiveStreamsTypeNames.PUBLISHER}<Int>",
+ "${KotlinTypeNames.FLOW}<Int>"
+ ).forEach { type ->
+ singleShortcutMethodKotlin(
+ """
+ @${annotation.java.canonicalName}
+ abstract suspend fun foo(user: User): $type
+ """
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ val rawTypeName = type.substringBefore("<")
+ hasErrorContaining(ProcessorErrors.suspendReturnsDeferredType(rawTypeName))
+ }
+ }
+ }
+ }
+
+ @Test
fun targetEntity() {
val usernameSource = Source.java(
"foo.bar.Username",
@@ -621,4 +672,46 @@
handler(processed, invocation)
}
}
+
+ fun singleShortcutMethodKotlin(
+ vararg input: String,
+ additionalSources: List<Source> = emptyList(),
+ handler: (T, XTestInvocation) -> Unit
+ ) {
+ val inputSource = Source.kotlin(
+ "MyClass.kt",
+ DAO_PREFIX_KT + input.joinToString("\n") + DAO_SUFFIX
+ )
+
+ val commonSources = listOf(
+ COMMON.USER, COMMON.BOOK, COMMON.NOT_AN_ENTITY, COMMON.RX2_COMPLETABLE,
+ COMMON.RX2_MAYBE, COMMON.RX2_SINGLE, COMMON.RX2_FLOWABLE, COMMON.RX2_OBSERVABLE,
+ COMMON.RX3_COMPLETABLE, COMMON.RX3_MAYBE, COMMON.RX3_SINGLE, COMMON.RX3_FLOWABLE,
+ COMMON.RX3_OBSERVABLE, COMMON.LISTENABLE_FUTURE, COMMON.LIVE_DATA,
+ COMMON.COMPUTABLE_LIVE_DATA, COMMON.PUBLISHER, COMMON.FLOW, COMMON.GUAVA_ROOM
+ )
+
+ runProcessorTest(
+ sources = commonSources + additionalSources + inputSource
+ ) { invocation ->
+ val (owner, methods) = invocation.roundEnv
+ .getElementsAnnotatedWith(Dao::class.qualifiedName!!)
+ .filterIsInstance<XTypeElement>()
+ .map {
+ Pair(
+ it,
+ it.getAllMethods().filter {
+ it.hasAnnotation(annotation)
+ }.toList()
+ )
+ }.first { it.second.isNotEmpty() }
+
+ val processed = process(
+ baseContext = invocation.context,
+ containing = owner.type,
+ executableElement = methods.first()
+ )
+ handler(processed, invocation)
+ }
+ }
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
index 1dc88b9..4d72db8 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
@@ -42,6 +42,7 @@
import java.util.*;
import androidx.lifecycle.*;
import com.google.common.util.concurrent.*;
+ import org.reactivestreams.*;
@Dao
abstract class MyClass {
"""
@@ -133,6 +134,25 @@
}
@Test
+ fun deferredReturnType_computableLiveData() {
+ singleTransactionMethod(
+ """
+ @Transaction
+ public ComputableLiveData<String> doInTransaction(int param) { return null; }
+ """
+ ) { transaction, invocation ->
+ assertThat(transaction.jvmName, `is`("doInTransaction"))
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.transactionMethodAsync(
+ "androidx.lifecycle.ComputableLiveData"
+ )
+ )
+ }
+ }
+ }
+
+ @Test
fun deferredReturnType_rx2_flowable() {
singleTransactionMethod(
"""
@@ -271,6 +291,25 @@
}
}
+ @Test
+ fun deferredReturnType_publisher() {
+ singleTransactionMethod(
+ """
+ @Transaction
+ public Publisher<String> doInTransaction(int param) { return null; }
+ """
+ ) { transaction, invocation ->
+ assertThat(transaction.jvmName, `is`("doInTransaction"))
+ invocation.assertCompilationResult {
+ hasErrorContaining(
+ ProcessorErrors.transactionMethodAsync(
+ "org.reactivestreams.Publisher"
+ )
+ )
+ }
+ }
+ }
+
private val TransactionMethod.jvmName: String
get() = element.jvmName
@@ -285,8 +324,8 @@
)
)
val otherSources = listOf(
- COMMON.LIVE_DATA, COMMON.RX2_FLOWABLE, COMMON.PUBLISHER, COMMON.RX2_COMPLETABLE,
- COMMON.RX2_SINGLE, COMMON.RX3_FLOWABLE, COMMON.RX3_COMPLETABLE,
+ COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.RX2_FLOWABLE, COMMON.PUBLISHER,
+ COMMON.RX2_COMPLETABLE, COMMON.RX2_SINGLE, COMMON.RX3_FLOWABLE, COMMON.RX3_COMPLETABLE,
COMMON.RX3_SINGLE, COMMON.LISTENABLE_FUTURE, COMMON.FLOW
)
runProcessorTest(