Extend data-binding resource types

In DB expressions users can use different key words for resource types, which are later converted to R class field references, e.g. "text" for "string".
Extend the resource types based on these rules.

Fixes: 184833062
Test: verified with user's project + added and updated unit tests + MultiModuleTestApp
Change-Id: I62f6caf2badddbcb47cafc4fa3251373ccd4f057
diff --git a/compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java b/compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java
index b0d1b1c..14b4c9d 100644
--- a/compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java
+++ b/compiler/src/main/java/android/databinding/tool/expr/ResourceExpr.java
@@ -22,6 +22,7 @@
 import android.databinding.tool.reflection.ModelClass;
 import android.databinding.tool.writer.KCode;
 import android.databinding.tool.writer.LayoutBinderWriterKt;
+import com.google.common.collect.Lists;
 
 import java.util.HashMap;
 import java.util.List;
@@ -32,6 +33,7 @@
 public class ResourceExpr extends Expr {
 
     private final static Map<String, String> RESOURCE_TYPE_TO_R_OBJECT;
+    public final static Map<String, List<String>> R_OBJECT_TO_RESOURCE_TYPE;
     static {
         RESOURCE_TYPE_TO_R_OBJECT = new HashMap<String, String>();
         RESOURCE_TYPE_TO_R_OBJECT.put("colorStateList", "color");
@@ -42,6 +44,15 @@
         RESOURCE_TYPE_TO_R_OBJECT.put("stringArray", "array");
         RESOURCE_TYPE_TO_R_OBJECT.put("text", "string");
         RESOURCE_TYPE_TO_R_OBJECT.put("typedArray", "array");
+
+        // TODO(184833062): clean up so it reads from the above map and reverses it more elegantly
+        R_OBJECT_TO_RESOURCE_TYPE = new HashMap<String, List<String>>();
+        R_OBJECT_TO_RESOURCE_TYPE.put("color", Lists.newArrayList("colorStateList"));
+        R_OBJECT_TO_RESOURCE_TYPE.put("dimen", Lists.newArrayList("dimenOffset", "dimenSize"));
+        R_OBJECT_TO_RESOURCE_TYPE.put(
+                "array", Lists.newArrayList("intArray", "stringArray", "typedArray"));
+        R_OBJECT_TO_RESOURCE_TYPE.put("animator", Lists.newArrayList("stateListAnimator"));
+        R_OBJECT_TO_RESOURCE_TYPE.put("string", Lists.newArrayList("text"));
     }
     // lazily initialized
     private Map<String, ModelClass> mResourceToTypeMapping;
diff --git a/compiler/src/main/java/android/databinding/tool/util/SymbolTableUtil.kt b/compiler/src/main/java/android/databinding/tool/util/SymbolTableUtil.kt
index 5b0bd1d..ab57d62 100644
--- a/compiler/src/main/java/android/databinding/tool/util/SymbolTableUtil.kt
+++ b/compiler/src/main/java/android/databinding/tool/util/SymbolTableUtil.kt
@@ -16,6 +16,7 @@
 @file:JvmName("SymbolTableUtil")
 package android.databinding.tool.util
 
+import android.databinding.tool.expr.ResourceExpr
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMultimap
 import java.io.File
@@ -29,6 +30,9 @@
      * Returns the prefix to be used in R class references in Java/Kotlin. For example: "android."
      * for "android.R.color.white", "com.my.lib." for "com.my.lib.R.string.text" and an empty string
      * for default R class reference e.g. "R.attr.my_attr".
+     *
+     * This method accepts data-binding specific "types", e.g. using "text" (for "string" resources)
+     * or "intArray" ( for "array" resources).
      */
     fun getRPackagePrefix(packageName: String?, type: String, name: String): String {
         when {
@@ -192,7 +196,7 @@
         // Format is <type> <name> for all resources apart from Styleables.
         if (chunks.size < 2 || (chunks[0] != STYLEABLE && chunks.size != 2))
             error("Illegal line in R.txt: '$line'")
-        resources.put(chunks[0], sanitizeName(chunks[1]))
+        addResource(chunks[0], sanitizeName(chunks[1]), resources)
         if (chunks[0] == STYLEABLE) {
             // For styleables the format is <type> <name> <child1> <child2> ... <childN>
             // The resulting children need to be added as Styleable <parent>_<child>.
@@ -207,6 +211,22 @@
     return resources.build()
 }
 
+fun addResource(
+        type: String,
+        name: String,
+        resourcesBuilder: ImmutableMultimap.Builder<String, String>) {
+
+    // Some expressions in data-binding correspond to different names in the R class. To be able
+    // to verify them, we need to add the resource to all matching expressions, e.g:
+    //  - "string" to "text"
+    //  - "array" to "intArray" AND "stringArray" AND "typedArray"
+    ResourceExpr.R_OBJECT_TO_RESOURCE_TYPE[type]?.forEach {
+        resourcesBuilder.put(it, name)
+    }
+
+    resourcesBuilder.put(type, name)
+}
+
 private fun sanitizeName(name: String): String {
     return name.replace('.', '_').replace(':', '_')
 }
diff --git a/compiler/src/test/java/android/databinding/tool/util/SymbolTableUtilTest.kt b/compiler/src/test/java/android/databinding/tool/util/SymbolTableUtilTest.kt
index 867477a..f33738c 100644
--- a/compiler/src/test/java/android/databinding/tool/util/SymbolTableUtilTest.kt
+++ b/compiler/src/test/java/android/databinding/tool/util/SymbolTableUtilTest.kt
@@ -56,14 +56,15 @@
         val result = parseLocalRTxt(testFile.toFile())
 
         assertEquals(result.rPackage, "")
-        assertEquals(result.resources.keySet().size, 3)
-        assertEquals(result.resources.values().size, 5)
+        assertEquals(result.resources.keySet().size, 4) // 3 + "text"
+        assertEquals(result.resources.values().size, 6) // "first" counted twice
 
         assertTrue(result.contains("string", "first"))
         assertTrue(result.contains("int", "second"))
         assertTrue(result.contains("styleable", "parent"))
         assertTrue(result.contains("styleable", "parent_child1"))
         assertTrue(result.contains("styleable", "parent_child2"))
+        assertTrue(result.contains("text", "first"))
     }
 
     @Test
@@ -79,9 +80,10 @@
         val result = parsePackageAwareRTxt(testFile.toFile())
 
         assertEquals(result.rPackage, "com.test.lib")
-        assertEquals(result.resources.keySet().size, 1)
-        assertEquals(result.resources.values().size, 1)
+        assertEquals(result.resources.keySet().size, 2) // string -> string, text
+        assertEquals(result.resources.values().toSet().size, 1) // "first" twice
         assertTrue(result.contains("string", "first"))
+        assertTrue(result.contains("text", "first"))
     }
 
     @Test
@@ -142,8 +144,9 @@
         val symbolTables = result.build()
         assertEquals(symbolTables.size, 4)
         assertEquals(symbolTables[0].rPackage, "com.test.mid.lib")
-        assertEquals(symbolTables[0].resources.size(), 2)
+        assertEquals(symbolTables[0].resources.size(), 4) // string -> string, text
         assertTrue(symbolTables[0].contains("string", "bar"))
+        assertTrue(symbolTables[0].contains("text", "bar"))
 
         assertEquals(symbolTables[2].rPackage, "com.test.empty.lib")
         assertEquals(symbolTables[2].resources.size(), 0)
@@ -178,6 +181,36 @@
     }
 
     @Test
