Support for retaining annotations on items subject to optimization

Bug: 190469643
Change-Id: I6d98297a49366a32770e6df6e8b48810fd7bafa7
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 6a4089d..fdf4ea0 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -265,15 +265,17 @@
   private void removeAnnotations(ProgramDefinition definition, KeepInfo<?, ?> keepInfo) {
     boolean isAnnotation =
         definition.isProgramClass() && definition.asProgramClass().isAnnotation();
-    if ((options.isForceProguardCompatibilityEnabled() || keepInfo.isPinned())) {
-      definition.rewriteAllAnnotations(
-          (annotation, kind) -> rewriteAnnotation(definition, annotation, kind));
-    } else if (!isAnnotation) {
-      definition.clearAllAnnotations();
+    if (keepInfo.isAnnotationRemovalAllowed(options)) {
+      if (isAnnotation) {
+        definition.rewriteAllAnnotations(
+            (annotation, isParameterAnnotation) ->
+                shouldRetainAnnotationOnAnnotationClass(annotation) ? annotation : null);
+      } else {
+        definition.clearAllAnnotations();
+      }
     } else {
       definition.rewriteAllAnnotations(
-          (annotation, isParameterAnnotation) ->
-              shouldRetainAnnotationOnAnnotationClass(annotation) ? annotation : null);
+          (annotation, kind) -> rewriteAnnotation(definition, annotation, kind));
     }
   }
 
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 1e2cab6..b1c0f56 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3036,7 +3036,12 @@
       // This is simulating the effect of the "root set" applied rules.
       // This is done only in the initial pass, in subsequent passes the "rules" are reapplied
       // by iterating the instances.
+      assert appView.options().isAnnotationRemovalEnabled()
+          || rootSet.noAnnotationRemoval.isEmpty();
       assert appView.options().isMinificationEnabled() || rootSet.noObfuscation.isEmpty();
+      for (DexReference reference : rootSet.noAnnotationRemoval) {
+        keepInfo.evaluateRule(reference, appInfo, Joiner::disallowAnnotationRemoval);
+      }
       for (DexReference reference : rootSet.noObfuscation) {
         keepInfo.evaluateRule(reference, appInfo, Joiner::disallowMinification);
       }
@@ -3114,12 +3119,15 @@
           joiner.requireAccessModificationForRepackaging();
         }
       }
-      if (!modifiers.allowsObfuscation) {
-        joiner.disallowMinification();
-      }
       if (!modifiers.allowsAccessModification) {
         joiner.disallowAccessModification();
       }
+      if (!modifiers.allowsAnnotationRemoval) {
+        joiner.disallowAnnotationRemoval();
+      }
+      if (!modifiers.allowsObfuscation) {
+        joiner.disallowMinification();
+      }
     }
   }
 
