[KeepAnno] Syntax for extends patterns and keep options.

Bug: b/248408342
Change-Id: I274aa8293e1538098a99807b6a565b10f479fcf3
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
index 01c493f..8b76c13 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
@@ -29,6 +29,10 @@
 
   String classTypeName() default "";
 
+  String extendsClassTypeName() default "";
+
+  Class<?> extendsClassConstant() default Object.class;
+
   String methodName() default "";
 
   String methodReturnType() default "";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
index 0018dfc..faa5587 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
@@ -19,6 +19,16 @@
     return classTypeName.replace('.', '/');
   }
 
+  public static boolean isKeepAnnotation(String descriptor, boolean visible) {
+    if (visible) {
+      return false;
+    }
+    return descriptor.equals(Edge.DESCRIPTOR)
+        || descriptor.equals(UsesReflection.DESCRIPTOR)
+        || descriptor.equals(Condition.DESCRIPTOR)
+        || descriptor.equals(Target.DESCRIPTOR);
+  }
+
   public static final class Edge {
     public static final Class<KeepEdge> CLASS = KeepEdge.class;
     public static final String DESCRIPTOR = getDescriptor(CLASS);
@@ -38,6 +48,9 @@
   public static final class Item {
     public static final String classConstant = "classConstant";
 
+    public static final String extendsClassConstant = "extendsClassConstant";
+    public static final String extendsTypeName = "extendsTypeName";
+
     public static final String methodName = "methodName";
     public static final String methodReturnType = "methodReturnType";
     public static final String methodParameters = "methodParameters";
@@ -64,5 +77,19 @@
   public static final class Target {
     public static final Class<KeepTarget> CLASS = KeepTarget.class;
     public static final String DESCRIPTOR = getDescriptor(CLASS);
+
+    public static final String allow = "allow";
+    public static final String disallow = "disallow";
+  }
+
+  public static final class Option {
+    public static final Class<KeepOption> CLASS = KeepOption.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+
+    public static final String SHRINKING = "SHRINKING";
+    public static final String OBFUSCATION = "OBFUSCATION";
+    public static final String OPTIMIZATION = "OPTIMIZATION";
+    public static final String ACCESS_MODIFICATION = "ACCESS_MODIFICATION";
+    public static final String ANNOTATION_REMOVAL = "ANNOTATION_REMOVAL";
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java
index fb9a772..d2b0356 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java
@@ -8,7 +8,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
-@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepEdge {
   KeepCondition[] preconditions() default {};
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepOption.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepOption.java
new file mode 100644
index 0000000..e3c6d31
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepOption.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2022, 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.keepanno.annotations;
+
+public enum KeepOption {
+  SHRINKING,
+  OPTIMIZATION,
+  OBFUSCATION,
+  ACCESS_MODIFICATION,
+  ANNOTATION_REMOVAL,
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index 73559d6..8a01df9 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -25,10 +25,23 @@
 @Target(ElementType.ANNOTATION_TYPE)
 @Retention(RetentionPolicy.CLASS)
 public @interface KeepTarget {
+
+  // KeepTarget only content (keep options) =========
+
+  KeepOption[] allow() default {};
+
+  KeepOption[] disallow() default {};
+
+  // Shared KeepItem content ========================
+
   Class<?> classConstant() default Object.class;
 
   String classTypeName() default "";
 
+  String extendsClassTypeName() default "";
+
+  Class<?> extendsClassConstant() default Object.class;
+
   String methodName() default "";
 
   String methodReturnType() default "";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
index 22fcb36..ca6f32c 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsesReflection.java
@@ -53,7 +53,7 @@
  *   }
  * </pre>
  */
-@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
 @Retention(RetentionPolicy.CLASS)
 public @interface UsesReflection {
   KeepTarget[] value();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index 672a99e7..ca700b0 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -7,11 +7,13 @@
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Condition;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Item;
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Option;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Target;
 import com.android.tools.r8.keepanno.ast.KeepCondition;
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepExtendsPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldTypePattern;
@@ -21,12 +23,15 @@
 import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
 import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepOptions;
+import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
 import com.android.tools.r8.keepanno.ast.KeepPreconditions;
 import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepTarget;
 import com.android.tools.r8.keepanno.ast.KeepTypePattern;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -325,7 +330,7 @@
     @Override
     public AnnotationVisitor visitAnnotation(String name, String descriptor) {
       if (descriptor.equals(Target.DESCRIPTOR)) {
-        return new KeepTargetVisitor(builder::addTarget);
+        return KeepTargetVisitor.create(builder::addTarget);
       }
       return super.visitAnnotation(name, descriptor);
     }
@@ -343,6 +348,9 @@
     private KeepMethodPattern.Builder lazyMethodBuilder = null;
     private KeepFieldPattern.Builder lazyFieldBuilder = null;
 
+    private KeepExtendsPattern extendsPattern = null;
+    private String extendPatternDeclaration = null;
+
     public KeepItemVisitorBase(Parent<KeepItemPattern> parent) {
       this.parent = parent;
     }
@@ -373,6 +381,9 @@
         classNamePattern = KeepQualifiedClassNamePattern.exact(((Type) value).getClassName());
         return;
       }
+      if (tryParseExtendsPattern(name, value)) {
+        return;
+      }
       if (name.equals(Item.methodName) && value instanceof String) {
         String methodName = (String) value;
         if (!Item.methodNameDefaultValue.equals(methodName)) {
@@ -408,6 +419,38 @@
       super.visit(name, value);
     }
 
+    private boolean tryParseExtendsPattern(String name, Object value) {
+      if (name.equals(Item.extendsClassConstant) && value instanceof Type) {
+        checkExtendsDeclaration(name);
+        extendsPattern =
+            KeepExtendsPattern.builder()
+                .classPattern(KeepQualifiedClassNamePattern.exact(((Type) value).getClassName()))
+                .build();
+        return true;
+      }
+      if (name.equals(Item.extendsTypeName) && value instanceof String) {
+        checkExtendsDeclaration(name);
+        extendsPattern =
+            KeepExtendsPattern.builder()
+                .classPattern(KeepQualifiedClassNamePattern.exact(((String) value)))
+                .build();
+        return true;
+      }
+      return false;
+    }
+
+    private void checkExtendsDeclaration(String declaration) {
+      if (extendPatternDeclaration != null) {
+        throw new KeepEdgeException(
+            "Multiple declarations defining an extends pattern: '"
+                + extendPatternDeclaration
+                + "' and '"
+                + declaration
+                + "'");
+      }
+      extendPatternDeclaration = declaration;
+    }
+
     @Override
     public AnnotationVisitor visitArray(String name) {
       if (name.equals(Item.methodParameters)) {
@@ -433,6 +476,9 @@
       if (classNamePattern != null) {
         itemBuilder.setClassPattern(classNamePattern);
       }
+      if (extendsPattern != null) {
+        itemBuilder.setExtendsPattern(extendsPattern);
+      }
       if (lazyMethodBuilder != null) {
         itemBuilder.setMemberPattern(lazyMethodBuilder.build());
       }
@@ -470,8 +516,25 @@
 
   private static class KeepTargetVisitor extends KeepItemVisitorBase {
 
-    public KeepTargetVisitor(Parent<KeepTarget> parent) {
-      super(item -> parent.accept(KeepTarget.builder().setItem(item).build()));
+    private final KeepTarget.Builder builder;
+
+    static KeepTargetVisitor create(Parent<KeepTarget> parent) {
+      KeepTarget.Builder builder = KeepTarget.builder();
+      return new KeepTargetVisitor(parent, builder);
+    }
+
+    private KeepTargetVisitor(Parent<KeepTarget> parent, KeepTarget.Builder builder) {
+      super(item -> parent.accept(builder.setItem(item).build()));
+      this.builder = builder;
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(KeepConstants.Target.disallow)) {
+        return new KeepOptionsVisitor(
+            options -> builder.setOptions(KeepOptions.disallowBuilder().addAll(options).build()));
+      }
+      return super.visitArray(name);
     }
   }
 
@@ -481,4 +544,49 @@
       super(item -> parent.accept(KeepCondition.builder().setItem(item).build()));
     }
   }
+
+  private static class KeepOptionsVisitor extends AnnotationVisitorBase {
+
+    private final Parent<Collection<KeepOption>> parent;
+    private final Set<KeepOption> options = new HashSet<>();
+
+    public KeepOptionsVisitor(Parent<Collection<KeepOption>> parent) {
+      this.parent = parent;
+    }
+
+    @Override
+    public void visitEnum(String ignore, String descriptor, String value) {
+      if (!descriptor.equals(KeepConstants.Option.DESCRIPTOR)) {
+        super.visitEnum(ignore, descriptor, value);
+      }
+      KeepOption option;
+      switch (value) {
+        case Option.SHRINKING:
+          option = KeepOption.SHRINKING;
+          break;
+        case Option.OPTIMIZATION:
+          option = KeepOption.OPTIMIZING;
+          break;
+        case Option.OBFUSCATION:
+          option = KeepOption.OBFUSCATING;
+          break;
+        case Option.ACCESS_MODIFICATION:
+          option = KeepOption.ACCESS_MODIFICATION;
+          break;
+        case Option.ANNOTATION_REMOVAL:
+          option = KeepOption.ANNOTATION_REMOVAL;
+          break;
+        default:
+          super.visitEnum(ignore, descriptor, value);
+          return;
+      }
+      options.add(option);
+    }
+
+    @Override
+    public void visitEnd() {
+      parent.accept(options);
+      super.visitEnd();
+    }
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
index e1d6d4e..0049c90 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -27,7 +27,7 @@
  *   CONSEQUENCES ::= TARGET+
  *   TARGET ::= OPTIONS ITEM_PATTERN
  *   OPTIONS ::= keep-all | OPTION+
- *   OPTION ::= shrinking | optimizing | obfuscating | access-modifying
+ *   OPTION ::= shrinking | optimizing | obfuscating | access-modification | annotation-removal
  *
  *   ITEM_PATTERN
  *     ::= any
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java
index 02c8768..ea30422 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java
@@ -7,57 +7,34 @@
 public abstract class KeepExtendsPattern {
 
   public static KeepExtendsPattern any() {
-    return Any.getInstance();
+    return Some.getAnyInstance();
   }
 
   public static class Builder {
 
-    private KeepExtendsPattern pattern;
+    private KeepExtendsPattern pattern = KeepExtendsPattern.any();
 
     private Builder() {}
 
-    public Builder any() {
-      pattern = Any.getInstance();
-      return this;
-    }
-
     public Builder classPattern(KeepQualifiedClassNamePattern pattern) {
       this.pattern = new Some(pattern);
       return this;
     }
-  }
 
-  private static class Any extends KeepExtendsPattern {
-
-    private static final Any INSTANCE = new Any();
-
-    public static Any getInstance() {
-      return INSTANCE;
-    }
-
-    @Override
-    public boolean isAny() {
-      return true;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      return this == obj;
-    }
-
-    @Override
-    public int hashCode() {
-      return System.identityHashCode(this);
-    }
-
-    @Override
-    public String toString() {
-      return "*";
+    public KeepExtendsPattern build() {
+      return pattern;
     }
   }
 
   private static class Some extends KeepExtendsPattern {
 
+    private static final KeepExtendsPattern ANY_INSTANCE =
+        new Some(KeepQualifiedClassNamePattern.any());
+
+    private static KeepExtendsPattern getAnyInstance() {
+      return ANY_INSTANCE;
+    }
+
     private final KeepQualifiedClassNamePattern pattern;
 
     public Some(KeepQualifiedClassNamePattern pattern) {
@@ -71,6 +48,11 @@
     }
 
     @Override
+    public KeepQualifiedClassNamePattern asClassNamePattern() {
+      return pattern;
+    }
+
+    @Override
     public boolean equals(Object o) {
       if (this == o) {
         return true;
@@ -100,4 +82,6 @@
   private KeepExtendsPattern() {}
 
   public abstract boolean isAny();
+
+  public abstract KeepQualifiedClassNamePattern asClassNamePattern();
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
index 50d3921..7f84090 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
@@ -29,7 +29,7 @@
 
   public static class Builder {
 
-    private KeepQualifiedClassNamePattern classNamePattern;
+    private KeepQualifiedClassNamePattern classNamePattern = KeepQualifiedClassNamePattern.any();
     private KeepExtendsPattern extendsPattern = KeepExtendsPattern.any();
     private KeepMemberPattern memberPattern = KeepMemberPattern.none();
 
@@ -58,9 +58,6 @@
     }
 
     public KeepItemPattern build() {
-      if (classNamePattern == null) {
-        throw new KeepEdgeException("Class pattern must define a class name pattern.");
-      }
       return new KeepItemPattern(classNamePattern, extendsPattern, memberPattern);
     }
   }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
index 8e1b2e9..ba647eb 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
@@ -22,7 +22,8 @@
     SHRINKING,
     OPTIMIZING,
     OBFUSCATING,
-    ACCESS_MODIFYING,
+    ACCESS_MODIFICATION,
+    ANNOTATION_REMOVAL,
   }
 
   public static KeepOptions keepAll() {
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index 286c07a..f20459e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.keepanno.ast.KeepConsequences;
 import com.android.tools.r8.keepanno.ast.KeepEdge;
 import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepExtendsPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldAccessPattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern;
 import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
@@ -103,8 +104,10 @@
   private static StringBuilder printItem(StringBuilder builder, KeepItemPattern clazzPattern) {
     builder.append("class ");
     printClassName(builder, clazzPattern.getClassNamePattern());
-    if (!clazzPattern.getExtendsPattern().isAny()) {
-      throw new Unimplemented();
+    KeepExtendsPattern extendsPattern = clazzPattern.getExtendsPattern();
+    if (!extendsPattern.isAny()) {
+      builder.append(" extends ");
+      printClassName(builder, extendsPattern.asClassNamePattern());
     }
     KeepMemberPattern member = clazzPattern.getMemberPattern();
     if (member.isNone()) {
@@ -247,8 +250,10 @@
         return "optimization";
       case OBFUSCATING:
         return "obfuscation";
-      case ACCESS_MODIFYING:
+      case ACCESS_MODIFICATION:
         return "accessmodification";
+      case ANNOTATION_REMOVAL:
+        return "annotationremoval";
       default:
         throw new Unimplemented();
     }
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnotationViaSuperTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnotationViaSuperTest.java
new file mode 100644
index 0000000..5f2237d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnotationViaSuperTest.java
@@ -0,0 +1,151 @@
+// Copyright (c) 2022, 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.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepOption;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+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.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.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepAnnotationViaSuperTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("42");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepAnnotationViaSuperTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    List<String> rules = getExtractedKeepRules();
+    System.out.println(rules);
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(getInputClassesWithoutKeepAnnotations())
+        .addKeepRules(rules)
+        .addKeepMainRule(TestClass.class)
+        .addKeepRuntimeVisibleAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkOutput);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(
+        TestClass.class,
+        Base.class,
+        SubA.class,
+        SubB.class,
+        SubC.class,
+        Anno.class,
+        UnusedAnno.class);
+  }
+
+  public List<byte[]> getInputClassesWithoutKeepAnnotations() throws Exception {
+    return KeepEdgeAnnotationsTest.getInputClassesWithoutKeepAnnotations(getInputClasses());
+  }
+
+  public List<String> getExtractedKeepRules() throws Exception {
+    List<Class<?>> classes = getInputClasses();
+    List<String> rules = new ArrayList<>();
+    for (Class<?> clazz : classes) {
+      rules.addAll(KeepEdgeAnnotationsTest.getKeepRulesForClass(clazz));
+    }
+    return rules;
+  }
+
+  private void checkOutput(CodeInspector inspector) {
+    assertThat(inspector.clazz(Base.class), isPresent());
+
+    ClassSubject classA = inspector.clazz(SubA.class);
+    assertThat(classA, isPresent());
+    assertThat(classA.annotation(Anno.class), isPresent());
+    assertThat(classA.annotation(UnusedAnno.class), isAbsent());
+
+    ClassSubject classB = inspector.clazz(SubB.class);
+    assertThat(classB, isPresent());
+    assertThat(classB.annotation(Anno.class), isPresent());
+
+    assertThat(inspector.clazz(SubC.class), isAbsent());
+  }
+
+  @Target({ElementType.TYPE})
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface Anno {
+    int value();
+  }
+
+  @Target({ElementType.TYPE})
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface UnusedAnno {
+    int value();
+  }
+
+  abstract static class Base {
+
+    @UsesReflection({
+      @KeepTarget(
+          extendsClassConstant = Base.class,
+          disallow = {KeepOption.ANNOTATION_REMOVAL})
+    })
+    public Base() {
+      Anno annotation = getClass().getAnnotation(Anno.class);
+      System.out.println(annotation.value());
+    }
+  }
+
+  @Anno(42)
+  @UnusedAnno(123)
+  static class SubA extends Base {}
+
+  @Anno(7)
+  static class SubB extends Base {}
+
+  // Unused.
+  @Anno(-1)
+  static class SubC extends Base {}
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      Base b = System.nanoTime() > 0 ? new SubA() : new SubB();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
index 8743b48..f99f3bf 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepEdgeAnnotationsTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.annotations.KeepConstants;
 import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
 import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
 import com.android.tools.r8.keepanno.asm.KeepEdgeWriter;
@@ -36,6 +37,7 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -138,6 +140,16 @@
             });
   }
 
+  public static List<byte[]> getInputClassesWithoutKeepAnnotations(Collection<Class<?>> classes)
+      throws Exception {
+    List<byte[]> transformed = new ArrayList<>(classes.size());
+    for (Class<?> clazz : classes) {
+      transformed.add(
+          transformer(clazz).removeAnnotations(KeepConstants::isKeepAnnotation).transform());
+    }
+    return transformed;
+  }
+
   @Test
   public void testAsmReader() throws Exception {
     assumeTrue(parameters.isCfRuntime());
@@ -147,11 +159,7 @@
     byte[] original = ToolHelper.getClassAsBytes(source);
     // Strip out all the annotations to ensure they are actually added again.
     byte[] stripped =
-        transformer(source)
-            .removeClassAnnotations()
-            .removeMethodAnnotations()
-            .removeFieldAnnotations()
-            .transform();
+        getInputClassesWithoutKeepAnnotations(Collections.singletonList(source)).get(0);
     // Manually add in the expected edges again.
     byte[] readded =
         transformer(stripped, clazz)
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
index f9d98e3..d692791 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationTest.java
@@ -65,12 +65,7 @@
   }
 
   public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
-    List<Class<?>> classes = getInputClasses();
-    List<byte[]> transformed = new ArrayList<>(classes.size());
-    for (Class<?> clazz : classes) {
-      transformed.add(transformer(clazz).removeAllAnnotations().transform());
-    }
-    return transformed;
+    return KeepEdgeAnnotationsTest.getInputClassesWithoutKeepAnnotations(getInputClasses());
   }
 
   public List<String> getExtractedKeepRules() throws Exception {
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationWithAdditionalPreconditionTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationWithAdditionalPreconditionTest.java
index 732f2d2..33f2531 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationWithAdditionalPreconditionTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionAnnotationWithAdditionalPreconditionTest.java
@@ -64,12 +64,7 @@
   }
 
   public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
-    List<Class<?>> classes = getInputClasses();
-    List<byte[]> transformed = new ArrayList<>(classes.size());
-    for (Class<?> clazz : classes) {
-      transformed.add(transformer(clazz).removeAllAnnotations().transform());
-    }
-    return transformed;
+    return KeepEdgeAnnotationsTest.getInputClassesWithoutKeepAnnotations(getInputClasses());
   }
 
   public List<String> getExtractedKeepRules() throws Exception {
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionFieldAnnotationTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionFieldAnnotationTest.java
index b6cc560..bfc7fb5 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionFieldAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionFieldAnnotationTest.java
@@ -64,12 +64,7 @@
   }
 
   public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
-    List<Class<?>> classes = getInputClasses();
-    List<byte[]> transformed = new ArrayList<>(classes.size());
-    for (Class<?> clazz : classes) {
-      transformed.add(transformer(clazz).removeAllAnnotations().transform());
-    }
-    return transformed;
+    return KeepEdgeAnnotationsTest.getInputClassesWithoutKeepAnnotations(getInputClasses());
   }
 
   public List<String> getExtractedKeepRules() throws Exception {
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionOnFieldTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionOnFieldTest.java
index 6739144..b362802 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionOnFieldTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepUsesReflectionOnFieldTest.java
@@ -66,12 +66,7 @@
   }
 
   public List<byte[]> getInputClassesWithoutAnnotations() throws Exception {
-    List<Class<?>> classes = getInputClasses();
-    List<byte[]> transformed = new ArrayList<>(classes.size());
-    for (Class<?> clazz : classes) {
-      transformed.add(transformer(clazz).removeAllAnnotations().transform());
-    }
-    return transformed;
+    return KeepEdgeAnnotationsTest.getInputClassesWithoutKeepAnnotations(getInputClasses());
   }
 
   public List<String> getExtractedKeepRules() throws Exception {
diff --git a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
index d9a6eaf..0386448 100644
--- a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
@@ -11,6 +11,8 @@
 import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
 import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -62,10 +64,10 @@
                     .build())
             .build();
     // Disallow will issue the full inverse of the known options, e.g., 'allowaccessmodification'.
-    assertEquals(
-        StringUtils.unixLines(
-            "-keep,allowshrinking,allowobfuscation,allowaccessmodification class * { *; }"),
-        extract(edge));
+    List<String> options =
+        ImmutableList.of("shrinking", "obfuscation", "accessmodification", "annotationremoval");
+    String allows = String.join(",allow", options);
+    assertEquals(StringUtils.unixLines("-keep,allow" + allows + " class * { *; }"), extract(edge));
   }
 
   @Test
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 1974bcd..7742119 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -1463,32 +1463,51 @@
         });
   }
 
-  public ClassFileTransformer removeAllAnnotations() {
-    return removeClassAnnotations().removeMethodAnnotations().removeFieldAnnotations();
+  public interface AnnotationPredicate {
+    boolean test(String descriptor, boolean visible);
+
+    static AnnotationPredicate any() {
+      return (descriptor, visible) -> true;
+    }
   }
 
-  public ClassFileTransformer removeClassAnnotations() {
+  public ClassFileTransformer removeAllAnnotations() {
+    return removeAnnotations(AnnotationPredicate.any());
+  }
+
+  public ClassFileTransformer removeAnnotations(AnnotationPredicate predicate) {
+    return removeClassAnnotations(predicate)
+        .removeMethodAnnotations(predicate)
+        .removeFieldAnnotations(predicate);
+  }
+
+  public ClassFileTransformer removeClassAnnotations(AnnotationPredicate predicate) {
     return addClassTransformer(
         new ClassTransformer() {
           @Override
           public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
-            // Ignore all input annotations.
-            return null;
+            if (predicate.test(descriptor, visible)) {
+              return null;
+            }
+            return super.visitAnnotation(descriptor, visible);
           }
         });
   }
 
-  public ClassFileTransformer removeMethodAnnotations() {
+  public ClassFileTransformer removeMethodAnnotations(AnnotationPredicate predicate) {
     return addMethodTransformer(
         new MethodTransformer() {
           @Override
           public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
-            return null;
+            if (predicate.test(descriptor, visible)) {
+              return null;
+            }
+            return super.visitAnnotation(descriptor, visible);
           }
         });
   }
 
-  public ClassFileTransformer removeFieldAnnotations() {
+  public ClassFileTransformer removeFieldAnnotations(AnnotationPredicate predicate) {
     return addClassTransformer(
         new ClassTransformer() {
           @Override
@@ -1498,7 +1517,10 @@
             return new FieldVisitor(ASM_VERSION, fv) {
               @Override
               public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
-                return null;
+                if (predicate.test(descriptor, visible)) {
+                  return null;
+                }
+                return super.visitAnnotation(descriptor, visible);
               }
             };
           }