Merge "Implementing a check for verifying that equals() and hashCode() are implemented by the key of a multimap query result." into androidx-main
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index a7a1d84..3ad7e2a 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -215,6 +215,7 @@
field public static final String CANNOT_CREATE_VERIFICATION_DATABASE = "ROOM_CANNOT_CREATE_VERIFICATION_DATABASE";
field public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
field public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+ field public static final String DOES_NOT_IMPLEMENT_EQUALS_HASHCODE = "ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE";
field public static final String INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED = "ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
diff --git a/room/room-common/api/public_plus_experimental_current.txt b/room/room-common/api/public_plus_experimental_current.txt
index a7a1d84..3ad7e2a 100644
--- a/room/room-common/api/public_plus_experimental_current.txt
+++ b/room/room-common/api/public_plus_experimental_current.txt
@@ -215,6 +215,7 @@
field public static final String CANNOT_CREATE_VERIFICATION_DATABASE = "ROOM_CANNOT_CREATE_VERIFICATION_DATABASE";
field public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
field public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+ field public static final String DOES_NOT_IMPLEMENT_EQUALS_HASHCODE = "ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE";
field public static final String INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED = "ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index 220d387..722a507 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -224,6 +224,7 @@
field public static final String CANNOT_CREATE_VERIFICATION_DATABASE = "ROOM_CANNOT_CREATE_VERIFICATION_DATABASE";
field public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
field public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+ field public static final String DOES_NOT_IMPLEMENT_EQUALS_HASHCODE = "ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE";
field public static final String INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED = "ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
diff --git a/room/room-common/src/main/java/androidx/room/RoomWarnings.java b/room/room-common/src/main/java/androidx/room/RoomWarnings.java
index 919e42f..df8868b 100644
--- a/room/room-common/src/main/java/androidx/room/RoomWarnings.java
+++ b/room/room-common/src/main/java/androidx/room/RoomWarnings.java
@@ -31,6 +31,13 @@
public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
/**
+ * The warning dispatched by Room when the object in the provided method's multimap return
+ * type does not implement equals() and hashCode().
+ */
+ public static final String DOES_NOT_IMPLEMENT_EQUALS_HASHCODE =
+ "ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE";
+
+ /**
* Reported when Room cannot verify database queries during compilation due to lack of
* tmp dir access in JVM.
*/
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
index cc94880..f003876 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
@@ -21,6 +21,8 @@
import androidx.room.compiler.processing.isKotlinUnit
import androidx.room.compiler.processing.isVoid
import androidx.room.compiler.processing.isVoidObject
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
/**
* Returns `true` if this type is not the `void` type.
@@ -51,3 +53,36 @@
* Returns `true` if this is not `byte` type.
*/
fun XType.isNotByte() = !isByte()
+
+/**
+ * Checks if the class of the provided type has the equals() and hashCode() methods declared.
+ * If they are not found at the current class level, the method recursively moves on to the
+ * super class level and continues to look for these declared methods.
+ */
+fun XType.implementsEqualsAndHashcode(): Boolean {
+ if (this.typeName.isPrimitive || this.typeName.isBoxedPrimitive) {
+ return true
+ }
+ val typeElement = this.typeElement ?: return false
+
+ if (typeElement.className == ClassName.OBJECT) {
+ return false
+ }
+
+ val hasEquals = typeElement.getDeclaredMethods().any {
+ it.name == "equals" &&
+ it.returnType.typeName == TypeName.BOOLEAN &&
+ it.parameters.count() == 1 &&
+ it.parameters[0].type.typeName == TypeName.OBJECT
+ }
+ val hasHashCode = typeElement.getDeclaredMethods().any {
+ it.name == "hashCode" &&
+ it.returnType.typeName == TypeName.INT &&
+ it.parameters.count() == 0
+ }
+
+ if (hasEquals && hasHashCode) {
+ return true
+ }
+ return typeElement.superType?.let { it.implementsEqualsAndHashcode() } ?: false
+}
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 eecf7b1..656b9e8 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
@@ -132,6 +132,10 @@
fun cannotFindQueryResultAdapter(returnTypeName: TypeName) = "Not sure how to convert a " +
"Cursor to this method's return type ($returnTypeName)."
+ fun classMustImplementEqualsAndHashCode(mapType: TypeName, keyType: TypeName) = "The key" +
+ " of the provided method's multimap return type ($mapType) must implement equals() and " +
+ "hashCode(). Key type is: $keyType."
+
val INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT = "Method annotated with" +
" @Insert but does not have any parameters to insert."
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 e42de73..ae7a141 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
@@ -21,6 +21,7 @@
import androidx.room.compiler.processing.isEnum
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.implementsEqualsAndHashcode
import androidx.room.ext.isEntityElement
import androidx.room.ext.isNotByte
import androidx.room.ext.isNotKotlinUnit
@@ -33,6 +34,7 @@
import androidx.room.processor.FieldProcessor
import androidx.room.processor.PojoProcessor
import androidx.room.processor.ProcessorErrors.DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP
+import androidx.room.processor.ProcessorErrors.classMustImplementEqualsAndHashCode
import androidx.room.solver.binderprovider.CoroutineFlowResultBinderProvider
import androidx.room.solver.binderprovider.CursorQueryResultBinderProvider
import androidx.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
@@ -96,13 +98,14 @@
import androidx.room.solver.types.StringColumnTypeAdapter
import androidx.room.solver.types.TypeConverter
import androidx.room.vo.ShortcutQueryParameter
+import androidx.room.vo.Warning
import com.google.common.annotations.VisibleForTesting
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableListMultimap
import com.google.common.collect.ImmutableMultimap
import com.google.common.collect.ImmutableSetMultimap
-import com.squareup.javapoet.ClassName
import com.google.common.collect.ImmutableMap
+import com.squareup.javapoet.ClassName
import java.util.LinkedList
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@@ -521,9 +524,6 @@
immutableClassName = immutableClassName
)
} else if (typeMirror.isTypeOf(java.util.Map::class)) {
- // TODO: Handle nested collection values in the map
- // TODO: Verify that hashCode() and equals() are declared by the keyTypeArg
-
val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
val mapValueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
@@ -534,9 +534,19 @@
)
return null
}
+ // TODO: Handle nested collection values in the map
+ if (!keyTypeArg.implementsEqualsAndHashcode()) {
+ context.logger.w(
+ Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
+ keyTypeArg.typeElement,
+ classMustImplementEqualsAndHashCode(
+ typeMirror.typeName,
+ keyTypeArg.typeName
+ )
+ )
+ }
val collectionTypeRaw = context.COMMON_TYPES.READONLY_COLLECTION.rawType
-
if (collectionTypeRaw.isAssignableFrom(mapValueTypeArg.rawType)) {
// The Map's value type argument is assignable to a Collection, we need to make
// sure it is either a list or a set.
@@ -544,8 +554,7 @@
mapValueTypeArg.isTypeOf(java.util.List::class) ||
mapValueTypeArg.isTypeOf(java.util.Set::class)
) {
- val valueTypeArg =
- mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
+ val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
return MapQueryResultAdapter(
keyTypeArg = keyTypeArg,
valueTypeArg = valueTypeArg,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
index e825f29..fc3bd6a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
@@ -25,6 +25,7 @@
enum class Warning(val publicKey: String) {
ALL("ALL"),
CURSOR_MISMATCH("ROOM_CURSOR_MISMATCH"),
+ DOES_NOT_IMPLEMENT_EQUALS_HASHCODE("ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE"),
MISSING_JAVA_TMP_DIR("ROOM_MISSING_JAVA_TMP_DIR"),
CANNOT_CREATE_VERIFICATION_DATABASE("ROOM_CANNOT_CREATE_VERIFICATION_DATABASE"),
PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED("ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED"),
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 8659c89..3e9bbfd 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
@@ -988,6 +988,18 @@
"""
public static class Username {
public String name;
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Username username = (Username) o;
+ if (name != username.name) return false;
+ return true;
+ }
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
}
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM User JOIN Relation ON (User.uid = Relation.userId)")
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 4b7ebf6..e9c4b7f 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
@@ -35,6 +35,7 @@
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
import androidx.room.ext.T
+import androidx.room.ext.implementsEqualsAndHashcode
import androidx.room.parser.SQLTypeAffinity
import androidx.room.processor.Context
import androidx.room.processor.CustomConverterProcessor
@@ -1139,6 +1140,152 @@
}
}
+ @Test
+ fun testEqualsAndHashcodeImplemented() {
+ val classExtendsClassWithEqualsAndHashcodeFunctions = Source.java(
+ "foo.bar.Human",
+ """
+ package foo.bar;
+ public class Human extends Username {
+ public String relationId;
+ }
+ """.trimIndent()
+ )
+ val classWithFncs = Source.java(
+ "foo.bar.Username",
+ """
+ package foo.bar;
+ public class Username extends Person {
+ public String name;
+ @Override
+ public boolean equals(Object o) {
+ return false;
+ }
+ @Override
+ public int hashCode() {
+ return 0;
+ }
+ }
+ """.trimIndent()
+ )
+ val classWithoutFncs = Source.java(
+ "foo.bar.Person",
+ """
+ package foo.bar;
+ public class Person {
+ public String userId;
+ }
+ """.trimIndent()
+ )
+ val enumClass = Source.java(
+ "foo.bar.Names",
+ """
+ package foo.bar;
+ public enum Names {
+ ELLA,
+ BOB,
+ JAMES
+ }
+ """.trimIndent()
+ )
+ val classWithWrongFncs = Source.java(
+ "foo.bar.UsernameWithWrongFncs",
+ """
+ package foo.bar;
+ public class UsernameWithWrongFncs {
+ public String name;
+ public boolean equals() {
+ return true;
+ }
+ public int hashCode(int num) {
+ return num;
+ }
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(
+ classExtendsClassWithEqualsAndHashcodeFunctions,
+ classWithFncs,
+ classWithoutFncs,
+ enumClass,
+ classWithWrongFncs
+ )
+ ) { invocation ->
+ val enumCase = invocation.processingEnv.requireTypeElement("foo.bar.Names")
+ val inheritedCase = invocation.processingEnv.requireTypeElement("foo.bar.Human")
+ val wrongFunctionsCase = invocation.processingEnv.requireTypeElement(
+ "foo.bar.UsernameWithWrongFncs"
+ )
+ val noEqualsOrHashcodeCase = invocation.processingEnv.requireTypeElement(
+ "foo.bar.Person"
+ )
+ assertThat(enumCase.type.implementsEqualsAndHashcode()).isTrue()
+ assertThat(inheritedCase.type.implementsEqualsAndHashcode()).isTrue()
+ assertThat(wrongFunctionsCase.type.implementsEqualsAndHashcode()).isFalse()
+ assertThat(noEqualsOrHashcodeCase.type.implementsEqualsAndHashcode()).isFalse()
+ }
+ }
+
+ @Test
+ fun testEqualsAndHashcodeCheckWithJavaPrimitive() {
+ val inputSource = Source.java(
+ "foo.bar.Subject",
+ """
+ package foo.bar;
+ public class Subject {
+ public int primitiveInt = 0;
+ public Integer boxedInt = 1;
+ public boolean primitiveBool = true;
+ public Boolean boxedBool = false;
+ public double primitiveDouble = 2.2;
+ public Double boxedDouble = 3.3;
+ public long primitiveLong = 4L;
+ public Long boxedLong = 5L;
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(
+ inputSource,
+ COMMON.USER,
+ COMMON.PAGING_SOURCE,
+ COMMON.LIMIT_OFFSET_PAGING_SOURCE,
+ ),
+ ) { invocation ->
+ val subjectTypeElement =
+ invocation.processingEnv.requireTypeElement("foo.bar.Subject")
+ subjectTypeElement.getAllFieldsIncludingPrivateSupers().forEach { field ->
+ assertThat(field.type.implementsEqualsAndHashcode()).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testEqualsAndHashcodeCheckWithKotlinPrimitive() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.*
+ class Subject {
+ val anInteger = 0
+ val aBoolean = true
+ val aDouble = 2.2
+ val aLong = 5L
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(source)
+ ) { invocation ->
+ val subjectTypeElement = invocation.processingEnv.requireTypeElement("Subject")
+
+ subjectTypeElement.getDeclaredFields().forEach {
+ assertThat(it.type.implementsEqualsAndHashcode()).isTrue()
+ }
+ }
+ }
+
private fun createIntListToStringBinders(invocation: XTestInvocation): List<TypeConverter> {
val intType = invocation.processingEnv.requireType(Integer::class)
val listElement = invocation.processingEnv.requireTypeElement(java.util.List::class)