@@ -3733,6 +3741,9 @@
     // TODO(b/132600955): This modifies the root set. Should the consequent be persistent?
     rootSet.addConsequentRootSet(consequentRootSet, addNoShrinking);
     if (mode.isInitialTreeShaking()) {
+      for (DexReference reference : consequentRootSet.noAnnotationRemoval) {
+        keepInfo.evaluateRule(reference, appView, Joiner::disallowAnnotationRemoval);
+      }
       for (DexReference reference : consequentRootSet.noObfuscation) {
         keepInfo.evaluateRule(reference, appView, Joiner::disallowMinification);
       }
diff --git a/src/main/java/com/android/tools/r8/shaking/GlobalKeepInfoConfiguration.java b/src/main/java/com/android/tools/r8/shaking/GlobalKeepInfoConfiguration.java
index e509230..cd2df59 100644
--- a/src/main/java/com/android/tools/r8/shaking/GlobalKeepInfoConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/GlobalKeepInfoConfiguration.java
@@ -6,6 +6,8 @@
 /** Globally controlled settings that affect the default values for kept items. */
 public interface GlobalKeepInfoConfiguration {
 
+  boolean isAnnotationRemovalEnabled();
+
   boolean isTreeShakingEnabled();
 
   boolean isMinificationEnabled();
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
index acf36aa..559362a 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
@@ -11,32 +11,55 @@
 public abstract class KeepInfo<B extends Builder<B, K>, K extends KeepInfo<B, K>> {
 
   private final boolean pinned;
-  private final boolean allowMinification;
   private final boolean allowAccessModification;
+  private final boolean allowAnnotationRemoval;
+  private final boolean allowMinification;
   private final boolean requireAccessModificationForRepackaging;
 
   private KeepInfo(
       boolean pinned,
-      boolean allowMinification,
       boolean allowAccessModification,
+      boolean allowAnnotationRemoval,
+      boolean allowMinification,
       boolean requireAccessModificationForRepackaging) {
     this.pinned = pinned;
-    this.allowMinification = allowMinification;
     this.allowAccessModification = allowAccessModification;
+    this.allowAnnotationRemoval = allowAnnotationRemoval;
+    this.allowMinification = allowMinification;
     this.requireAccessModificationForRepackaging = requireAccessModificationForRepackaging;
   }
 
   KeepInfo(B builder) {
     this(
         builder.isPinned(),
-        builder.isMinificationAllowed(),
         builder.isAccessModificationAllowed(),
+        builder.isAnnotationRemovalAllowed(),
+        builder.isMinificationAllowed(),
         builder.isAccessModificationRequiredForRepackaging());
   }
 
   abstract B builder();
 
-  /** True if an item must be present in the output. */
+  /**
+   * True if an item may have all of its annotations removed.
+   *
+   * <p>If this returns false, some annotations may still be removed if the configuration does not
+   * keep all annotation attributes.
+   */
+  public boolean isAnnotationRemovalAllowed(GlobalKeepInfoConfiguration configuration) {
+    return configuration.isAnnotationRemovalEnabled() && internalIsAnnotationRemovalAllowed();
+  }
+
+  boolean internalIsAnnotationRemovalAllowed() {
+    return allowAnnotationRemoval;
+  }
+
+  /**
+   * True if an item must be present in the output.
+   *
+   * @deprecated Prefer task dependent predicates.
+   */
+  @Deprecated
   public boolean isPinned() {
     return pinned;
   }
@@ -151,8 +174,9 @@
 
     private K original;
     private boolean pinned;
-    private boolean allowMinification;
     private boolean allowAccessModification;
+    private boolean allowAnnotationRemoval;
+    private boolean allowMinification;
     private boolean requireAccessModificationForRepackaging;
 
     Builder() {
@@ -162,25 +186,28 @@
     Builder(K original) {
       this.original = original;
       pinned = original.isPinned();
-      allowMinification = original.internalIsMinificationAllowed();
       allowAccessModification = original.internalIsAccessModificationAllowed();
+      allowAnnotationRemoval = original.internalIsAnnotationRemovalAllowed();
+      allowMinification = original.internalIsMinificationAllowed();
       requireAccessModificationForRepackaging =
           original.internalIsAccessModificationRequiredForRepackaging();
     }
 
     B makeTop() {
       pin();
+      disallowAccessModification();
+      disallowAnnotationRemoval();
       disallowMinification();
       requireAccessModificationForRepackaging();
-      disallowAccessModification();
       return self();
     }
 
     B makeBottom() {
       unpin();
+      allowAccessModification();
+      allowAnnotationRemoval();
       allowMinification();
       unsetRequireAccessModificationForRepackaging();
-      allowAccessModification();
       return self();
     }
 
@@ -201,10 +228,11 @@
 
     private boolean internalIsEqualTo(K other) {
       return isPinned() == other.isPinned()
+          && isAccessModificationAllowed() == other.internalIsAccessModificationAllowed()
+          && isAnnotationRemovalAllowed() == other.internalIsAnnotationRemovalAllowed()
           && isMinificationAllowed() == other.internalIsMinificationAllowed()
           && isAccessModificationRequiredForRepackaging()
               == other.internalIsAccessModificationRequiredForRepackaging()
-          && isAccessModificationAllowed() == other.internalIsAccessModificationAllowed()
           && isEqualTo(other);
     }
 
@@ -212,10 +240,6 @@
       return pinned;
     }
 
-    public boolean isMinificationAllowed() {
-      return allowMinification;
-    }
-
     public boolean isAccessModificationRequiredForRepackaging() {
       return requireAccessModificationForRepackaging;
     }
@@ -224,6 +248,14 @@
       return allowAccessModification;
     }
 
+    public boolean isAnnotationRemovalAllowed() {
+      return allowAnnotationRemoval;
+    }
+
+    public boolean isMinificationAllowed() {
+      return allowMinification;
+    }
+
     public B setPinned(boolean pinned) {
       this.pinned = pinned;
       return self();
@@ -276,6 +308,19 @@
     public B disallowAccessModification() {
       return setAllowAccessModification(false);
     }
+
+    public B setAllowAnnotationRemoval(boolean allowAnnotationRemoval) {
+      this.allowAnnotationRemoval = allowAnnotationRemoval;
+      return self();
+    }
+
+    public B allowAnnotationRemoval() {
+      return setAllowAnnotationRemoval(true);
+    }
+
+    public B disallowAnnotationRemoval() {
+      return setAllowAnnotationRemoval(false);
+    }
   }
 
   /** Joiner to construct monotonically increasing keep info object. */
@@ -304,13 +349,18 @@
       return self();
     }
 
-    public J disallowMinification() {
-      builder.disallowMinification();
+    public J disallowAccessModification() {
+      builder.disallowAccessModification();
       return self();
     }
 
-    public J disallowAccessModification() {
-      builder.disallowAccessModification();
+    public J disallowAnnotationRemoval() {
+      builder.disallowAnnotationRemoval();
+      return self();
+    }
+
+    public J disallowMinification() {
+      builder.disallowMinification();
       return self();
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index 49c4a1e..948caf2 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -388,6 +388,12 @@
                   // TODO(b/189807246): This should be removed.
                   modifiers.setAllowsOptimization(true);
                   modifiers.setAllowsObfuscation(isObfuscating());
+
+                  // In non-compatibility mode, adding -dontoptimize does not cause all annotations
+                  // to be retained.
+                  if (!forceProguardCompatibility && isShrinking()) {
+                    modifiers.setAllowsAnnotationRemoval(true);
+                  }
                 });
         addRule(rule);
         this.keepAllRule = rule;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index ff71812..3072667 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -1024,6 +1024,10 @@
             builder.getModifiersBuilder().setAllowsObfuscation(true);
           } else if (acceptString("accessmodification")) {
             builder.getModifiersBuilder().setAllowsAccessModification(true);
+          } else if (allowTestOptions) {
+            if (acceptString("annotationremoval")) {
+              builder.getModifiersBuilder().setAllowsAnnotationRemoval(true);
+            }
           }
         } else if (acceptString("includedescriptorclasses")) {
           builder.getModifiersBuilder().setIncludeDescriptorClasses(true);
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
index a02117d..63c0787 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRuleModifiers.java
@@ -7,6 +7,7 @@
   public static class Builder {
 
     private boolean allowsAccessModification = false;
+    private boolean allowsAnnotationRemoval = false;
     private boolean allowsShrinking = false;
     private boolean allowsOptimization = false;
     private boolean allowsObfuscation = false;
@@ -19,6 +20,11 @@
       return this;
     }
 
+    public Builder setAllowsAnnotationRemoval(boolean allowsAnnotationRemoval) {
+      this.allowsAnnotationRemoval = allowsAnnotationRemoval;
+      return this;
+    }
+
     public void setAllowsShrinking(boolean allowsShrinking) {
       this.allowsShrinking = allowsShrinking;
     }
@@ -39,6 +45,7 @@
     ProguardKeepRuleModifiers build() {
       return new ProguardKeepRuleModifiers(
           allowsAccessModification,
+          allowsAnnotationRemoval,
           allowsShrinking,
           allowsOptimization,
           allowsObfuscation,
@@ -47,6 +54,7 @@
   }
 
   public final boolean allowsAccessModification;
+  public final boolean allowsAnnotationRemoval;
   public final boolean allowsShrinking;
   public final boolean allowsOptimization;
   public final boolean allowsObfuscation;
@@ -54,11 +62,13 @@
 
   private ProguardKeepRuleModifiers(
       boolean allowsAccessModification,
+      boolean allowsAnnotationRemoval,
       boolean allowsShrinking,
       boolean allowsOptimization,
       boolean allowsObfuscation,
       boolean includeDescriptorClasses) {
     this.allowsAccessModification = allowsAccessModification;
+    this.allowsAnnotationRemoval = allowsAnnotationRemoval;
     this.allowsShrinking = allowsShrinking;
     this.allowsOptimization = allowsOptimization;
     this.allowsObfuscation = allowsObfuscation;
@@ -79,6 +89,7 @@
     }
     ProguardKeepRuleModifiers that = (ProguardKeepRuleModifiers) o;
     return allowsAccessModification == that.allowsAccessModification
+        && allowsAnnotationRemoval == that.allowsAnnotationRemoval
         && allowsShrinking == that.allowsShrinking
         && allowsOptimization == that.allowsOptimization
         && allowsObfuscation == that.allowsObfuscation
@@ -88,16 +99,18 @@
   @Override
   public int hashCode() {
     return (allowsAccessModification ? 1 : 0)
-        | (allowsShrinking ? 2 : 0)
-        | (allowsOptimization ? 4 : 0)
-        | (allowsObfuscation ? 8 : 0)
-        | (includeDescriptorClasses ? 16 : 0);
+        | (allowsAnnotationRemoval ? 2 : 0)
+        | (allowsShrinking ? 4 : 0)
+        | (allowsOptimization ? 8 : 0)
+        | (allowsObfuscation ? 16 : 0)
+        | (includeDescriptorClasses ? 32 : 0);
   }
 
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
     appendWithComma(builder, allowsAccessModification, "allowaccessmodification");
+    appendWithComma(builder, allowsAnnotationRemoval, "allowannotationremoval");
     appendWithComma(builder, allowsObfuscation, "allowobfuscation");
     appendWithComma(builder, allowsShrinking, "allowshrinking");
     appendWithComma(builder, allowsOptimization, "allowoptimization");
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index e4890a0..03ed0ad 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -91,6 +91,7 @@
     private final Iterable<? extends ProguardConfigurationRule> rules;
     private final MutableItemsWithRules noShrinking = new MutableItemsWithRules();
     private final MutableItemsWithRules softPinned = new MutableItemsWithRules();
+    private final Set<DexReference> noAnnotationRemoval = Sets.newIdentityHashSet();
     private final Set<DexReference> noObfuscation = Sets.newIdentityHashSet();
     private final LinkedHashMap<DexReference, DexReference> reasonAsked = new LinkedHashMap<>();
     private final LinkedHashMap<DexReference, DexReference> checkDiscarded = new LinkedHashMap<>();
@@ -346,10 +347,12 @@
       assert Sets.intersection(neverInline, alwaysInline).isEmpty()
               && Sets.intersection(neverInline, forceInline).isEmpty()
           : "A method cannot be marked as both -neverinline and -forceinline/-alwaysinline.";
+      assert appView.options().isAnnotationRemovalEnabled() || noAnnotationRemoval.isEmpty();
       assert appView.options().isMinificationEnabled() || noObfuscation.isEmpty();
       return new RootSet(
           noShrinking,
           softPinned,
+          noAnnotationRemoval,
           noObfuscation,
           ImmutableList.copyOf(reasonAsked.values()),
           ImmutableList.copyOf(checkDiscarded.values()),
@@ -447,6 +450,7 @@
           neverClassInline,
           noShrinking,
           softPinned,
+          noAnnotationRemoval,
           noObfuscation,
           dependentNoShrinking,
           dependentSoftPinned,
@@ -1188,6 +1192,10 @@
           context.markAsUsed();
         }
 
+        if (appView.options().isAnnotationRemovalEnabled() && !modifiers.allowsAnnotationRemoval) {
+          noAnnotationRemoval.add(item.getReference());
+          context.markAsUsed();
+        }
         if (appView.options().isMinificationEnabled() && !modifiers.allowsObfuscation) {
           noObfuscation.add(item.getReference());
           context.markAsUsed();
@@ -1410,6 +1418,7 @@
     final Set<DexType> neverClassInline;
     final MutableItemsWithRules noShrinking;
     final MutableItemsWithRules softPinned;
+    final Set<DexReference> noAnnotationRemoval;
     final Set<DexReference> noObfuscation;
     final Map<DexReference, MutableItemsWithRules> dependentNoShrinking;
     final Map<DexReference, MutableItemsWithRules> dependentSoftPinned;
@@ -1422,6 +1431,7 @@
         Set<DexType> neverClassInline,
         MutableItemsWithRules noShrinking,
         MutableItemsWithRules softPinned,
+        Set<DexReference> noAnnotationRemoval,
         Set<DexReference> noObfuscation,
         Map<DexReference, MutableItemsWithRules> dependentNoShrinking,
         Map<DexReference, MutableItemsWithRules> dependentSoftPinned,
@@ -1432,6 +1442,7 @@
       this.neverClassInline = neverClassInline;
       this.noShrinking = noShrinking;
       this.softPinned = softPinned;
+      this.noAnnotationRemoval = noAnnotationRemoval;
       this.noObfuscation = noObfuscation;
       this.dependentNoShrinking = dependentNoShrinking;
       this.dependentSoftPinned = dependentSoftPinned;
@@ -1835,6 +1846,7 @@
     private RootSet(
         MutableItemsWithRules noShrinking,
         MutableItemsWithRules softPinned,
+        Set<DexReference> noAnnotationRemoval,
         Set<DexReference> noObfuscation,
         ImmutableList<DexReference> reasonAsked,
         ImmutableList<DexReference> checkDiscarded,
@@ -1869,6 +1881,7 @@
           neverClassInline,
           noShrinking,
           softPinned,
+          noAnnotationRemoval,
           noObfuscation,
           dependentNoShrinking,
           dependentSoftPinned,
@@ -1916,6 +1929,7 @@
       neverInline.addAll(consequentRootSet.neverInline);
       neverInlineDueToSingleCaller.addAll(consequentRootSet.neverInlineDueToSingleCaller);
       neverClassInline.addAll(consequentRootSet.neverClassInline);
+      noAnnotationRemoval.addAll(consequentRootSet.noAnnotationRemoval);
       noObfuscation.addAll(consequentRootSet.noObfuscation);
       if (addNoShrinking) {
         noShrinking.addAll(consequentRootSet.noShrinking);
@@ -1945,6 +1959,9 @@
       if (noShrinking.containsReference(original)) {
         noShrinking.putReferenceWithRules(rewritten, noShrinking.getRulesForReference(original));
       }
+      if (noAnnotationRemoval.contains(original)) {
+        noAnnotationRemoval.add(rewritten);
+      }
       if (noObfuscation.contains(original)) {
         noObfuscation.add(rewritten);
       }
@@ -1962,6 +1979,7 @@
 
     public void prune(DexReference reference) {
       noShrinking.removeReference(reference);
+      noAnnotationRemoval.remove(reference);
       noObfuscation.remove(reference);
       noSideEffects.remove(reference);
       assumedValues.remove(reference);
@@ -2154,6 +2172,7 @@
       StringBuilder builder = new StringBuilder();
       builder.append("RootSet");
       builder.append("\nnoShrinking: " + noShrinking.size());
+      builder.append("\nnoAnnotationRemoval: " + noAnnotationRemoval.size());
       builder.append("\nnoObfuscation: " + noObfuscation.size());
       builder.append("\nreasonAsked: " + reasonAsked.size());
       builder.append("\ncheckDiscarded: " + checkDiscarded.size());
@@ -2210,6 +2229,7 @@
         Set<DexType> neverClassInline,
         MutableItemsWithRules noShrinking,
         MutableItemsWithRules softPinned,
+        Set<DexReference> noAnnotationRemoval,
         Set<DexReference> noObfuscation,
         Map<DexReference, MutableItemsWithRules> dependentNoShrinking,
         Map<DexReference, MutableItemsWithRules> dependentSoftPinned,
@@ -2221,6 +2241,7 @@
           neverClassInline,
           noShrinking,
           softPinned,
+          noAnnotationRemoval,
           noObfuscation,
           dependentNoShrinking,
           dependentSoftPinned,
@@ -2277,6 +2298,7 @@
           noShrinking,
           new MutableItemsWithRules(),
           Collections.emptySet(),
+          Collections.emptySet(),
           reasonAsked,
           checkDiscarded,
           Collections.emptySet(),
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 9bb81bd..af2b4ad 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -587,6 +587,11 @@
   }
 
   @Override
+  public boolean isAnnotationRemovalEnabled() {
+    return !isForceProguardCompatibilityEnabled();
+  }
+
+  @Override
   public boolean isTreeShakingEnabled() {
     return isShrinking();
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java
index f198845..e6e8b69 100644
--- a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java
@@ -64,8 +64,6 @@
     if (shrinker.isR8()) {
       run(
           testForR8(parameters.getBackend())
-              // Allowing all of shrinking, optimization and obfuscation will amount to a nop rule.
-              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation)
               .enableNoHorizontalClassMergingAnnotations());
     } else {
       run(
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassFieldsAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassFieldsAllowShrinkingCompatibilityTest.java
index 8b01419..314ea33 100644
--- a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassFieldsAllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassFieldsAllowShrinkingCompatibilityTest.java
@@ -63,10 +63,7 @@
   @Test
   public void test() throws Exception {
     if (shrinker.isR8()) {
-      run(
-          testForR8(parameters.getBackend())
-              // Allowing all of shrinking, optimization and obfuscation will amount to a nop rule.
-              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation));
+      run(testForR8(parameters.getBackend()));
     } else {
       run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
     }
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java
index 5a1d0f9..5305ced 100644
--- a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java
@@ -64,8 +64,6 @@
     if (shrinker.isR8()) {
       run(
           testForR8(parameters.getBackend())
-              // Allowing all of shrinking, optimization and obfuscation will amount to a nop rule.
-              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation)
               .enableNoHorizontalClassMergingAnnotations());
     } else {
       run(
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/KeepAllowAnnotationRemovalTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/KeepAllowAnnotationRemovalTest.java
new file mode 100644
index 0000000..a47afde
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/KeepAllowAnnotationRemovalTest.java
@@ -0,0 +1,71 @@
+// Copyright (c) 2021, 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.shaking.annotations;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KeepAllowAnnotationRemovalTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableCompatibilityMode;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, compat: {0}")
+  public static List<Object[]> parameters() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8Compat(parameters.getBackend(), enableCompatibilityMode)
+        .addProgramClasses(Main.class, Keep.class)
+        .addKeepRules(
+            "-keep,allowannotationremoval @" + Keep.class.getTypeName() + " class * {",
+            "  public static void main(java.lang.String[]);",
+            "}")
+        .addKeepRuntimeInvisibleAnnotations()
+        .enableProguardTestOptions()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject = inspector.clazz(Main.class);
+              assertThat(classSubject, isPresent());
+              assertThat(
+                  classSubject.annotation(Keep.class),
+                  onlyIf(enableCompatibilityMode, isPresent()));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .apply(
+            result ->
+                result.assertSuccessWithOutputLines(
+                    result.inspector().clazz(Keep.class).getFinalName()));
+  }
+
+  @Keep
+  static class Main {
+    public static void main(String[] args) {
+      System.out.println(Keep.class.getName());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/KeepDisallowAnnotationRemovalAllowOptimizationTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/KeepDisallowAnnotationRemovalAllowOptimizationTest.java
new file mode 100644
index 0000000..8fa7bc1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/KeepDisallowAnnotationRemovalAllowOptimizationTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2021, 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.shaking.annotations;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.containsThrow;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KeepDisallowAnnotationRemovalAllowOptimizationTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableCompatibilityMode;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, compat: {0}")
+  public static List<Object[]> parameters() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8Compat(parameters.getBackend(), enableCompatibilityMode)
+        .addProgramClasses(Main.class)
+        .addKeepMainRule(Main.class)
+        .addKeepClassAndMembersRules(NeverInline.class)
+        .addKeepRules(
+            "-keepclassmembers,allowobfuscation,allowoptimization,allowshrinking class * {",
+            "  static java.lang.Object getNonNull();",
+            "}")
+        .addKeepRuntimeInvisibleAnnotations()
+        // In compatibility mode the rule above is a no-op.
+        .allowUnusedProguardConfigurationRules(enableCompatibilityMode)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject = inspector.clazz(Main.class);
+              assertThat(classSubject, isPresent());
+
+              // The annotation on getNonNull() is kept meanwhile it is subject to other
+              // optimizations.
+              MethodSubject getNonNullSubject = classSubject.uniqueMethodWithName("getNonNull");
+              assertThat(getNonNullSubject, isPresentAndRenamed());
+              assertThat(getNonNullSubject.annotation(NeverInline.class), isPresent());
+
+              // Check that the code has been optimized using the fact that getNonNull() returns a
+              // non-null value.
+              assertThat(classSubject.uniqueMethodWithName("dead"), isAbsent());
+              assertThat(classSubject.mainMethod(), not(containsThrow()));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("getNonNull()");
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      Object o = getNonNull();
+      if (o == null) {
+        dead();
+      }
+    }
+
+    @NeverInline
+    static Object getNonNull() {
+      System.out.println("getNonNull()");
+      return new Object();
+    }
+
+    @NeverInline
+    static void dead() {
+      throw new RuntimeException();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/SerializedNameAlternateTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/SerializedNameAlternateTest.java
index 6913147..de259e5 100644
--- a/src/test/java/com/android/tools/r8/shaking/annotations/SerializedNameAlternateTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/SerializedNameAlternateTest.java
@@ -66,7 +66,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public SerializedNameAlternateTest(TestParameters parameters) {
@@ -83,7 +83,14 @@
             .addKeepMainRule(Main.class)
             .addKeepClassRules(Foo.class)
             .addKeepClassRules(SerializedName.class)
-            .setMinApi(parameters.getRuntime())
+            .addKeepRules(
+                // Non-compat mode only retains annotations for items matched by a -keep rule.
+                "-keepclassmembers,allowobfuscation,allowshrinking class "
+                    + Foo.class.getTypeName()
+                    + " {",
+                "  @" + SerializedName.class.getTypeName() + " <fields>;",
+                "}")
+            .setMinApi(parameters.getApiLevel())
             .noMinification()
             .run(parameters.getRuntime(), Main.class);
     checkRunResult(result);
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassOrMemberSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassOrMemberSubject.java
index d86cc6c..bca691f 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassOrMemberSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassOrMemberSubject.java
@@ -5,9 +5,16 @@
 package com.android.tools.r8.utils.codeinspector;
 
 import com.android.tools.r8.graph.AccessFlags;
+import java.lang.annotation.Annotation;
 
 public abstract class ClassOrMemberSubject extends Subject {
 
+  public abstract AnnotationSubject annotation(String name);
+
+  public final AnnotationSubject annotation(Class<? extends Annotation> clazz) {
+    return annotation(clazz.getTypeName());
+  }
+
   public abstract AccessFlags<?> getAccessFlags();
 
   public abstract String getOriginalName();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
index 452669a..4f8ecd3 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
@@ -191,8 +191,6 @@
 
   public abstract DexProgramClass getDexProgramClass();
 
-  public abstract AnnotationSubject annotation(String name);
-
   @Override
   public abstract String getOriginalName();
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index 10a68c1..c5a8cde 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -44,6 +44,27 @@
     };
   }
 
+  public static Matcher<MethodSubject> containsThrow() {
+    return new TypeSafeMatcher<MethodSubject>() {
+      @Override
+      protected boolean matchesSafely(MethodSubject subject) {
+        return subject.isPresent()
+            && subject.getMethod().hasCode()
+            && subject.streamInstructions().anyMatch(InstructionSubject::isThrow);
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("contains throw");
+      }
+
+      @Override
+      public void describeMismatchSafely(final MethodSubject subject, Description description) {
+        description.appendText("method did not");
+      }
+    };
+  }
+
   public static Matcher<MethodSubject> instantiatesClass(Class<?> clazz) {
     return instantiatesClass(clazz.getTypeName());
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MemberSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MemberSubject.java
index d8fd4b9..bd9081e 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/MemberSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MemberSubject.java
@@ -38,8 +38,6 @@
 
   public abstract String getFinalSignatureAttribute();
 
-  public abstract AnnotationSubject annotation(String name);
-
   public FieldSubject asFieldSubject() {
     return null;
   }