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 d9158fc..c512c44 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -910,7 +910,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 5c92ac9..30b2c1e 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 4adcef9..7761697 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1740,6 +1740,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 206a8ab..bf3a72c 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 0ee68d2..0c8e080 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -782,6 +782,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() {