+    fun testDataBindingKeyWords() {
+        val testFile = Files.createTempFile("dependency", "R.txt")
+        Files.write(
+                testFile,
+                """
+                com.test.lib
+                string first
+                array my_list
+                dimen dimension
+                color foo
+                """.trimIndent().toByteArray())
+
+        val result = parsePackageAwareRTxt(testFile.toFile())
+
+        assertEquals(result.resources.size(), 11)
+        assertTrue(result.contains("string", "first"))
+        assertTrue(result.contains("text", "first"))
+        assertTrue(result.contains("array", "my_list"))
+        assertTrue(result.contains("intArray", "my_list"))
+        assertTrue(result.contains("stringArray", "my_list"))
+        assertTrue(result.contains("typedArray", "my_list"))
+        assertTrue(result.contains("dimen", "dimension"))
+        assertTrue(result.contains("dimenOffset", "dimension"))
+        assertTrue(result.contains("dimenSize", "dimension"))
+        assertTrue(result.contains("color", "foo"))
+        assertTrue(result.contains("colorStateList", "foo"))
+
+    }
+
+    @Test
     fun testEmptyResources() {
         val testFile = Files.createTempFile("local", "R.txt")
         Files.write(testFile, "".toByteArray())
diff --git a/integration-tests/MultiModuleTestApp/app/src/main/res/layout/layout_with_text_ref.xml b/integration-tests/MultiModuleTestApp/app/src/main/res/layout/layout_with_text_ref.xml
new file mode 100644
index 0000000..1dcee03
--- /dev/null
+++ b/integration-tests/MultiModuleTestApp/app/src/main/res/layout/layout_with_text_ref.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 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.
+  -->
+
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+    <LinearLayout
+            android:orientation="vertical" android:layout_width="match_parent"
+            android:layout_height="match_parent">
+        <TextView
+                android:text="@{@text/my_text}"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"/>
+    </LinearLayout>
+</layout>
diff --git a/integration-tests/MultiModuleTestApp/testlibrary1/src/main/res/values/strings.xml b/integration-tests/MultiModuleTestApp/testlibrary1/src/main/res/values/strings.xml
index 4fbed97..b24bfb3 100644
--- a/integration-tests/MultiModuleTestApp/testlibrary1/src/main/res/values/strings.xml
+++ b/integration-tests/MultiModuleTestApp/testlibrary1/src/main/res/values/strings.xml
@@ -21,5 +21,6 @@
     <string name="hello_world">Hello world!</string>
     <string name="action_settings">Settings</string>
     <string name="library_string">Lib</string>
+    <string name="my_text">Hello world</string>
 
 </resources>