Version 1.4.95

Cherry pick: Remove parameter annotations in unused argument removal
CL: https://r8-review.googlesource.com/c/r8/+/37882

Cherry pick: Remove parameter annotations in uninstantiated type optimization
CL: https://r8-review.googlesource.com/c/r8/+/37883

Bug: 131663970, 131718819
Change-Id: I97debc4a7f923ff55c04e6709c45e1bf1bab3fa5
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index fa693c5..356d63d 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.4.94";
+  public static final String LABEL = "1.4.95";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
index 44b09b0..7bafd01 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
@@ -12,6 +12,8 @@
 
 public class DexAnnotationSet extends CachedHashValueDexItem {
 
+  public static final DexAnnotationSet[] EMPTY_ARRAY = {};
+
   private static final int UNSORTED = 0;
   private static final DexAnnotationSet THE_EMPTY_ANNOTATIONS_SET =
       new DexAnnotationSet(new DexAnnotation[0]);
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 8cab73b..79c3b28 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -46,11 +46,13 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.function.IntPredicate;
 
 public class DexEncodedMethod extends KeyedDexItem<DexMethod> implements ResolutionResult {
 
@@ -614,15 +616,25 @@
 
   public DexEncodedMethod toTypeSubstitutedMethod(DexMethod method) {
     checkIfObsolete();
+    return toTypeSubstitutedMethod(method, null);
+  }
+
+  public DexEncodedMethod toTypeSubstitutedMethod(DexMethod method, Consumer<Builder> consumer) {
+    checkIfObsolete();
     if (this.method == method) {
       return this;
     }
     Builder builder = builder(this);
     builder.setMethod(method);
     // TODO(b/112847660): Fix type fixers that use this method: ProguardMapApplier
-    // TODO(b/112847660): Fix type fixers that use this method: Staticizer
-    // TODO(b/112847660): Fix type fixers that use this method: VerticalClassMerger
+    // TODO(b/112847660): Fix type fixers that use this method: Class staticizer
+    // TODO(b/112847660): Fix type fixers that use this method: Uninstantiated type optimization
+    // TODO(b/112847660): Fix type fixers that use this method: Unused argument removal
+    // TODO(b/112847660): Fix type fixers that use this method: Vertical class merger
     // setObsolete();
+    if (consumer != null) {
+      consumer.accept(builder);
+    }
     return builder.build();
   }
 
@@ -1226,12 +1238,12 @@
     return new Builder(from);
   }
 
-  private static class Builder {
+  public static class Builder {
 
     private DexMethod method;
     private final MethodAccessFlags accessFlags;
     private final DexAnnotationSet annotations;
-    private final ParameterAnnotationsList parameterAnnotations;
+    private ParameterAnnotationsList parameterAnnotations;
     private Code code;
     private CompilationState compilationState;
     private OptimizationInfo optimizationInfo;
@@ -1242,17 +1254,63 @@
       method = from.method;
       accessFlags = from.accessFlags.copy();
       annotations = from.annotations;
-      parameterAnnotations = from.parameterAnnotationsList;
       code = from.code;
       compilationState = from.compilationState;
       optimizationInfo = from.optimizationInfo.mutableCopy();
       classFileVersion = from.classFileVersion;
+
+      if (from.parameterAnnotationsList.isEmpty()
+          || from.parameterAnnotationsList.size() == method.proto.parameters.size()) {
+        parameterAnnotations = from.parameterAnnotationsList;
+      } else {
+        assert false
+            : "Parameter annotations does not match proto of method `"
+                + method.toSourceString()
+                + "` (was: "
+                + parameterAnnotations
+                + ")";
+        parameterAnnotations = ParameterAnnotationsList.empty();
+      }
     }
 
     public void setMethod(DexMethod method) {
       this.method = method;
     }
 
+    public Builder setParameterAnnotations(ParameterAnnotationsList parameterAnnotations) {
+      this.parameterAnnotations = parameterAnnotations;
+      return this;
+    }
+
+    public Builder removeParameterAnnotations(IntPredicate predicate) {
+      if (parameterAnnotations.isEmpty()) {
+        // Nothing to do.
+        return this;
+      }
+
+      List<DexAnnotationSet> newParameterAnnotations = new ArrayList<>();
+      int newNumberOfMissingParameterAnnotations = 0;
+
+      for (int oldIndex = 0; oldIndex < parameterAnnotations.size(); oldIndex++) {
+        if (!predicate.test(oldIndex)) {
+          if (parameterAnnotations.isMissing(oldIndex)) {
+            newNumberOfMissingParameterAnnotations++;
+          } else {
+            newParameterAnnotations.add(parameterAnnotations.get(oldIndex));
+          }
+        }
+      }
+
+      if (newParameterAnnotations.isEmpty()) {
+        return setParameterAnnotations(ParameterAnnotationsList.empty());
+      }
+
+      return setParameterAnnotations(
+          new ParameterAnnotationsList(
+              newParameterAnnotations.toArray(DexAnnotationSet.EMPTY_ARRAY),
+              newNumberOfMissingParameterAnnotations));
+    }
+
     public Builder setStatic() {
       this.accessFlags.setStatic();
       return this;
@@ -1282,6 +1340,8 @@
       assert accessFlags != null;
       assert annotations != null;
       assert parameterAnnotations != null;
+      assert parameterAnnotations.isEmpty()
+          || parameterAnnotations.size() == method.proto.parameters.size();
       DexEncodedMethod result =
           new DexEncodedMethod(
               method, accessFlags, annotations, parameterAnnotations, code, classFileVersion);
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index b591bab..72c20a3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -13,7 +13,7 @@
 
 public class DexString extends IndexedDexItem implements PresortedComparable<DexString> {
 
-  public static final DexString[] EMPTY_ARRAY = new DexString[]{};
+  public static final DexString[] EMPTY_ARRAY = {};
 
   public final int size;  // size of this string, in UTF-16
   public final byte[] content;
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index 402b56d..9e124f2 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.IteratorUtils;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -29,6 +30,7 @@
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
 /**
@@ -217,6 +219,18 @@
         }
         return new RemovedArgumentsInfo(newRemovedArguments);
       }
+
+      public Consumer<DexEncodedMethod.Builder> createParameterAnnotationsRemover(
+          DexEncodedMethod method) {
+        if (numberOfRemovedArguments() > 0 && !method.parameterAnnotationsList.isEmpty()) {
+          return builder -> {
+            int firstArgumentIndex = BooleanUtils.intValue(!method.isStatic());
+            builder.removeParameterAnnotations(
+                oldIndex -> isArgumentRemoved(oldIndex + firstArgumentIndex));
+          };
+        }
+        return null;
+      }
     }
 
     private static final RewrittenPrototypeDescription none = new RewrittenPrototypeDescription();
diff --git a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
index 97ae20c..68c7da1 100644
--- a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
+++ b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
@@ -46,7 +46,7 @@
   }
 
   private ParameterAnnotationsList() {
-    this.values = new DexAnnotationSet[0];
+    this.values = DexAnnotationSet.EMPTY_ARRAY;
     this.missingParameterAnnotations = 0;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
index c0d0266..68f5002 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
@@ -208,7 +208,11 @@
         // TODO(b/110806787): Can be extended to handle collisions by renaming the given
         // method.
         if (usedSignatures.add(wrapper)) {
-          clazz.setDirectMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
+          clazz.setDirectMethod(
+              i,
+              encodedMethod.toTypeSubstitutedMethod(
+                  newMethod,
+                  removedArgumentsInfo.createParameterAnnotationsRemover(encodedMethod)));
           methodMapping.put(method, newMethod);
           if (removedArgumentsInfo.hasRemovedArguments()) {
             removedArgumentsInfoPerMethod.put(newMethod, removedArgumentsInfo);
@@ -229,6 +233,7 @@
       DexMethod method = encodedMethod.method;
       RewrittenPrototypeDescription prototypeChanges =
           getPrototypeChanges(encodedMethod, DISALLOW_ARGUMENT_REMOVAL);
+      RemovedArgumentsInfo removedArgumentsInfo = prototypeChanges.getRemovedArgumentsInfo();
       DexMethod newMethod = getNewMethodSignature(encodedMethod, prototypeChanges);
       if (newMethod != method) {
         Wrapper<DexMethod> wrapper = equivalence.wrap(newMethod);
@@ -242,7 +247,11 @@
           boolean signatureIsAvailable = usedSignatures.add(wrapper);
           assert signatureIsAvailable;
 
-          clazz.setVirtualMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
+          clazz.setVirtualMethod(
+              i,
+              encodedMethod.toTypeSubstitutedMethod(
+                  newMethod,
+                  removedArgumentsInfo.createParameterAnnotationsRemover(encodedMethod)));
           methodMapping.put(method, newMethod);
         }
       }
@@ -252,6 +261,7 @@
       DexMethod method = encodedMethod.method;
       RewrittenPrototypeDescription prototypeChanges =
           getPrototypeChanges(encodedMethod, DISALLOW_ARGUMENT_REMOVAL);
+      RemovedArgumentsInfo removedArgumentsInfo = prototypeChanges.getRemovedArgumentsInfo();
       DexMethod newMethod = getNewMethodSignature(encodedMethod, prototypeChanges);
       if (newMethod != method) {
         Wrapper<DexMethod> wrapper = equivalence.wrap(newMethod);
@@ -262,7 +272,11 @@
         if (!methodPool.hasSeen(wrapper) && usedSignatures.add(wrapper)) {
           methodPool.seen(wrapper);
 
-          clazz.setVirtualMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod));
+          clazz.setVirtualMethod(
+              i,
+              encodedMethod.toTypeSubstitutedMethod(
+                  newMethod,
+                  removedArgumentsInfo.createParameterAnnotationsRemover(encodedMethod)));
           methodMapping.put(method, newMethod);
 
           boolean added =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
index fe2c067..450a549 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
@@ -174,7 +174,8 @@
       } while (!isMethodSignatureAvailable(newSignature));
       markSignatureAsNoLongerUsed(method.method);
       markSignatureAsUsed(newSignature);
-      return method.toTypeSubstitutedMethod(newSignature);
+      return method.toTypeSubstitutedMethod(
+          newSignature, unused.createParameterAnnotationsRemover(method));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/BooleanUtils.java b/src/main/java/com/android/tools/r8/utils/BooleanUtils.java
index bacea6f..cc94778 100644
--- a/src/main/java/com/android/tools/r8/utils/BooleanUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/BooleanUtils.java
@@ -7,6 +7,10 @@
 
   private static final Boolean[] VALUES = new Boolean[] { Boolean.TRUE, Boolean.FALSE };
 
+  public static int intValue(boolean value) {
+    return value ? 1 : 0;
+  }
+
   public static Boolean[] values() {
     return VALUES;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index c91c288..2c2bcac 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -281,4 +281,12 @@
 
     return new String(characters);
   }
+
+  public static String times(String string, int count) {
+    StringBuilder builder = new StringBuilder();
+    while (--count >= 0) {
+      builder.append(string);
+    }
+    return builder.toString();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 7f06ea2..5900399c 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -183,19 +183,35 @@
   }
 
   public R8TestBuilder enableConstantArgumentAnnotations() {
-    if (!enableConstantArgumentAnnotations) {
-      enableConstantArgumentAnnotations = true;
-      addInternalKeepRules(
-          "-keepconstantarguments class * { @com.android.tools.r8.KeepConstantArguments *; }");
+    return enableConstantArgumentAnnotations(true);
+  }
+
+  public R8TestBuilder enableConstantArgumentAnnotations(boolean value) {
+    if (value) {
+      if (!enableConstantArgumentAnnotations) {
+        enableConstantArgumentAnnotations = true;
+        addInternalKeepRules(
+            "-keepconstantarguments class * { @com.android.tools.r8.KeepConstantArguments *; }");
+      }
+    } else {
+      assert !enableConstantArgumentAnnotations;
     }
     return self();
   }
 
   public R8TestBuilder enableUnusedArgumentAnnotations() {
-    if (!enableUnusedArgumentAnnotations) {
-      enableUnusedArgumentAnnotations = true;
-      addInternalKeepRules(
-          "-keepunusedarguments class * { @com.android.tools.r8.KeepUnusedArguments *; }");
+    return enableUnusedArgumentAnnotations(true);
+  }
+
+  public R8TestBuilder enableUnusedArgumentAnnotations(boolean value) {
+    if (value) {
+      if (!enableUnusedArgumentAnnotations) {
+        enableUnusedArgumentAnnotations = true;
+        addInternalKeepRules(
+            "-keepunusedarguments class * { @com.android.tools.r8.KeepUnusedArguments *; }");
+      }
+    } else {
+      assert !enableUnusedArgumentAnnotations;
     }
     return self();
   }
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index 0047eec..d6ac028 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -118,8 +118,12 @@
     return self();
   }
 
+  public T addKeepAttributes(String... attributes) {
+    return addKeepRules("-keepattributes " + String.join(",", attributes));
+  }
+
   public T addKeepAllAttributes() {
-    return addKeepRules("-keepattributes *");
+    return addKeepAttributes("*");
   }
 
   private static String getMethodLine(MethodReference method) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java
new file mode 100644
index 0000000..a15b418
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/UninstantiatedAnnotatedArgumentsTest.java
@@ -0,0 +1,232 @@
+// Copyright (c) 2019, 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.uninstantiatedtypes;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.KeepConstantArguments;
+import com.android.tools.r8.KeepUnusedArguments;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UninstantiatedAnnotatedArgumentsTest extends TestBase {
+
+  private final boolean keepUninstantiatedArguments;
+  private final Backend backend;
+
+  @Parameters(name = "{1}, keep uninstantiated arguments: {0}")
+  public static List<Object[]> params() {
+    return buildParameters(BooleanUtils.values(), Backend.values());
+  }
+
+  public UninstantiatedAnnotatedArgumentsTest(
+      boolean keepUninstantiatedArguments, Backend backend) {
+    this.keepUninstantiatedArguments = keepUninstantiatedArguments;
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(backend)
+        .addInnerClasses(UninstantiatedAnnotatedArgumentsTest.class)
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassRules(Instantiated.class, Uninstantiated.class)
+        .addKeepAttributes("RuntimeVisibleParameterAnnotations")
+        .enableClassInliningAnnotations()
+        .enableConstantArgumentAnnotations(keepUninstantiatedArguments)
+        .enableInliningAnnotations()
+        .enableUnusedArgumentAnnotations()
+        // TODO(b/123060011): Mapping not working in presence of argument removal.
+        .minification(keepUninstantiatedArguments)
+        .compile()
+        .inspect(this::verifyOutput)
+        .run(TestClass.class)
+        .assertSuccessWithOutput(StringUtils.times(StringUtils.lines("Hello world!"), 6));
+  }
+
+  private void verifyOutput(CodeInspector inspector) {
+    ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+    assertThat(testClassSubject, isPresent());
+
+    ClassSubject instantiatedClassSubject = inspector.clazz(Instantiated.class);
+    assertThat(instantiatedClassSubject, isPresent());
+
+    ClassSubject uninstantiatedClassSubject = inspector.clazz(Uninstantiated.class);
+    assertThat(uninstantiatedClassSubject, isPresent());
+
+    List<MethodSubject> methodSubjects =
+        ImmutableList.of(
+            testClassSubject.uniqueMethodWithName("testRemoveStaticFromStart"),
+            testClassSubject.uniqueMethodWithName("testRemoveStaticFromMiddle"),
+            testClassSubject.uniqueMethodWithName("testRemoveStaticFromEnd"),
+            testClassSubject.uniqueMethodWithName("testRemoveVirtualFromStart"),
+            testClassSubject.uniqueMethodWithName("testRemoveVirtualFromMiddle"),
+            testClassSubject.uniqueMethodWithName("testRemoveVirtualFromEnd"));
+
+    for (MethodSubject methodSubject : methodSubjects) {
+      assertThat(methodSubject, isPresent());
+
+      // TODO(b/131735725): Should also remove arguments from the virtual methods.
+      if (keepUninstantiatedArguments || methodSubject.getOriginalName().contains("Virtual")) {
+        assertEquals(3, methodSubject.getMethod().method.proto.parameters.size());
+        assertEquals(3, methodSubject.getMethod().parameterAnnotationsList.size());
+
+        for (int i = 0; i < 3; ++i) {
+          DexAnnotationSet annotationSet =
+              methodSubject.getMethod().parameterAnnotationsList.get(i);
+          assertEquals(1, annotationSet.annotations.length);
+
+          DexAnnotation annotation = annotationSet.annotations[0];
+          if (i == getPositionOfUnusedArgument(methodSubject)) {
+            assertEquals(
+                uninstantiatedClassSubject.getFinalName(),
+                annotation.annotation.type.toSourceString());
+          } else {
+            assertEquals(
+                instantiatedClassSubject.getFinalName(),
+                annotation.annotation.type.toSourceString());
+          }
+        }
+      } else {
+        assertEquals(2, methodSubject.getMethod().method.proto.parameters.size());
+        assertEquals(2, methodSubject.getMethod().parameterAnnotationsList.size());
+
+        for (int i = 0; i < 2; ++i) {
+          DexAnnotationSet annotationSet =
+              methodSubject.getMethod().parameterAnnotationsList.get(i);
+          assertEquals(1, annotationSet.annotations.length);
+
+          DexAnnotation annotation = annotationSet.annotations[0];
+          assertEquals(
+              instantiatedClassSubject.getFinalName(), annotation.annotation.type.toSourceString());
+        }
+      }
+    }
+  }
+
+  private static int getPositionOfUnusedArgument(MethodSubject methodSubject) {
+    switch (methodSubject.getOriginalName(false)) {
+      case "testRemoveStaticFromStart":
+      case "testRemoveVirtualFromStart":
+        return 0;
+
+      case "testRemoveStaticFromMiddle":
+      case "testRemoveVirtualFromMiddle":
+        return 1;
+
+      case "testRemoveStaticFromEnd":
+      case "testRemoveVirtualFromEnd":
+        return 2;
+
+      default:
+        throw new Unreachable();
+    }
+  }
+
+  @NeverClassInline
+  static class TestClass {
+
+    public static void main(String[] args) {
+      testRemoveStaticFromStart(null, "Hello", " world!");
+      testRemoveStaticFromMiddle("Hello", null, " world!");
+      testRemoveStaticFromEnd("Hello", " world!", null);
+      new TestClass().testRemoveVirtualFromStart(null, "Hello", " world!");
+      new TestClass().testRemoveVirtualFromMiddle("Hello", null, " world!");
+      new TestClass().testRemoveVirtualFromEnd("Hello", " world!", null);
+    }
+
+    @KeepConstantArguments
+    @KeepUnusedArguments
+    @NeverInline
+    static void testRemoveStaticFromStart(
+        @Uninstantiated Dead uninstantiated,
+        @Instantiated String instantiated,
+        @Instantiated String otherInstantiated) {
+      System.out.println(instantiated + otherInstantiated);
+    }
+
+    @KeepConstantArguments
+    @KeepUnusedArguments
+    @NeverInline
+    static void testRemoveStaticFromMiddle(
+        @Instantiated String instantiated,
+        @Uninstantiated Dead uninstantiated,
+        @Instantiated String otherInstantiated) {
+      System.out.println(instantiated + otherInstantiated);
+    }
+
+    @KeepConstantArguments
+    @KeepUnusedArguments
+    @NeverInline
+    static void testRemoveStaticFromEnd(
+        @Instantiated String instantiated,
+        @Instantiated String otherInstantiated,
+        @Uninstantiated Dead uninstantiated) {
+      System.out.println(instantiated + otherInstantiated);
+    }
+
+    @KeepConstantArguments
+    @KeepUnusedArguments
+    @NeverInline
+    void testRemoveVirtualFromStart(
+        @Uninstantiated Dead uninstantiated,
+        @Instantiated String instantiated,
+        @Instantiated String otherInstantiated) {
+      System.out.println(instantiated + otherInstantiated);
+    }
+
+    @KeepConstantArguments
+    @KeepUnusedArguments
+    @NeverInline
+    void testRemoveVirtualFromMiddle(
+        @Instantiated String instantiated,
+        @Uninstantiated Dead uninstantiated,
+        @Instantiated String otherInstantiated) {
+      System.out.println(instantiated + otherInstantiated);
+    }
+
+    @KeepConstantArguments
+    @KeepUnusedArguments
+    @NeverInline
+    void testRemoveVirtualFromEnd(
+        @Instantiated String instantiated,
+        @Instantiated String otherInstantiated,
+        @Uninstantiated Dead uninstantiated) {
+      System.out.println(instantiated + otherInstantiated);
+    }
+  }
+
+  static class Dead {}
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.PARAMETER)
+  @interface Instantiated {}
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.PARAMETER)
+  @interface Uninstantiated {}
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java
new file mode 100644
index 0000000..128a2ec
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedAnnotatedArgumentsTest.java
@@ -0,0 +1,206 @@
+// Copyright (c) 2019, 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.unusedarguments;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.KeepUnusedArguments;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UnusedAnnotatedArgumentsTest extends TestBase {
+
+  private final boolean keepUnusedArguments;
+  private final Backend backend;
+
+  @Parameters(name = "{1}, keep unused arguments: {0}")
+  public static List<Object[]> params() {
+    return buildParameters(BooleanUtils.values(), Backend.values());
+  }
+
+  public UnusedAnnotatedArgumentsTest(boolean keepUnusedArguments, Backend backend) {
+    this.keepUnusedArguments = keepUnusedArguments;
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(backend)
+        .addInnerClasses(UnusedAnnotatedArgumentsTest.class)
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassRules(Used.class, Unused.class)
+        .addKeepAttributes("RuntimeVisibleParameterAnnotations")
+        .enableClassInliningAnnotations()
+        .enableInliningAnnotations()
+        .enableUnusedArgumentAnnotations(keepUnusedArguments)
+        // TODO(b/123060011): Mapping not working in presence of unused argument removal.
+        .minification(keepUnusedArguments)
+        .compile()
+        .inspect(this::verifyOutput)
+        .run(TestClass.class)
+        .assertSuccessWithOutput(StringUtils.times(StringUtils.lines("Hello world!"), 6));
+  }
+
+  private void verifyOutput(CodeInspector inspector) {
+    ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+    assertThat(testClassSubject, isPresent());
+
+    ClassSubject usedClassSubject = inspector.clazz(Used.class);
+    assertThat(usedClassSubject, isPresent());
+
+    ClassSubject unusedClassSubject = inspector.clazz(Unused.class);
+    assertThat(unusedClassSubject, isPresent());
+
+    List<MethodSubject> methodSubjects =
+        ImmutableList.of(
+            testClassSubject.uniqueMethodWithName("testRemoveStaticFromStart"),
+            testClassSubject.uniqueMethodWithName("testRemoveStaticFromMiddle"),
+            testClassSubject.uniqueMethodWithName("testRemoveStaticFromEnd"),
+            testClassSubject.uniqueMethodWithName("testRemoveVirtualFromStart"),
+            testClassSubject.uniqueMethodWithName("testRemoveVirtualFromMiddle"),
+            testClassSubject.uniqueMethodWithName("testRemoveVirtualFromEnd"));
+
+    for (MethodSubject methodSubject : methodSubjects) {
+      assertThat(methodSubject, isPresent());
+
+      if (keepUnusedArguments || methodSubject.getOriginalName().contains("Virtual")) {
+        assertEquals(3, methodSubject.getMethod().method.proto.parameters.size());
+        assertEquals(3, methodSubject.getMethod().parameterAnnotationsList.size());
+
+        for (int i = 0; i < 3; ++i) {
+          DexAnnotationSet annotationSet =
+              methodSubject.getMethod().parameterAnnotationsList.get(i);
+          assertEquals(1, annotationSet.annotations.length);
+
+          DexAnnotation annotation = annotationSet.annotations[0];
+          if (i == getPositionOfUnusedArgument(methodSubject)) {
+            assertEquals(
+                unusedClassSubject.getFinalName(), annotation.annotation.type.toSourceString());
+          } else {
+            assertEquals(
+                usedClassSubject.getFinalName(), annotation.annotation.type.toSourceString());
+          }
+        }
+      } else {
+        assertEquals(2, methodSubject.getMethod().method.proto.parameters.size());
+        assertEquals(2, methodSubject.getMethod().parameterAnnotationsList.size());
+
+        for (int i = 0; i < 2; ++i) {
+          DexAnnotationSet annotationSet =
+              methodSubject.getMethod().parameterAnnotationsList.get(i);
+          assertEquals(1, annotationSet.annotations.length);
+
+          DexAnnotation annotation = annotationSet.annotations[0];
+          assertEquals(
+              usedClassSubject.getFinalName(), annotation.annotation.type.toSourceString());
+        }
+      }
+    }
+  }
+
+  private static int getPositionOfUnusedArgument(MethodSubject methodSubject) {
+    switch (methodSubject.getOriginalName(false)) {
+      case "testRemoveStaticFromStart":
+      case "testRemoveVirtualFromStart":
+        return 0;
+
+      case "testRemoveStaticFromMiddle":
+      case "testRemoveVirtualFromMiddle":
+        return 1;
+
+      case "testRemoveStaticFromEnd":
+      case "testRemoveVirtualFromEnd":
+        return 2;
+
+      default:
+        throw new Unreachable();
+    }
+  }
+
+  @NeverClassInline
+  static class TestClass {
+
+    public static void main(String[] args) {
+      testRemoveStaticFromStart(null, "Hello", " world!");
+      testRemoveStaticFromMiddle("Hello", null, " world!");
+      testRemoveStaticFromEnd("Hello", " world!", null);
+      new TestClass().testRemoveVirtualFromStart(null, "Hello", " world!");
+      new TestClass().testRemoveVirtualFromMiddle("Hello", null, " world!");
+      new TestClass().testRemoveVirtualFromEnd("Hello", " world!", null);
+    }
+
+    @KeepUnusedArguments
+    @NeverInline
+    static void testRemoveStaticFromStart(
+        @Unused String unused, @Used String used, @Used String otherUsed) {
+      System.out.println(used + otherUsed);
+    }
+
+    @KeepUnusedArguments
+    @NeverInline
+    static void testRemoveStaticFromMiddle(
+        @Used String used, @Unused String unused, @Used String otherUsed) {
+      System.out.println(used + otherUsed);
+    }
+
+    @KeepUnusedArguments
+    @NeverInline
+    static void testRemoveStaticFromEnd(
+        @Used String used, @Used String otherUsed, @Unused String unused) {
+      System.out.println(used + otherUsed);
+    }
+
+    @KeepUnusedArguments
+    @NeverInline
+    void testRemoveVirtualFromStart(
+        @Unused String unused, @Used String used, @Used String otherUsed) {
+      System.out.println(used + otherUsed);
+    }
+
+    @KeepUnusedArguments
+    @NeverInline
+    void testRemoveVirtualFromMiddle(
+        @Used String used, @Unused String unused, @Used String otherUsed) {
+      System.out.println(used + otherUsed);
+    }
+
+    @KeepUnusedArguments
+    @NeverInline
+    void testRemoveVirtualFromEnd(
+        @Used String used, @Used String otherUsed, @Unused String unused) {
+      System.out.println(used + otherUsed);
+    }
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.PARAMETER)
+  @interface Used {}
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.PARAMETER)
+  @interface Unused {}
+}