Correctly handle null-valued names in MethodParameters.
Bug: b/281536562
Change-Id: Ib050db5aa7ac8f8e08ab74a36b9d6e9c4c6d86f6
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 8cb3ed0..067e0ae 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -899,7 +899,13 @@
parameterNames = new ArrayList<>(parameterCount);
parameterFlags = new ArrayList<>(parameterCount);
}
- parameterNames.add(new DexValueString(parent.application.getFactory().createString(name)));
+ if (name == null) {
+ // The JVM spec defines a null entry as valid and indicating no parameter name.
+ // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.24
+ parameterNames.add(DexValueNull.NULL);
+ } else {
+ parameterNames.add(new DexValueString(parent.application.getFactory().createString(name)));
+ }
parameterFlags.add(DexValueInt.create(access));
super.visitParameter(name, access);
}
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index 19febfd..c2bbb70 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -513,7 +513,8 @@
for (int i = 0; i < names.getValues().length; i++) {
DexValueString name = names.getValues()[i].asDexValueString();
DexValueInt access = accessFlags.getValues()[i].asDexValueInt();
- visitor.visitParameter(name.value.toString(), access.value);
+ String nameString = name != null ? name.value.toString() : null;
+ visitor.visitParameter(nameString, access.value);
}
}
}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index f3fc72a..9e88abb 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1743,6 +1743,10 @@
return AndroidApiLevel.O;
}
+ public static AndroidApiLevel apiLevelWithMethodParametersSupport() {
+ return AndroidApiLevel.O;
+ }
+
public static boolean canUseJavaUtilObjects(TestParameters parameters) {
return parameters.isCfRuntime()
|| parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
diff --git a/src/test/java/com/android/tools/r8/TestParameters.java b/src/test/java/com/android/tools/r8/TestParameters.java
index 1c44201..6c2f27a 100644
--- a/src/test/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/java/com/android/tools/r8/TestParameters.java
@@ -138,6 +138,10 @@
return isDexRuntime() && getDexRuntimeVersion().isNewerThanOrEqual(vm);
}
+ public boolean isDexRuntimeVersionOlderThanOrEqual(DexVm.Version vm) {
+ return isDexRuntime() && getDexRuntimeVersion().isOlderThanOrEqual(vm);
+ }
+
public boolean isNoneRuntime() {
return runtime == NoneRuntime.getInstance();
}
diff --git a/src/test/java/com/android/tools/r8/naming/methodparameters/MethodParametersNullValueTest.java b/src/test/java/com/android/tools/r8/naming/methodparameters/MethodParametersNullValueTest.java
new file mode 100644
index 0000000..e6a1197
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/methodparameters/MethodParametersNullValueTest.java
@@ -0,0 +1,110 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.naming.methodparameters;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.reflect.MalformedParametersException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/** Regression test for b/281536562 */
+@RunWith(Parameterized.class)
+public class MethodParametersNullValueTest extends TestBase {
+
+ static final String EXPECTED = StringUtils.lines("bar", "arg1", "baz");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+ }
+
+ public MethodParametersNullValueTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testJvm() throws Exception {
+ parameters.assumeJvmTestParameters();
+ testForJvm(parameters)
+ .addProgramClassFileData(getTransformedTestClass())
+ .run(parameters.getRuntime(), TestClass.class)
+ .apply(this::checkExpected);
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ testForD8(parameters.getBackend())
+ .addProgramClassFileData(getTransformedTestClass())
+ .setMinApi(parameters)
+ .run(parameters.getRuntime(), TestClass.class)
+ .apply(this::checkExpected);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ parameters.assumeR8TestParameters();
+ // Don't run on old API as that build is "ill configured" and triggers missing type refs.
+ assumeTrue(
+ parameters.isCfRuntime()
+ || parameters
+ .getApiLevel()
+ .isGreaterThanOrEqualTo(apiLevelWithMethodParametersSupport()));
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(getTransformedTestClass())
+ .setMinApi(parameters)
+ .addKeepClassAndMembersRules(TestClass.class)
+ .addKeepAllAttributes()
+ .run(parameters.getRuntime(), TestClass.class)
+ .apply(this::checkExpected);
+ }
+
+ private void checkExpected(TestRunResult<?> result) {
+ if (parameters.isCfRuntime(CfVm.JDK8)) {
+ // JDK8 will throw when accessing a null valued method parameter name.
+ result.assertFailureWithErrorThatThrows(MalformedParametersException.class);
+ } else if (parameters.isDexRuntimeVersionOlderThanOrEqual(Version.V7_0_0)) {
+ // API 26 introduced the java.lang.reflect.Parameter and methods.
+ result.assertFailureWithErrorThatThrows(NoSuchMethodError.class);
+ } else {
+ result.assertSuccessWithOutput(EXPECTED);
+ }
+ }
+
+ private byte[] getTransformedTestClass() throws Exception {
+ return transformer(TestClass.class)
+ .setMethodParameters(MethodPredicate.onName("foo"), "bar", null, "baz")
+ .transform();
+ }
+
+ static class TestClass {
+
+ public static void foo(String bar, String willBeNull, String baz) {
+ System.out.println("" + bar + willBeNull + baz);
+ }
+
+ public static void main(String[] args) {
+ for (Method method : TestClass.class.getMethods()) {
+ if (method.getName().equals("foo")) {
+ for (Parameter parameter : method.getParameters()) {
+ System.out.println(parameter.getName());
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index e010aa2..2e3df74 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -783,6 +783,30 @@
});
}
+ public ClassFileTransformer setMethodParameters(
+ MethodPredicate predicate, String... parameterNames) {
+ return addClassTransformer(
+ new ClassTransformer() {
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String descriptor, String signature, String[] exceptions) {
+ MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
+ if (predicate.test(access, name, descriptor, signature, exceptions)) {
+ for (String parameterName : parameterNames) {
+ mv.visitParameter(parameterName, 0);
+ }
+ return new MethodVisitor(ASM_VERSION, mv) {
+ @Override
+ public void visitParameter(String name, int access) {
+ // Ignore all existing method parameter.
+ }
+ };
+ }
+ return mv;
+ }
+ });
+ }
+
public ClassFileTransformer setFieldType(FieldPredicate predicate, String newFieldType) {
return addClassTransformer(
new ClassTransformer() {