Merge "Infer args types" into flatfoot-navigation
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParser.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParser.kt
index b46c066..9c47b29 100644
--- a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParser.kt
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParser.kt
@@ -38,8 +38,9 @@
 private const val NAMESPACE_RES_AUTO = "http://schemas.android.com/apk/res-auto"
 private const val NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
 
-private fun parseDestination(parser: XmlPullParser, rFilePackage: String,
-                             applicationId: String): Destination {
+private fun parseDestination(
+        parser: XmlPullParser, rFilePackage: String,
+        applicationId: String): Destination {
     val type = parser.name
     val name = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_NAME) ?: ""
     val idValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_ID)
@@ -68,15 +69,32 @@
     val name = parser.attrValueOrThrow(NAMESPACE_ANDROID, ATTRIBUTE_NAME)
     val defaultValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_DEFAULT_VALUE)
     val typeString = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_TYPE)
+    if (typeString == null && defaultValue != null) {
+        return inferArgument(name, defaultValue, rFilePackage)
+    }
 
     val (type, defaultTypedValue) = when (typeString) {
-        "integer" -> NavType.INT to parseIntValue(defaultValue)
-        "reference" -> NavType.REFERENCE to parseReferenceValue(defaultValue, rFilePackage)
+        "integer" -> NavType.INT to defaultValue?.let { parseIntValue(defaultValue) }
+        "reference" -> NavType.REFERENCE to defaultValue?.let {
+            ReferenceValue(parseReference(defaultValue, rFilePackage))
+        }
         else -> NavType.STRING to defaultValue?.let { StringValue(it) }
     }
     return Argument(name, type, defaultTypedValue)
 }
 
+internal fun inferArgument(name: String, defaultValue: String, rFilePackage: String): Argument {
+    val reference = tryToParseReference(defaultValue, rFilePackage)
+    if (reference != null) {
+        return Argument(name, NavType.REFERENCE, ReferenceValue(reference))
+    }
+    val intValue = tryToParseIntValue(defaultValue)
+    if (intValue != null) {
+        return Argument(name, NavType.INT, intValue)
+    }
+    return Argument(name, NavType.STRING, StringValue(defaultValue))
+}
+
 private fun parseAction(parser: XmlPullParser, rFilePackage: String): Action {
     val idValue = parser.attrValueOrThrow(NAMESPACE_ANDROID, ATTRIBUTE_ID)
     val destValue = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_DESTINATION)
@@ -91,7 +109,7 @@
 }
 
 fun parseNavigationFile(navigationXml: File, rFilePackage: String,
-                        applicationId: String): Destination {
+        applicationId: String): Destination {
     FileReader(navigationXml).use { reader ->
         val parser = XmlPullParserFactory.newInstance().newPullParser().apply {
             setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true)
@@ -103,15 +121,20 @@
 }
 
 // @[+][package:]id/resource_name -> package.R.id.resource_name
+private val RESOURCE_REGEX = Regex("^@[+]?(.+?:)?(.+?)/(.+)$")
+
 internal fun parseReference(xmlValue: String, rFilePackage: String): ResReference {
-    val split = xmlValue.removePrefix("@").removePrefix("+").split(":", "/")
-    if (split.size != 2 && split.size != 3) {
-        throw IllegalArgumentException("id should be in format: " +
-                "@[+][package:]res_type/resource_name, but is: $xmlValue")
-    }
-    val resourceName = split.last()
-    val resType = split[split.size - 2]
-    val packageName = if (split.size == 3) split[0] else rFilePackage
+    return tryToParseReference(xmlValue, rFilePackage) ?:
+            throw IllegalArgumentException("id should be in format: " +
+                    "@[+][package:]res_type/resource_name, but is: $xmlValue")
+}
+
+internal fun tryToParseReference(xmlValue: String, rFilePackage: String): ResReference? {
+    val matchEntire = RESOURCE_REGEX.matchEntire(xmlValue) ?: return null
+    val groups = matchEntire.groupValues
+    val resourceName = groups.last()
+    val resType = groups[groups.size - 2]
+    val packageName = if (groups[1].isNotEmpty()) groups[1].removeSuffix(":") else rFilePackage
     return ResReference(packageName, resType, resourceName)
 }
 
@@ -127,26 +150,20 @@
     parseId(it, rFilePackage)
 }
 
-internal fun parseIntValue(value: String?): IntValue? {
-    if (value == null) {
+private fun tryToParseIntValue(value: String): IntValue? {
+    try {
+        if (value.startsWith("0x")) {
+            Integer.parseUnsignedInt(value.substring(2), 16)
+        } else {
+            Integer.parseInt(value)
+        }
+    } catch (ex: NumberFormatException) {
         return null
     }
-    if (value.startsWith("0x")) {
-        try {
-            Integer.parseUnsignedInt(value.substring(2), 16)
-            return IntValue(value)
-        } catch (ex: NumberFormatException) {
-            throw IllegalArgumentException("Failed to parse $value as int")
-        }
-    }
-
-    try {
-        Integer.parseInt(value)
-        return IntValue(value)
-    } catch (ex: NumberFormatException) {
-        throw IllegalArgumentException("Failed to parse $value as int")
-    }
+    return IntValue(value)
 }
 
-internal fun parseReferenceValue(value: String?, rFilePackage: String) =
-        value?.let { ReferenceValue(parseReference(value, rFilePackage)) }
+internal fun parseIntValue(value: String): IntValue {
+    return tryToParseIntValue(value)
+            ?: throw IllegalArgumentException("Failed to parse $value as int")
+}
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavParserTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavParserTest.kt
index 1fab491..2164674 100644
--- a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavParserTest.kt
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavParserTest.kt
@@ -17,6 +17,7 @@
 package androidx.navigation.safe.args.generator
 
 import androidx.navigation.safe.args.generator.NavType.INT
+import androidx.navigation.safe.args.generator.NavType.REFERENCE
 import androidx.navigation.safe.args.generator.NavType.STRING
 import androidx.navigation.safe.args.generator.models.Action
 import androidx.navigation.safe.args.generator.models.Argument
@@ -85,6 +86,26 @@
                 instanceOf(IllegalArgumentException::class.java))
     }
 
+    @Test
+    fun testArgInference() {
+        val infer = { value: String -> inferArgument("foo", value, "a.b") }
+        val intArg = { value: String -> Argument("foo", INT, IntValue(value)) }
+        val stringArg = { value: String -> Argument("foo", STRING, StringValue(value)) }
+        val referenceArg = { pName: String, type: String, value: String ->
+            Argument("foo", REFERENCE, ReferenceValue(ResReference(pName, type, value)))
+        }
+
+        assertThat(infer("spb"), `is`(stringArg("spb")))
+        assertThat(infer("10"), `is`(intArg("10")))
+        assertThat(infer("0x10"), `is`(intArg("0x10")))
+        assertThat(infer("@android:id/some_la"), `is`(referenceArg("android", "id", "some_la")))
+        assertThat(infer("@foo"), `is`(stringArg("@foo")))
+        assertThat(infer("@+id/foo"), `is`(referenceArg("a.b", "id", "foo")))
+        assertThat(infer("@foo:stuff"), `is`(stringArg("@foo:stuff")))
+        assertThat(infer("@/stuff"), `is`(stringArg("@/stuff")))
+        assertThat(infer("10101010100100"), `is`(stringArg("10101010100100")))
+    }
+
     private fun errorOf(f: () -> Unit, message: String = ""): Exception {
         try {
             f()