Adding additional error coverage for MapInfo.

Adding checks for scenarios in which a Dao method with a map or multimap return type may have a key or value argument that either does not implement equals() and hashcode(), or is one of the built-in types that require further information (e.g String, primitive...).

Test: QueryMethodProcessorTest.kt
Change-Id: I5f908a596ad08d7181fe0547da23379cc5f5aaf0
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
index a8d63c4..76e5981 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
@@ -246,7 +246,6 @@
     ImmutableMap<Artist, ByteBuffer> getAllArtistsWithAlbumCoversRawQuery(SupportSQLiteQuery query);
 
     @RewriteQueriesToDropUnusedColumns
-
     @MapInfo(valueColumn = "mImageYear")
     @Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
     ImmutableMap<Artist, Long> getAllArtistsWithAlbumCoverYear();
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 f8fcf20..e97af36a 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
@@ -66,15 +66,9 @@
  * super class level and continues to look for these declared methods.
  */
 fun XType.implementsEqualsAndHashcode(): Boolean {
-    if (this.typeName.isPrimitive) return true
-    if (this.typeName.isBoxedPrimitive) return true
-    if (this.typeName == CommonTypeNames.STRING) return true
-    if (this.isTypeOf(ByteArray::class)) return true
-    if (this.isArray() && this.isByte()) return true
+    if (this.isSupportedMapTypeArg()) return true
 
     val typeElement = this.typeElement ?: return false
-
-    if (this.typeElement!!.isEnum()) return true
     if (typeElement.className == ClassName.OBJECT) {
         return false
     }
@@ -91,8 +85,22 @@
             it.parameters.count() == 0
     }
 
-    if (hasEquals && hasHashCode) {
-        return true
-    }
+    if (hasEquals && hasHashCode) return true
+
     return typeElement.superType?.let { it.implementsEqualsAndHashcode() } ?: false
 }
+
+/**
+ * Checks if the class of the provided type is one of the types supported in Dao functions with a
+ * Map or Multimap return type.
+ */
+fun XType.isSupportedMapTypeArg(): Boolean {
+    if (this.typeName.isPrimitive) return true
+    if (this.typeName.isBoxedPrimitive) return true
+    if (this.typeName == CommonTypeNames.STRING) return true
+    if (this.isTypeOf(ByteArray::class)) return true
+    if (this.isArray() && this.isByte()) return true
+    val typeElement = this.typeElement ?: return false
+    if (typeElement.isEnum()) return true
+    return 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 543b2fe9..d884c99 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
@@ -134,8 +134,8 @@
     fun cannotFindQueryResultAdapter(returnTypeName: TypeName) = "Not sure how to convert a " +
         "Cursor to this method's return type ($returnTypeName)."
 
-    fun classMustImplementEqualsAndHashCode(mapType: String, keyType: String) = "The key" +
-        " of the provided method's multimap return type ($mapType) must implement equals() and " +
+    fun classMustImplementEqualsAndHashCode(keyType: String) = "The key" +
+        " of the provided method's multimap return type must implement equals() and " +
         "hashCode(). Key type is: $keyType."
 
     val INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT = "Method annotated with" +
@@ -151,6 +151,22 @@
     val MAP_INFO_MUST_HAVE_AT_LEAST_ONE_COLUMN_PROVIDED = "To use the @MapInfo annotation, you " +
         "must provide either the key column name, value column name, or both."
 
+    fun keyMayNeedMapInfo(keyArg: TypeName): String {
+        return """
+            Looks like you may need to use @MapInfo to clarify the 'keyColumnName' needed for
+            the return type of a method. Type argument that needs
+            @MapInfo: $keyArg
+            """.trim()
+    }
+
+    fun valueMayNeedMapInfo(valueArg: TypeName): String {
+        return """
+            Looks like you may need to use @MapInfo to clarify the 'valueColumnName' needed for
+            the return type of a method. Type argument that needs
+            @MapInfo: $valueArg
+            """.trim()
+    }
+
     val CANNOT_FIND_DELETE_RESULT_ADAPTER = "Not sure how to handle delete method's " +
         "return type. Currently the supported return types are void, int or Int."
 
@@ -225,6 +241,10 @@
         return MISSING_PARAMETER_FOR_BIND.format(bindVarName.joinToString(", "))
     }
 
