Sanitize argument names to avoid generating invalid safe-args classes.
This change allows for argument names to have some non-valid java field
name, such as dots or bangs while still generating correct safe-args
classes. The names are stripped out of non-alphabetical and non-numeric
characters, then they are camel-cased to generate Java/Kotlin consistent
names. Note that this strategy is not a silver bullet that will
perfectly sanitize any string, but it is nicer than doing nothing.
Moreover, an error is now thrown when multiple different non-sanitized
argument names result in the same string after being sanitized.
Finally, this change also applies camel-case to actions class and method
names generated. Specifically if the id uses the underscore convention
such as R.id.open_details, then the generated class and method will be
camel-cased to OpenDetails (class) and openDetails (method).
Bug: 79995048
Bug: 79642240
Test: ./gradlew :navigation:navigation-safe-args-generator:test
Change-Id: Ie81ebf660c90663510ff33e6a0d045d2a2ed1261
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 a21846b..ad4b0af 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
@@ -16,6 +16,7 @@
package androidx.navigation.safe.args.generator
+import androidx.navigation.safe.args.generator.NavParserErrors.sameSanitizedNameArguments
import androidx.navigation.safe.args.generator.models.Action
import androidx.navigation.safe.args.generator.models.Argument
import androidx.navigation.safe.args.generator.models.Destination
@@ -74,6 +75,12 @@
}
}
+ args.groupBy { it.sanitizedName }.forEach { (sanitizedName, args) ->
+ if (args.size > 1) {
+ context.logger.error(sameSanitizedNameArguments(sanitizedName, args), position)
+ }
+ }
+
val id = idValue?.let { parseId(idValue, rFilePackage, position) }
val className = Destination.createName(id, name, applicationId)
if (className == null && (actions.isNotEmpty() || args.isNotEmpty())) {
@@ -139,6 +146,12 @@
}
}
+ args.groupBy { it.sanitizedName }.forEach { (sanitizedName, args) ->
+ if (args.size > 1) {
+ context.logger.error(sameSanitizedNameArguments(sanitizedName, args), position)
+ }
+ }
+
val id = if (idValue != null) {
parseId(idValue, rFilePackage, position)
} else {
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParserErrors.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParserErrors.kt
index 611c955..150032c 100644
--- a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParserErrors.kt
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParserErrors.kt
@@ -16,6 +16,8 @@
package androidx.navigation.safe.args.generator
+import androidx.navigation.safe.args.generator.models.Argument
+
object NavParserErrors {
val UNNAMED_DESTINATION = "Destination with arguments or actions must have " +
"'name' or 'id' attributes."
@@ -30,4 +32,9 @@
" @[+][package:]id/resource_name "
fun unknownType(type: String?) = "Unknown type '$type'"
+
+ fun sameSanitizedNameArguments(sanitizedName: String, args: List<Argument>) =
+ "Multiple same name arguments. The named arguments: " +
+ "[${args.joinToString(", ") { it.name }}] result in the generator using " +
+ "the same name: '$sanitizedName'."
}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavWriter.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavWriter.kt
index d613f9b..398fbaa 100644
--- a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavWriter.kt
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavWriter.kt
@@ -19,6 +19,8 @@
import androidx.navigation.safe.args.generator.ext.N
import androidx.navigation.safe.args.generator.ext.S
import androidx.navigation.safe.args.generator.ext.T
+import androidx.navigation.safe.args.generator.ext.toCamelCase
+import androidx.navigation.safe.args.generator.ext.toCamelCaseAsVar
import androidx.navigation.safe.args.generator.models.Action
import androidx.navigation.safe.args.generator.models.Argument
import androidx.navigation.safe.args.generator.models.Destination
@@ -39,7 +41,7 @@
private class ClassWithArgsSpecs(val args: List<Argument>) {
fun fieldSpecs() = args.map { arg ->
- FieldSpec.builder(arg.type.typeName(), arg.name)
+ FieldSpec.builder(arg.type.typeName(), arg.sanitizedName)
.apply {
addModifiers(Modifier.PRIVATE)
if (arg.isOptional()) {
@@ -49,11 +51,11 @@
.build()
}
- fun setters(thisClassName: ClassName) = args.map { (name, type) ->
- MethodSpec.methodBuilder("set${name.capitalize()}")
+ fun setters(thisClassName: ClassName) = args.map { (_, type, _, sanitizedName) ->
+ MethodSpec.methodBuilder("set${sanitizedName.capitalize()}")
.addModifiers(Modifier.PUBLIC)
- .addParameter(type.typeName(), name)
- .addStatement("this.$N = $N", name, name)
+ .addParameter(type.typeName(), sanitizedName)
+ .addStatement("this.$N = $N", sanitizedName, sanitizedName)
.addStatement("return this")
.returns(thisClassName)
.build()
@@ -61,9 +63,9 @@
fun constructor() = MethodSpec.constructorBuilder().apply {
addModifiers(Modifier.PUBLIC)
- args.filterNot(Argument::isOptional).forEach { (argName, type) ->
- addParameter(type.typeName(), argName)
- addStatement("this.$N = $N", argName, argName)
+ args.filterNot(Argument::isOptional).forEach { (_, type, _, sanitizedName) ->
+ addParameter(type.typeName(), sanitizedName)
+ addStatement("this.$N = $N", sanitizedName, sanitizedName)
}
}.build()
@@ -72,23 +74,26 @@
returns(BUNDLE_CLASSNAME)
val bundleName = "__outBundle"
addStatement("$T $N = new $T()", BUNDLE_CLASSNAME, bundleName, BUNDLE_CLASSNAME)
- args.forEach { (argName, type) ->
- addStatement("$N.$N($S, this.$N)", bundleName, type.bundlePutMethod(), argName, argName)
+ args.forEach { (name, type, _, sanitizedName) ->
+ addStatement("$N.$N($S, this.$N)", bundleName, type.bundlePutMethod(), name,
+ sanitizedName)
}
addStatement("return $N", bundleName)
}.build()
fun copyProperties(to: String, from: String) = CodeBlock.builder()
.apply {
- args.forEach { arg -> addStatement("$to.${arg.name} = $from.${arg.name}") }
+ args.forEach { (_, _, _, sanitizedName) ->
+ addStatement("$to.$sanitizedName = $from.$sanitizedName")
+ }
}
.build()
- fun getters() = args.map { arg ->
- MethodSpec.methodBuilder("get${arg.name.capitalize()}")
+ fun getters() = args.map { (_, type, _, sanitizedName) ->
+ MethodSpec.methodBuilder("get${sanitizedName.capitalize()}")
.addModifiers(Modifier.PUBLIC)
- .addStatement("return $N", arg.name)
- .returns(arg.type.typeName())
+ .addStatement("return $N", sanitizedName)
+ .returns(type.typeName())
.build()
}
@@ -109,13 +114,13 @@
""".trimIndent())
addStatement("${className.simpleName()} that = (${className.simpleName()}) object")
- args.forEach {
- val compareExpression = when (it.type) {
+ args.forEach { (_, type, _, sanitizedName) ->
+ val compareExpression = when (type) {
NavType.INT, NavType.BOOLEAN, NavType.REFERENCE, NavType.LONG ->
- "${it.name} != that.${it.name}"
- NavType.FLOAT -> "Float.compare(that.${it.name}, ${it.name}) != 0"
- NavType.STRING -> "${it.name} != null ? !${it.name}.equals(that.${it.name}) " +
- ": that.${it.name} != null"
+ "$sanitizedName != that.$sanitizedName"
+ NavType.FLOAT -> "Float.compare(that.$sanitizedName, $sanitizedName) != 0"
+ NavType.STRING -> "$sanitizedName != null ? " +
+ "!$sanitizedName.equals(that.$sanitizedName) : that.$sanitizedName != null"
}
beginControlFlow("if ($N)", compareExpression).apply {
addStatement("return false")
@@ -130,13 +135,13 @@
addAnnotation(Override::class.java)
addModifiers(Modifier.PUBLIC)
addStatement("int result = super.hashCode()")
- args.forEach {
- val hashCodeExpression = when (it.type) {
- NavType.INT, NavType.REFERENCE -> it.name
- NavType.FLOAT -> "Float.floatToIntBits(${it.name})"
- NavType.STRING -> "(${it.name} != null ? ${it.name}.hashCode() : 0)"
- NavType.BOOLEAN -> "(${it.name} ? 1 : 0)"
- NavType.LONG -> "(int)(${it.name} ^ (${it.name} >>> 32))"
+ args.forEach { (_, type, _, sanitizedName) ->
+ val hashCodeExpression = when (type) {
+ NavType.INT, NavType.REFERENCE -> sanitizedName
+ NavType.FLOAT -> "Float.floatToIntBits($sanitizedName)"
+ NavType.STRING -> "($sanitizedName != null ? $sanitizedName.hashCode() : 0)"
+ NavType.BOOLEAN -> "($sanitizedName ? 1 : 0)"
+ NavType.LONG -> "(int)($sanitizedName ^ ($sanitizedName >>> 32))"
}
addStatement("result = 31 * result + $N", hashCodeExpression)
}
@@ -158,7 +163,7 @@
val constructor = actionType.methodSpecs.find(MethodSpec::isConstructor)!!
val params = constructor.parameters.joinToString(", ") { param -> param.name }
val actionTypeName = ClassName.get("", actionType.name)
- MethodSpec.methodBuilder(action.id.name)
+ MethodSpec.methodBuilder(action.id.name.toCamelCaseAsVar())
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameters(constructor.parameters)
.returns(actionTypeName)
@@ -182,7 +187,7 @@
.addStatement("return $N", action.id.accessor())
.build()
- val className = ClassName.get("", action.id.name.capitalize())
+ val className = ClassName.get("", action.id.name.toCamelCase())
return TypeSpec.classBuilder(className)
.addSuperinterface(NAV_DIRECTION_CLASSNAME)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
@@ -210,7 +215,7 @@
addStatement("$T $N = new $T()", className, result, className)
args.forEach { arg ->
beginControlFlow("if ($N.containsKey($S))", bundle, arg.name).apply {
- addStatement("$N.$N = $N.$N($S)", result, arg.name, bundle,
+ addStatement("$N.$N = $N.$N($S)", result, arg.sanitizedName, bundle,
arg.type.bundleGetMethod(), arg.name)
}
if (!arg.isOptional()) {
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/List_ext.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/List_ext.kt
new file mode 100644
index 0000000..2c19c7f
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/List_ext.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018 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 androidx.navigation.safe.args.generator.ext
+
+fun List<String>.joinToCamelCase(): String = when (size) {
+ 0 -> throw IllegalArgumentException("invalid section size, cannot be zero")
+ 1 -> this[0].toCamelCase()
+ else -> this.joinToString("") { it.toCamelCase() }
+}
+
+fun List<String>.joinToCamelCaseAsVar(): String = when (size) {
+ 0 -> throw IllegalArgumentException("invalid section size, cannot be zero")
+ 1 -> this[0].toCamelCaseAsVar()
+ else -> get(0).toCamelCaseAsVar() + drop(1).joinToCamelCase()
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/String_ext.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/String_ext.kt
new file mode 100644
index 0000000..e3c0387
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/String_ext.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 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 androidx.navigation.safe.args.generator.ext
+
+fun String.toCamelCase(): String {
+ val split = this.split("_")
+ if (split.size == 0) return ""
+ if (split.size == 1) return split[0].capitalize()
+ return split.joinToCamelCase()
+}
+
+fun String.toCamelCaseAsVar(): String {
+ val split = this.split("_")
+ if (split.size == 0) return ""
+ if (split.size == 1) return split[0]
+ return split.joinToCamelCaseAsVar()
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Argument.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Argument.kt
index fea1bdb..1dd1321 100644
--- a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Argument.kt
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Argument.kt
@@ -18,8 +18,14 @@
import androidx.navigation.safe.args.generator.NavType
import androidx.navigation.safe.args.generator.WriteableValue
+import androidx.navigation.safe.args.generator.ext.joinToCamelCaseAsVar
data class Argument(val name: String, val type: NavType, val defaultValue: WriteableValue? = null) {
- fun isOptional() = defaultValue != null
-}
+ val sanitizedName = name.split("[^a-zA-Z0-9]".toRegex())
+ .map { it.trim() }.joinToCamelCaseAsVar()
+
+ fun isOptional() = defaultValue != null
+
+ operator fun component4() = sanitizedName
+}
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/InvalidXmlTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/InvalidXmlTest.kt
index 639a24985..d30594e 100644
--- a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/InvalidXmlTest.kt
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/InvalidXmlTest.kt
@@ -20,6 +20,8 @@
import androidx.navigation.safe.args.generator.NavParserErrors.invalidDefaultValue
import androidx.navigation.safe.args.generator.NavParserErrors.invalidDefaultValueReference
import androidx.navigation.safe.args.generator.NavParserErrors.invalidId
+import androidx.navigation.safe.args.generator.NavParserErrors.sameSanitizedNameArguments
+import androidx.navigation.safe.args.generator.models.Argument
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
@@ -39,7 +41,9 @@
invalidDefaultValue("101034f", NavType.INT)),
ErrorMessage("invalid_id_action.xml", 22, 44, invalidId("@+fppid/finish")),
ErrorMessage("invalid_id_destination.xml", 17, 1, invalidId("@1234234+id/foo")),
- ErrorMessage("action_no_id.xml", 22, 5, mandatoryAttrMissingError("action", "id"))
+ ErrorMessage("action_no_id.xml", 22, 5, mandatoryAttrMissingError("action", "id")),
+ ErrorMessage("same_name_args.xml", 23, 9, sameSanitizedNameArguments("myArg", listOf(
+ Argument("my_arg", NavType.STRING), Argument("my.arg", NavType.STRING))))
)
}
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 64b127d..5f7188e 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
@@ -30,6 +30,7 @@
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -127,4 +128,30 @@
assertThat(infer("123L"), `is`(longArg("123L")))
assertThat(infer("1234123412341234L"), `is`(longArg("1234123412341234L")))
}
+
+ @Test
+ fun testArgSanitizedName() {
+ assertEquals("camelCaseName",
+ Argument("camelCaseName", INT).sanitizedName)
+ assertEquals("ALLCAPSNAME",
+ Argument("ALLCAPSNAME", INT).sanitizedName)
+ assertEquals("alllowercasename",
+ Argument("alllowercasename", INT).sanitizedName)
+ assertEquals("nameWithUnderscore",
+ Argument("name_with_underscore", INT).sanitizedName)
+ assertEquals("NameWithUnderscore",
+ Argument("Name_With_Underscore", INT).sanitizedName)
+ assertEquals("NAMEWITHUNDERSCORE",
+ Argument("NAME_WITH_UNDERSCORE", INT).sanitizedName)
+ assertEquals("nameWithSpaces",
+ Argument("name with spaces", INT).sanitizedName)
+ assertEquals("nameWithDot",
+ Argument("name.with.dot", INT).sanitizedName)
+ assertEquals("nameWithDollars",
+ Argument("name\$with\$dollars", INT).sanitizedName)
+ assertEquals("nameWithBangs",
+ Argument("name!with!bangs", INT).sanitizedName)
+ assertEquals("nameWithHyphens",
+ Argument("name-with-hyphens", INT).sanitizedName)
+ }
}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavWriterTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavWriterTest.kt
index b18ae5f..199ac62 100644
--- a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavWriterTest.kt
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavWriterTest.kt
@@ -43,7 +43,7 @@
import javax.tools.JavaFileObject
@RunWith(JUnit4::class)
-class WriterTest {
+class NavWriterTest {
@get:Rule
@Suppress("MemberVisibilityCanBePrivate")
@@ -126,6 +126,26 @@
}
@Test
+ fun testDirectionsClassGeneration_sanitizedNames() {
+ val nextAction = Action(id("next_action"), id("destA"),
+ listOf(
+ Argument("main_arg", STRING),
+ Argument("optional.arg", STRING, StringValue("bla"))))
+
+ val prevAction = Action(id("previous_action"), id("destB"),
+ listOf(
+ Argument("arg_1", STRING),
+ Argument("arg.2", STRING)))
+
+ val dest = Destination(null, ClassName.get("a.b", "SanitizedMainFragment"),
+ "fragment", listOf(), listOf(prevAction, nextAction))
+
+ val actual = toJavaFileObject(generateDirectionsJavaFile(dest))
+ JavaSourcesSubject.assertThat(actual).parsesAs("a.b.SanitizedMainFragmentDirections")
+ assertCompilesWithoutError(actual)
+ }
+
+ @Test
fun testArgumentsClassGeneration() {
val dest = Destination(null, ClassName.get("a.b", "MainFragment"), "fragment", listOf(
Argument("main", STRING),
@@ -140,4 +160,18 @@
JavaSourcesSubject.assertThat(actual).parsesAs("a.b.MainFragmentArgs")
assertCompilesWithoutError(actual)
}
+
+ @Test
+ fun testArgumentsClassGeneration_sanitizedNames() {
+ val dest = Destination(null, ClassName.get("a.b", "SanitizedMainFragment"),
+ "fragment", listOf(
+ Argument("name.with.dot", INT),
+ Argument("name_with_underscore", INT),
+ Argument("name with spaces", INT)),
+ listOf())
+
+ val actual = toJavaFileObject(generateArgsJavaFile(dest))
+ JavaSourcesSubject.assertThat(actual).parsesAs("a.b.SanitizedMainFragmentArgs")
+ assertCompilesWithoutError(actual)
+ }
}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/a/b/R.java b/navigation/safe-args-generator/src/tests/test-data/a/b/R.java
index 8d2fc90..815b7ae 100644
--- a/navigation/safe-args-generator/src/tests/test-data/a/b/R.java
+++ b/navigation/safe-args-generator/src/tests/test-data/a/b/R.java
@@ -23,6 +23,8 @@
public static final int finish = 0x7f060000;
public static final int previous = 0x7f060001;
public static final int next = 0x7f060002;
+ public static final int previous_action = 0x7f060003;
+ public static final int next_action = 0x7f060004;
}
public static final class drawable {
diff --git a/navigation/safe-args-generator/src/tests/test-data/expected/SanitizedMainFragmentArgs.java b/navigation/safe-args-generator/src/tests/test-data/expected/SanitizedMainFragmentArgs.java
new file mode 100644
index 0000000..1dfa138
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/expected/SanitizedMainFragmentArgs.java
@@ -0,0 +1,145 @@
+package a.b;
+
+import android.os.Bundle;
+import java.lang.IllegalArgumentException;
+import java.lang.Object;
+import java.lang.Override;
+
+public class SanitizedMainFragmentArgs {
+ private int nameWithDot;
+
+ private int nameWithUnderscore;
+
+ private int nameWithSpaces;
+
+ private SanitizedMainFragmentArgs() {
+ }
+
+ public static SanitizedMainFragmentArgs fromBundle(Bundle bundle) {
+ SanitizedMainFragmentArgs result = new SanitizedMainFragmentArgs();
+ if (bundle.containsKey("name.with.dot")) {
+ result.nameWithDot = bundle.getInt("name.with.dot");
+ } else {
+ throw new IllegalArgumentException("Required argument \"name.with.dot\" is missing and does not have an android:defaultValue");
+ }
+ if (bundle.containsKey("name_with_underscore")) {
+ result.nameWithUnderscore = bundle.getInt("name_with_underscore");
+ } else {
+ throw new IllegalArgumentException("Required argument \"name_with_underscore\" is missing and does not have an android:defaultValue");
+ }
+ if (bundle.containsKey("name with spaces")) {
+ result.nameWithSpaces = bundle.getInt("name with spaces");
+ } else {
+ throw new IllegalArgumentException("Required argument \"name with spaces\" is missing and does not have an android:defaultValue");
+ }
+ return result;
+ }
+
+ public int getNameWithDot() {
+ return nameWithDot;
+ }
+
+ public int getNameWithUnderscore() {
+ return nameWithUnderscore;
+ }
+
+ public int getNameWithSpaces() {
+ return nameWithSpaces;
+ }
+
+ public Bundle toBundle() {
+ Bundle __outBundle = new Bundle();
+ __outBundle.putInt("name.with.dot", this.nameWithDot);
+ __outBundle.putInt("name_with_underscore", this.nameWithUnderscore);
+ __outBundle.putInt("name with spaces", this.nameWithSpaces);
+ return __outBundle;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ if (!super.equals(object)) {
+ return false;
+ }
+ SanitizedMainFragmentArgs that = (SanitizedMainFragmentArgs) object;
+ if (nameWithDot != that.nameWithDot) {
+ return false;
+ }
+ if (nameWithUnderscore != that.nameWithUnderscore) {
+ return false;
+ }
+ if (nameWithSpaces != that.nameWithSpaces) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + nameWithDot;
+ result = 31 * result + nameWithUnderscore;
+ result = 31 * result + nameWithSpaces;
+ return result;
+ }
+
+ public static class Builder {
+ private int nameWithDot;
+
+ private int nameWithUnderscore;
+
+ private int nameWithSpaces;
+
+ public Builder(SanitizedMainFragmentArgs original) {
+ this.nameWithDot = original.nameWithDot;
+ this.nameWithUnderscore = original.nameWithUnderscore;
+ this.nameWithSpaces = original.nameWithSpaces;
+ }
+
+ public Builder(int nameWithDot, int nameWithUnderscore, int nameWithSpaces) {
+ this.nameWithDot = nameWithDot;
+ this.nameWithUnderscore = nameWithUnderscore;
+ this.nameWithSpaces = nameWithSpaces;
+ }
+
+ public SanitizedMainFragmentArgs build() {
+ SanitizedMainFragmentArgs result = new SanitizedMainFragmentArgs();
+ result.nameWithDot = this.nameWithDot;
+ result.nameWithUnderscore = this.nameWithUnderscore;
+ result.nameWithSpaces = this.nameWithSpaces;
+ return result;
+ }
+
+ public Builder setNameWithDot(int nameWithDot) {
+ this.nameWithDot = nameWithDot;
+ return this;
+ }
+
+ public Builder setNameWithUnderscore(int nameWithUnderscore) {
+ this.nameWithUnderscore = nameWithUnderscore;
+ return this;
+ }
+
+ public Builder setNameWithSpaces(int nameWithSpaces) {
+ this.nameWithSpaces = nameWithSpaces;
+ return this;
+ }
+
+ public int getNameWithDot() {
+ return nameWithDot;
+ }
+
+ public int getNameWithUnderscore() {
+ return nameWithUnderscore;
+ }
+
+ public int getNameWithSpaces() {
+ return nameWithSpaces;
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/expected/SanitizedMainFragmentDirections.java b/navigation/safe-args-generator/src/tests/test-data/expected/SanitizedMainFragmentDirections.java
new file mode 100644
index 0000000..97ef33c
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/expected/SanitizedMainFragmentDirections.java
@@ -0,0 +1,78 @@
+package a.b;
+
+import android.os.Bundle;
+import androidx.navigation.NavDirections;
+import java.lang.String;
+
+public class SanitizedMainFragmentDirections {
+ public static PreviousAction previousAction(String arg1, String arg2) {
+ return new PreviousAction(arg1, arg2);
+ }
+
+ public static NextAction nextAction(String mainArg) {
+ return new NextAction(mainArg);
+ }
+
+ public static class PreviousAction implements NavDirections {
+ private String arg1;
+
+ private String arg2;
+
+ public PreviousAction(String arg1, String arg2) {
+ this.arg1 = arg1;
+ this.arg2 = arg2;
+ }
+
+ public PreviousAction setArg1(String arg1) {
+ this.arg1 = arg1;
+ return this;
+ }
+
+ public PreviousAction setArg2(String arg2) {
+ this.arg2 = arg2;
+ return this;
+ }
+
+ public Bundle getArguments() {
+ Bundle __outBundle = new Bundle();
+ __outBundle.putString("arg_1", this.arg1);
+ __outBundle.putString("arg.2", this.arg2);
+ return __outBundle;
+ }
+
+ public int getActionId() {
+ return a.b.R.id.previous_action;
+ }
+ }
+
+ public static class NextAction implements NavDirections {
+ private String mainArg;
+
+ private String optionalArg = "bla";
+
+ public NextAction(String mainArg) {
+ this.mainArg = mainArg;
+ }
+
+ public NextAction setMainArg(String mainArg) {
+ this.mainArg = mainArg;
+ return this;
+ }
+
+ public NextAction setOptionalArg(String optionalArg) {
+ this.optionalArg = optionalArg;
+ return this;
+ }
+
+ public Bundle getArguments() {
+ Bundle __outBundle = new Bundle();
+ __outBundle.putString("main_arg", this.mainArg);
+ __outBundle.putString("optional.arg", this.optionalArg);
+ return __outBundle;
+ }
+
+ public int getActionId() {
+ return a.b.R.id.next_action;
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/invalid_xmls/same_name_args.xml b/navigation/safe-args-generator/src/tests/test-data/invalid_xmls/same_name_args.xml
new file mode 100644
index 0000000..af99cd9b
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/invalid_xmls/same_name_args.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/foo"
+ app:startDestination="@+id/first_screen">
+ <fragment android:name="a.FakeFragment">
+ <action android:id="@+id/finish" app:popUpTo="@id/first_screen">
+ <argument android:name="my_arg" app:type="string" />
+ <argument android:name="my.arg" app:type="string" />
+ </action>
+ </fragment>
+</navigation>
\ No newline at end of file