Optimize empty Object[] -> null in reflection APIs

Performs conversion for:
  * Class.getConstructor()
  * Class.getDeclaredConstructor()
  * Class.getMethod()
  * Class.getDeclaredMethod()
  * Method.invoke()
  * Constructor.newInstance()

Bug: b/316155816
Change-Id: Iafce1fdd7204fe7d4372c5ab9c0b21f5eccb2d83
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 39c4168..1c568cd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -675,6 +675,7 @@
   public final AssertionErrorMethods assertionErrorMethods = new AssertionErrorMethods();
   public final ClassMethods classMethods = new ClassMethods();
   public final ConstructorMethods constructorMethods = new ConstructorMethods();
+  public final MethodMethods methodMethods = new MethodMethods();
   public final EnumMembers enumMembers = new EnumMembers();
   public final AndroidUtilLogMembers androidUtilLogMembers = new AndroidUtilLogMembers();
   public final JavaLangReflectArrayMembers javaLangReflectArrayMembers =
@@ -1946,6 +1947,15 @@
     }
   }
 
+  public class MethodMethods {
+
+    public final DexMethod invoke =
+        createMethod(
+            methodType, createProto(objectType, objectType, objectArrayType), invokeMethodName);
+
+    private MethodMethods() {}
+  }
+
   public class AndroidUtilLogMembers {
 
     public final DexMethod i =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java
new file mode 100644
index 0000000..db81e4b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ClassOptimizer.java
@@ -0,0 +1,66 @@
+// 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.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.Set;
+
+public class ClassOptimizer extends StatelessLibraryMethodModelCollection {
+
+  private final InternalOptions options;
+  private final DexItemFactory dexItemFactory;
+  private final DexMethod getConstructor;
+  private final DexMethod getDeclaredConstructor;
+  private final DexMethod getMethod;
+  private final DexMethod getDeclaredMethod;
+
+  ClassOptimizer(AppView<?> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.options = appView.options();
+    this.dexItemFactory = dexItemFactory;
+    getConstructor = dexItemFactory.classMethods.getConstructor;
+    getDeclaredConstructor = dexItemFactory.classMethods.getDeclaredConstructor;
+    getMethod = dexItemFactory.classMethods.getMethod;
+    getDeclaredMethod = dexItemFactory.classMethods.getDeclaredMethod;
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.classType;
+  }
+
+  @Override
+  @SuppressWarnings("ReferenceEquality")
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    DexMethod singleTargetReference = singleTarget.getReference();
+    if (singleTargetReference.isIdenticalTo(getConstructor)
+        || singleTargetReference.isIdenticalTo(getDeclaredConstructor)
+        || singleTargetReference.isIdenticalTo(getMethod)
+        || singleTargetReference.isIdenticalTo(getDeclaredMethod)) {
+      EmptyVarargsUtil.replaceWithNullIfEmptyArray(
+          invoke.getLastArgument(), code, instructionIterator, options, affectedValues);
+      assert instructionIterator.peekPrevious() == invoke;
+    }
+    return instructionIterator;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ConstructorOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ConstructorOptimizer.java
new file mode 100644
index 0000000..718ed8b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ConstructorOptimizer.java
@@ -0,0 +1,57 @@
+// 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.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.Set;
+
+public class ConstructorOptimizer extends StatelessLibraryMethodModelCollection {
+
+  private final InternalOptions options;
+  private final DexItemFactory dexItemFactory;
+  private final DexMethod newInstance;
+
+  ConstructorOptimizer(AppView<?> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.options = appView.options();
+    this.dexItemFactory = dexItemFactory;
+    newInstance = dexItemFactory.constructorMethods.newInstance;
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.constructorType;
+  }
+
+  @Override
+  @SuppressWarnings("ReferenceEquality")
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    DexMethod singleTargetReference = singleTarget.getReference();
+    if (singleTargetReference.isIdenticalTo(newInstance)) {
+      EmptyVarargsUtil.replaceWithNullIfEmptyArray(
+          invoke.getArgument(1), code, instructionIterator, options, affectedValues);
+      assert instructionIterator.peekPrevious() == invoke;
+    }
+    return instructionIterator;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsUtil.java b/src/main/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsUtil.java
new file mode 100644
index 0000000..a81a0c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsUtil.java
@@ -0,0 +1,29 @@
+// 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.ir.optimize.library;
+
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
+
+public class EmptyVarargsUtil {
+  public static void replaceWithNullIfEmptyArray(
+      Value value,
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InternalOptions options,
+      AffectedValues affectedValues) {
+    if (value.isDefinedByInstructionSatisfying(Instruction::isNewArrayEmpty)
+        && value.definition.asNewArrayEmpty().sizeIfConst() == 0) {
+      instructionIterator.previous();
+      value.replaceUsers(
+          instructionIterator.insertConstNullInstruction(code, options), affectedValues);
+      instructionIterator.next();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index 1dc3ca2..1ea01a3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -46,6 +46,9 @@
     this.appView = appView;
     timing.begin("Register optimizers");
     PrimitiveMethodOptimizer.forEachPrimitiveOptimizer(appView, this::register);
+    register(new ClassOptimizer(appView));
+    register(new ConstructorOptimizer(appView));
+    register(new MethodOptimizer(appView));
     register(new ObjectMethodOptimizer(appView));
     register(new ObjectsMethodOptimizer(appView));
     register(new StringBuilderMethodOptimizer(appView));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/MethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/MethodOptimizer.java
new file mode 100644
index 0000000..78b0149
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/MethodOptimizer.java
@@ -0,0 +1,57 @@
+// 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.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.Set;
+
+public class MethodOptimizer extends StatelessLibraryMethodModelCollection {
+
+  private final InternalOptions options;
+  private final DexItemFactory dexItemFactory;
+  private final DexMethod invoke;
+
+  MethodOptimizer(AppView<?> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.options = appView.options();
+    this.dexItemFactory = dexItemFactory;
+    invoke = dexItemFactory.methodMethods.invoke;
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.methodType;
+  }
+
+  @Override
+  @SuppressWarnings("ReferenceEquality")
+  public InstructionListIterator optimize(
+      IRCode code,
+      BasicBlockIterator blockIterator,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      AffectedValues affectedValues,
+      Set<BasicBlock> blocksToRemove) {
+    DexMethod singleTargetReference = singleTarget.getReference();
+    if (singleTargetReference.isIdenticalTo(this.invoke)) {
+      EmptyVarargsUtil.replaceWithNullIfEmptyArray(
+          invoke.getArgument(2), code, instructionIterator, options, affectedValues);
+      assert instructionIterator.peekPrevious() == invoke;
+    }
+    return instructionIterator;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 4cdfc22..63e6bbb 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -5177,7 +5177,9 @@
     int parametersSize =
         newArrayEmpty != null
             ? newArrayEmpty.sizeIfConst()
-            : newArrayFilled != null ? newArrayFilled.size() : -1;
+            : newArrayFilled != null
+                ? newArrayFilled.size()
+                : parametersValue.isAlwaysNull(appView) ? 0 : -1;
     if (parametersSize < 0) {
       return;
     }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsTest.java
new file mode 100644
index 0000000..2384387
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/library/EmptyVarargsTest.java
@@ -0,0 +1,98 @@
+// 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.ir.optimize.library;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class EmptyVarargsTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EmptyVarargsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepAnnotation()
+        .addKeepRules("-keepclassmembers class * { @com.android.tools.r8.Keep *; }")
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+
+              MethodSubject testMethod =
+                  mainClassSubject.uniqueMethodWithOriginalName("testDeclared");
+              assertTrue(testMethod.streamInstructions().noneMatch(InstructionSubject::isNewArray));
+              testMethod = mainClassSubject.uniqueMethodWithOriginalName("testNonDeclared");
+              assertTrue(testMethod.streamInstructions().noneMatch(InstructionSubject::isNewArray));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("hi", "hi");
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      try {
+        testDeclared();
+        testNonDeclared();
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Keep
+    public Main() {}
+
+    @Keep
+    public void doPrint() {
+      System.out.println("hi");
+    }
+
+    @NeverInline
+    static void testNonDeclared() throws Exception {
+      Constructor<Main> ctor = Main.class.getConstructor();
+      Main instance = ctor.newInstance();
+      Method method = Main.class.getMethod("doPrint");
+      method.invoke(instance);
+    }
+
+    @NeverInline
+    static void testDeclared() throws Exception {
+      Constructor<Main> ctor = Main.class.getDeclaredConstructor();
+      Main instance = ctor.newInstance();
+      Method method = Main.class.getDeclaredMethod("doPrint");
+      method.invoke(instance);
+    }
+  }
+}