+    fun valueCollectionMustBeListOrSet(mapValueTypeName: TypeName): String {
+        return "Multimap 'value' collection type must be a List or Set. Found $mapValueTypeName."
+    }
+
     private val UNUSED_QUERY_METHOD_PARAMETER = "Unused parameter%s: %s"
     fun unusedQueryMethodParameter(unusedParams: List<String>): String {
         return UNUSED_QUERY_METHOD_PARAMETER.format(
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 8816693..3a55bb7 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,7 +21,6 @@
 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
@@ -34,7 +33,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.processor.ProcessorErrors.valueCollectionMustBeListOrSet
 import androidx.room.solver.binderprovider.CoroutineFlowResultBinderProvider
 import androidx.room.solver.binderprovider.CursorQueryResultBinderProvider
 import androidx.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
@@ -63,6 +62,7 @@
 import androidx.room.solver.query.result.ImmutableMapQueryResultAdapter
 import androidx.room.solver.query.result.ListQueryResultAdapter
 import androidx.room.solver.query.result.MapQueryResultAdapter
+import androidx.room.solver.query.result.MultimapQueryResultAdapter.Companion.validateMapTypeArgs
 import androidx.room.solver.query.result.OptionalQueryResultAdapter
 import androidx.room.solver.query.result.PojoRowAdapter
 import androidx.room.solver.query.result.QueryResultAdapter
@@ -100,7 +100,6 @@
 import androidx.room.solver.types.TypeConverter
 import androidx.room.vo.MapInfo
 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
@@ -513,17 +512,6 @@
                 valueTypeArg
             )
 
-            if (!keyTypeArg.implementsEqualsAndHashcode()) {
-                context.logger.w(
-                    Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
-                    keyTypeArg.typeElement,
-                    classMustImplementEqualsAndHashCode(
-                        typeMirror.typeName.toString(),
-                        keyTypeArg.typeName.toString()
-                    )
-                )
-            }
-
             val resultAdapter = findQueryResultAdapter(mapType, query, extras) ?: return null
             return ImmutableMapQueryResultAdapter(
                 keyTypeArg = keyTypeArg,
@@ -555,49 +543,39 @@
                 return null
             }
 
-            if (!keyTypeArg.implementsEqualsAndHashcode()) {
-                context.logger.w(
-                    Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
-                    keyTypeArg.typeElement,
-                    classMustImplementEqualsAndHashCode(
-                        typeMirror.typeName.toString(),
-                        keyTypeArg.typeName.toString()
-                    )
-                )
-            }
-
             // Get @MapInfo info if any (this might be null)
             val mapInfo = extras.getData(MapInfo::class)
+            val keyRowAdapter = findRowAdapter(
+                typeMirror = keyTypeArg,
+                query = query,
+                columnName = mapInfo?.keyColumnName
+            ) ?: return null
+
+            val valueRowAdapter = findRowAdapter(
+                typeMirror = valueTypeArg,
+                query = query,
+                columnName = mapInfo?.valueColumnName
+            ) ?: return null
+
+            validateMapTypeArgs(
+                keyTypeArg = keyTypeArg,
+                valueTypeArg = valueTypeArg,
+                keyReader = findCursorValueReader(keyTypeArg, null),
+                valueReader = findCursorValueReader(valueTypeArg, null),
+                mapInfo = mapInfo,
+                logger = context.logger
+            )
             return GuavaImmutableMultimapQueryResultAdapter(
                 keyTypeArg = keyTypeArg,
                 valueTypeArg = valueTypeArg,
-                keyRowAdapter = findRowAdapter(
-                    typeMirror = keyTypeArg,
-                    query = query,
-                    columnName = mapInfo?.keyColumnName
-                ) ?: return null,
-                valueRowAdapter = findRowAdapter(
-                    typeMirror = valueTypeArg,
-                    query = query,
-                    columnName = mapInfo?.valueColumnName
-                ) ?: return null,
+                keyRowAdapter = keyRowAdapter,
+                valueRowAdapter = valueRowAdapter,
                 immutableClassName = immutableClassName
             )
         } else if (typeMirror.isTypeOf(java.util.Map::class)) {
             val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
             val mapValueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
 
-            if (!keyTypeArg.implementsEqualsAndHashcode()) {
-                context.logger.w(
-                    Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
-                    keyTypeArg.typeElement,
-                    classMustImplementEqualsAndHashCode(
-                        typeMirror.typeName.toString(),
-                        keyTypeArg.typeName.toString()
-                    )
-                )
-            }
-
             if (mapValueTypeArg.typeElement == null) {
                 context.logger.e(
                     "Multimap 'value' collection type argument does not represent a class. " +
@@ -618,41 +596,65 @@
                     mapValueTypeArg.isTypeOf(java.util.Set::class)
                 ) {
                     val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
+
+                    val keyRowAdapter = findRowAdapter(
+                        typeMirror = keyTypeArg,
+                        query = query,
+                        columnName = mapInfo?.keyColumnName
+                    ) ?: return null
+
+                    val valueRowAdapter = findRowAdapter(
+                        typeMirror = valueTypeArg,
+                        query = query,
+                        columnName = mapInfo?.valueColumnName
+                    ) ?: return null
+
+                    validateMapTypeArgs(
+                        keyTypeArg = keyTypeArg,
+                        valueTypeArg = valueTypeArg,
+                        keyReader = findCursorValueReader(keyTypeArg, null),
+                        valueReader = findCursorValueReader(valueTypeArg, null),
+                        mapInfo = mapInfo,
+                        logger = context.logger
+                    )
+
                     return MapQueryResultAdapter(
                         keyTypeArg = keyTypeArg,
                         valueTypeArg = valueTypeArg,
-                        keyRowAdapter = findRowAdapter(
-                            typeMirror = keyTypeArg,
-                            query = query,
-                            columnName = mapInfo?.keyColumnName
-                        ) ?: return null,
-                        valueRowAdapter = findRowAdapter(
-                            typeMirror = valueTypeArg,
-                            query = query,
-                            columnName = mapInfo?.valueColumnName
-                        ) ?: return null,
+                        keyRowAdapter = keyRowAdapter,
+                        valueRowAdapter = valueRowAdapter,
                         valueCollectionType = mapValueTypeArg
                     )
                 } else {
                     context.logger.e(
-                        "Multimap 'value' collection type must be a List or Set. Found " +
-                            "${mapValueTypeArg.typeName}."
+                        valueCollectionMustBeListOrSet(mapValueTypeArg.typeName)
                     )
                 }
             } else {
+                val keyRowAdapter = findRowAdapter(
+                    typeMirror = keyTypeArg,
+                    query = query,
+                    columnName = mapInfo?.keyColumnName
+                ) ?: return null
+                val valueRowAdapter = findRowAdapter(
+                    typeMirror = mapValueTypeArg,
+                    query = query,
+                    columnName = mapInfo?.valueColumnName
+                ) ?: return null
+
+                validateMapTypeArgs(
+                    keyTypeArg = keyTypeArg,
+                    valueTypeArg = mapValueTypeArg,
+                    keyReader = findCursorValueReader(keyTypeArg, null),
+                    valueReader = findCursorValueReader(mapValueTypeArg, null),
+                    mapInfo = mapInfo,
+                    logger = context.logger
+                )
                 return MapQueryResultAdapter(
                     keyTypeArg = keyTypeArg,
                     valueTypeArg = mapValueTypeArg,
-                    keyRowAdapter = findRowAdapter(
-                        typeMirror = keyTypeArg,
-                        query = query,
-                        columnName = mapInfo?.keyColumnName
-                    ) ?: return null,
-                    valueRowAdapter = findRowAdapter(
-                        typeMirror = mapValueTypeArg,
-                        query = query,
-                        columnName = mapInfo?.valueColumnName
-                    ) ?: return null,
+                    keyRowAdapter = keyRowAdapter,
+                    valueRowAdapter = valueRowAdapter,
                     valueCollectionType = null
                 )
             }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
index 2acfb6d2..5e44a0e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
@@ -17,6 +17,12 @@
 package androidx.room.solver.query.result
 
 import androidx.room.compiler.processing.XType
+import androidx.room.ext.implementsEqualsAndHashcode
+import androidx.room.log.RLog
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.types.CursorValueReader
+import androidx.room.vo.MapInfo
+import androidx.room.vo.Warning
 
 /**
  * Common interface for Map and Multimap result adapters.
@@ -24,4 +30,47 @@
 interface MultimapQueryResultAdapter {
     val keyTypeArg: XType
     val valueTypeArg: XType
-}
\ No newline at end of file
+
+    companion object {
+        /**
+         * Checks if the @MapInfo annotation is needed for clarification regarding the return type
+         * of a Dao method.
+         */
+        fun validateMapTypeArgs(
+            keyTypeArg: XType,
+            valueTypeArg: XType,
+            keyReader: CursorValueReader?,
+            valueReader: CursorValueReader?,
+            mapInfo: MapInfo?,
+            logger: RLog
+        ) {
+
+            if (!keyTypeArg.implementsEqualsAndHashcode()) {
+                logger.w(
+                    Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
+                    ProcessorErrors.classMustImplementEqualsAndHashCode(
+                        keyTypeArg.typeName.toString()
+                    )
+                )
+            }
+
+            val hasKeyColumnName = mapInfo?.keyColumnName?.isNotEmpty() ?: false
+            if (!hasKeyColumnName && keyReader != null) {
+                logger.e(
+                    ProcessorErrors.keyMayNeedMapInfo(
+                        keyTypeArg.typeName
+                    )
+                )
+            }
+
+            val hasValueColumnName = mapInfo?.valueColumnName?.isNotEmpty() ?: false
+            if (!hasValueColumnName && valueReader != null) {
+                logger.e(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        valueTypeArg.typeName
+                    )
+                )
+            }
+        }
+    }
+}
diff --git a/room/room-compiler/src/test/data/common/input/DateConverter.java b/room/room-compiler/src/test/data/common/input/DateConverter.java
new file mode 100644
index 0000000..cc05818
--- /dev/null
+++ b/room/room-compiler/src/test/data/common/input/DateConverter.java
@@ -0,0 +1,28 @@
+/*
+ * 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 foo.bar;
+import androidx.room.TypeConverter;
+
+import java.util.Date;
+
+public class DateConverter {
+    @TypeConverter
+    public static Date toDate(Long input) {return null;}
+
+    @TypeConverter
+    public static Long fromDate(Date input) {return null;}
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/data/common/input/Image.java b/room/room-compiler/src/test/data/common/input/Image.java
new file mode 100644
index 0000000..d40cee9
--- /dev/null
+++ b/room/room-compiler/src/test/data/common/input/Image.java
@@ -0,0 +1,44 @@
+/*
+ * 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 foo.bar;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+import androidx.room.TypeConverters;
+
+import java.util.Date;
+
+@Entity
+@TypeConverters(DateConverter.class)
+public class Image {
+    @PrimaryKey
+    public final int mImageId;
+    public final Long mImageYear;
+    public final String mArtistInImage;
+    public final byte[] mAlbumCover;
+    public final Date mDateReleased;
+    public final ImageFormat mFormat;
+
+    public Image(int imageId, Long imageYear, String artistInImage, byte[] albumCover,
+            Date dateReleased, ImageFormat format) {
+        mImageId = imageId;
+        mImageYear = imageYear;
+        mArtistInImage = artistInImage;
+        mAlbumCover = albumCover;
+        mDateReleased = dateReleased;
+        mFormat = format;
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/data/common/input/ImageFormat.java b/room/room-compiler/src/test/data/common/input/ImageFormat.java
new file mode 100644
index 0000000..8150b4d
--- /dev/null
+++ b/room/room-compiler/src/test/data/common/input/ImageFormat.java
@@ -0,0 +1,22 @@
+/*
+ * 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 foo.bar;
+public enum ImageFormat {
+    JPG,
+    GIF,
+    MPEG
+}
\ No newline at end of file
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 f0e05bf..98f36d2 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
@@ -34,6 +34,8 @@
 import androidx.room.processor.ProcessorErrors.DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP
 import androidx.room.processor.ProcessorErrors.MAP_INFO_MUST_HAVE_AT_LEAST_ONE_COLUMN_PROVIDED
 import androidx.room.processor.ProcessorErrors.cannotFindQueryResultAdapter
+import androidx.room.processor.ProcessorErrors.keyMayNeedMapInfo
+import androidx.room.processor.ProcessorErrors.valueMayNeedMapInfo
 import androidx.room.solver.query.result.DataSourceFactoryQueryResultBinder
 import androidx.room.solver.query.result.ListQueryResultAdapter
 import androidx.room.solver.query.result.LiveDataQueryResultBinder
@@ -76,6 +78,7 @@
                 import androidx.annotation.NonNull;
                 import androidx.room.*;
                 import java.util.*;
+                import com.google.common.collect.*;
                 @Dao
                 abstract class MyClass {
                 """
@@ -1304,7 +1307,8 @@
         )
         val commonSources = listOf(
             COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER, COMMON.BOOK,
-            COMMON.NOT_AN_ENTITY, COMMON.ARTIST, COMMON.SONG
+            COMMON.NOT_AN_ENTITY, COMMON.ARTIST, COMMON.SONG, COMMON.IMAGE, COMMON.IMAGE_FORMAT,
+            COMMON.CONVERTER
         )
         runProcessorTest(
             sources = additionalSources + commonSources + inputSource,
@@ -1404,7 +1408,6 @@
                 hasWarningCount(1)
                 hasWarningContaining(
                     ProcessorErrors.classMustImplementEqualsAndHashCode(
-                        "java.util.Map<foo.bar.User, foo.bar.Book>",
                         "foo.bar.User"
                     )
                 )
@@ -1413,6 +1416,171 @@
     }
 
     @Test
+    fun testMissingMapInfoOneToOneString() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @Query("select * from Artist JOIN Song ON Artist.mArtistName == Song.mArtist")
+                abstract Map<Artist, String> getAllArtistsWithAlbumCoverYear();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "String")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testOneToOneStringMapInfoForKeyInsteadOfColumn() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @MapInfo(keyColumn = "mArtistName")
+                @Query("select * from Artist JOIN Song ON Artist.mArtistName == Song.mArtist")
+                abstract Map<Artist, String> getAllArtistsWithAlbumCoverYear();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "String")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoOneToManyString() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @Query("select * from Artist JOIN Song ON Artist.mArtistName == Song.mArtist")
+                abstract Map<Artist, List<String>> getAllArtistsWithAlbumCoverYear();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "String")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoImmutableListMultimapOneToOneString() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @Query("select * from Artist JOIN Song ON Artist.mArtistName == Song.mArtist")
+                abstract ImmutableListMultimap<Artist, String> getAllArtistsWithAlbumCoverYear();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "String")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoOneToOneLong() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
+                Map<Artist, Long> getAllArtistsWithAlbumCoverYear();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "Long")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoOneToManyLong() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
+                Map<Artist, Set<Long>> getAllArtistsWithAlbumCoverYear();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "Long")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoImmutableListMultimapOneToOneLong() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
+                ImmutableListMultimap<Artist, Long> getAllArtistsWithAlbumCoverYear();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "Long")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoImmutableListMultimapOneToOneTypeConverterKey() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @TypeConverters(DateConverter.class)
+                @Query("SELECT * FROM Image JOIN Artist ON Artist.mArtistName = Image.mArtistInImage")
+                ImmutableMap<java.util.Date, Artist> getAlbumDateWithBandActivity();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    keyMayNeedMapInfo(
+                        ClassName.get("java.util", "Date")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoImmutableListMultimapOneToOneTypeConverterValue() {
+        singleQueryMethod<ReadQueryMethod>(
+            """
+                @TypeConverters(DateConverter.class)
+                @Query("SELECT * FROM Artist JOIN Image ON Artist.mArtistName = Image.mArtistInImage")
+                ImmutableMap<Artist, java.util.Date> getAlbumDateWithBandActivity();
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    valueMayNeedMapInfo(
+                        ClassName.get("java.util", "Date")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
     fun testUseMapInfoWithColumnsNotInQuery() {
         if (!enableVerification) {
             return
@@ -1428,7 +1596,6 @@
                 hasWarningCount(1)
                 hasWarningContaining(
                     ProcessorErrors.classMustImplementEqualsAndHashCode(
-                        "java.util.Map<foo.bar.User, foo.bar.Book>",
                         "foo.bar.User"
                     )
                 )
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 721c703..9acb444 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
@@ -400,7 +400,6 @@
                 hasWarningCount(1)
                 hasWarningContaining(
                     ProcessorErrors.classMustImplementEqualsAndHashCode(
-                        "java.util.Map<foo.bar.User, foo.bar.Book>",
                         "foo.bar.User"
                     )
                 )
@@ -408,6 +407,171 @@
         }
     }
 
+    @Test
+    fun testMissingMapInfoOneToOneString() {
+        singleQueryMethod(
+            """
+                @RawQuery
+                abstract Map<Artist, String> getAllArtistsWithAlbumCoverYear(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "String")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoOneToManyString() {
+        singleQueryMethod(
+            """
+                @RawQuery
+                abstract Map<Artist, List<String>> getAllArtistsWithAlbumCoverYear(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "String")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoImmutableListMultimapOneToOneString() {
+        singleQueryMethod(
+            """
+                @RawQuery
+                abstract ImmutableListMultimap<Artist, String> getAllArtistsWithAlbumCoverYear(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "String")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoOneToOneLong() {
+        singleQueryMethod(
+            """
+                @RawQuery
+                Map<Artist, Long> getAllArtistsWithAlbumCoverYear(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "Long")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoOneToManyLong() {
+        singleQueryMethod(
+            """
+                @RawQuery
+                Map<Artist, Set<Long>> getAllArtistsWithAlbumCoverYear(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "Long")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoImmutableListMultimapOneToOneLong() {
+        singleQueryMethod(
+            """
+                @RawQuery
+                ImmutableListMultimap<Artist, Long> getAllArtistsWithAlbumCoverYear(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "Long")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoImmutableListMultimapOneToOneTypeConverterKey() {
+        singleQueryMethod(
+            """
+                @TypeConverters(DateConverter.class)
+                @RawQuery
+                ImmutableMap<java.util.Date, Artist> getAlbumDateWithBandActivity(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.keyMayNeedMapInfo(
+                        ClassName.get("java.util", "Date")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testMissingMapInfoImmutableListMultimapOneToOneTypeConverterValue() {
+        singleQueryMethod(
+            """
+                @TypeConverters(DateConverter.class)
+                @RawQuery
+                ImmutableMap<Artist, java.util.Date> getAlbumDateWithBandActivity(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        ClassName.get("java.util", "Date")
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun testOneToOneStringMapInfoForKeyInsteadOfColumn() {
+        singleQueryMethod(
+            """
+                @MapInfo(keyColumn = "mArtistName")
+                @RawQuery
+                abstract Map<Artist, String> getAllArtistsWithAlbumCoverYear(SupportSQLiteQuery query);
+            """
+        ) { _, invocation ->
+            invocation.assertCompilationResult {
+                hasErrorContaining(
+                    ProcessorErrors.valueMayNeedMapInfo(
+                        ClassName.get("java.lang", "String")
+                    )
+                )
+            }
+        }
+    }
+
     private fun singleQueryMethod(
         vararg input: String,
         handler: (RawQueryMethod, XTestInvocation) -> Unit
@@ -421,7 +585,8 @@
         val commonSources = listOf(
             COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
             COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE,
-            COMMON.NOT_AN_ENTITY, COMMON.BOOK, COMMON.ARTIST, COMMON.SONG
+            COMMON.NOT_AN_ENTITY, COMMON.BOOK, COMMON.ARTIST, COMMON.SONG, COMMON.IMAGE,
+            COMMON.IMAGE_FORMAT, COMMON.CONVERTER
         )
         runProcessorTest(
             sources = commonSources + inputSource
@@ -455,6 +620,7 @@
                 import androidx.sqlite.db.SupportSQLiteQuery;
                 import androidx.lifecycle.LiveData;
                 import java.util.*;
+                import com.google.common.collect.*;
                 @Dao
                 abstract class MyClass {
                 """
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index be1bdc8..31e60b3 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -49,6 +49,18 @@
         loadJavaCode("common/input/Artist.java", "foo.bar.Artist")
     }
 
+    val CONVERTER by lazy {
+        loadJavaCode("common/input/DateConverter.java", "foo.bar.DateConverter")
+    }
+
+    val IMAGE_FORMAT by lazy {
+        loadJavaCode("common/input/ImageFormat.java", "foo.bar.ImageFormat")
+    }
+
+    val IMAGE by lazy {
+        loadJavaCode("common/input/Image.java", "foo.bar.Image")
+    }
+
     val SONG by lazy {
         loadJavaCode("common/input/Song.java", "foo.bar.Song")
     }