Preliminary support for -whyareyounotobfuscating

Bug: b/450540158
Change-Id: Iee9a54cf4229a5908cc0da5814cfad1b63935e93
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 482d5a1..0ae1c43 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -37,6 +37,8 @@
 import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
 import com.android.tools.r8.ir.desugar.ServiceLoaderSourceCode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepInfo;
+import com.android.tools.r8.shaking.MinimumKeepInfoCollection;
 import com.android.tools.r8.utils.ConsumerUtils;
 import com.android.tools.r8.utils.DominatorChecker;
 import com.android.tools.r8.utils.ListUtils;
@@ -47,6 +49,7 @@
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
 
 /**
  * ServiceLoaderRewriter will attempt to rewrite calls on the form of: ServiceLoader.load(X.class,
@@ -107,9 +110,15 @@
   }
 
   private boolean shouldReportWhyAreYouNotInliningServiceLoaderLoad() {
-    AppInfoWithLiveness appInfo = appView().appInfo();
-    return appInfo.isWhyAreYouNotInliningMethod(serviceLoaderMethods.load)
-        || appInfo.isWhyAreYouNotInliningMethod(serviceLoaderMethods.loadWithClassLoader);
+    MinimumKeepInfoCollection keepInfo =
+        appView
+            .rootSet()
+            .getDependentMinimumKeepInfo()
+            .getUnconditionalMinimumKeepInfoOrDefault(MinimumKeepInfoCollection.empty());
+    Predicate<KeepInfo.Joiner<?, ?, ?>> test =
+        joiner -> joiner.asMethodJoiner().isWhyAreYouNotInliningEnabled();
+    return keepInfo.hasMinimumKeepInfoThatMatches(serviceLoaderMethods.load, test)
+        || keepInfo.hasMinimumKeepInfoThatMatches(serviceLoaderMethods.loadWithClassLoader, test);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
index e70e49e..eeb46c7 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.util.Set;
 
@@ -21,7 +22,7 @@
 
   public static WhyAreYouNotInliningReporter createFor(
       ProgramMethod callee, AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-    if (appView.appInfo().isWhyAreYouNotInliningMethod(callee.getReference())) {
+    if (appView.getKeepInfo(callee).isWhyAreYouNotInliningEnabled()) {
       return new WhyAreYouNotInliningReporterImpl(appView, callee, context);
     }
     return NopWhyAreYouNotInliningReporter.getInstance();
@@ -32,7 +33,9 @@
       InvokeMethod invoke,
       AppView<AppInfoWithLiveness> appView,
       ProgramMethod context) {
-    if (appView.appInfo().hasNoWhyAreYouNotInliningMethods()) {
+    InternalOptions options = appView.options();
+    if (!options.hasProguardConfiguration()
+        || !options.getProguardConfiguration().hasWhyAreYouNotInliningRule()) {
       return;
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 25f2a95..cb5e4b8 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -147,8 +147,6 @@
   public final Map<DexReference, ProguardMemberRule> mayHaveSideEffects;
   /** All methods that should be inlined if possible due to a configuration directive. */
   private final Set<DexMethod> alwaysInline;
-  /** Items for which to print inlining decisions for (testing only). */
-  private final Set<DexMethod> whyAreYouNotInlining;
   /** All methods that must be reprocessed (testing only). */
   private final Set<DexMethod> reprocess;
   /** All types that should be inlined if possible due to a configuration directive. */
@@ -210,7 +208,6 @@
       KeepInfoCollection keepInfo,
       Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
       Set<DexMethod> alwaysInline,
-      Set<DexMethod> whyAreYouNotInlining,
       Set<DexMethod> reprocess,
       PredicateSet<DexType> alwaysClassInline,
       IdentifierNameStringCollection identifierNameStrings,
@@ -236,7 +233,6 @@
     this.mayHaveSideEffects = mayHaveSideEffects;
     this.callSites = callSites;
     this.alwaysInline = alwaysInline;
-    this.whyAreYouNotInlining = whyAreYouNotInlining;
     this.reprocess = reprocess;
     this.alwaysClassInline = alwaysClassInline;
     this.identifierNameStrings = identifierNameStrings;
@@ -270,7 +266,6 @@
         previous.keepInfo,
         previous.mayHaveSideEffects,
         previous.alwaysInline,
-        previous.whyAreYouNotInlining,
         previous.reprocess,
         previous.alwaysClassInline,
         previous.identifierNameStrings,
@@ -305,7 +300,6 @@
         extendPinnedItems(previous, prunedItems.getAdditionalPinnedItems()),
         previous.mayHaveSideEffects,
         pruneMethods(previous.alwaysInline, prunedItems, tasks),
-        pruneMethods(previous.whyAreYouNotInlining, prunedItems, tasks),
         pruneMethods(previous.reprocess, prunedItems, tasks),
         previous.alwaysClassInline,
         previous.identifierNameStrings.prune(prunedItems, tasks),
@@ -431,7 +425,6 @@
         keepInfo,
         mayHaveSideEffects,
         alwaysInline,
-        whyAreYouNotInlining,
         reprocess,
         alwaysClassInline,
         identifierNameStrings,
@@ -502,7 +495,6 @@
     this.mayHaveSideEffects = previous.mayHaveSideEffects;
     this.callSites = previous.callSites;
     this.alwaysInline = previous.alwaysInline;
-    this.whyAreYouNotInlining = previous.whyAreYouNotInlining;
     this.reprocess = previous.reprocess;
     this.alwaysClassInline = previous.alwaysClassInline;
     this.identifierNameStrings = previous.identifierNameStrings;
@@ -638,14 +630,6 @@
     return alwaysInline.contains(method);
   }
 
-  public boolean isWhyAreYouNotInliningMethod(DexMethod method) {
-    return whyAreYouNotInlining.contains(method);
-  }
-
-  public boolean hasNoWhyAreYouNotInliningMethods() {
-    return whyAreYouNotInlining.isEmpty();
-  }
-
   public Set<DexMethod> getReprocessMethods() {
     return reprocess;
   }
@@ -999,7 +983,6 @@
         // Take any rule in case of collisions.
         lens.rewriteReferenceKeys(mayHaveSideEffects, (reference, rules) -> ListUtils.first(rules)),
         lens.rewriteReferences(alwaysInline),
-        lens.rewriteReferences(whyAreYouNotInlining),
         lens.rewriteReferences(reprocess),
         alwaysClassInline.rewriteItems(lens::lookupType),
         identifierNameStrings.rewrittenWithLens(lens),
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 e5cb200..4482352 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -506,7 +506,7 @@
     this.options = options;
     this.taskCollection =
         new EnqueuerTaskCollection(this, options.getThreadingModule(), executorService);
-    this.keepInfo = new MutableKeepInfoCollection(options);
+    this.keepInfo = new MutableKeepInfoCollection(appView, this);
     this.reflectiveIdentification = new EnqueuerReflectiveIdentification(appView, this);
     this.useRegistryFactory = createUseRegistryFactory();
     this.worklist = EnqueuerWorklist.createWorklist(this, options.getThreadingModule());
@@ -4746,7 +4746,6 @@
             getKeepInfo(),
             rootSet.mayHaveSideEffects,
             amendWithCompanionMethods(rootSet.alwaysInline),
-            amendWithCompanionMethods(rootSet.whyAreYouNotInlining),
             amendWithCompanionMethods(rootSet.reprocess),
             rootSet.alwaysClassInline,
             new IdentifierNameStringCollection(
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 3eca5de..4c4e47b 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
@@ -31,6 +31,7 @@
   private final boolean allowShrinking;
   private final boolean allowSignatureRemoval;
   private final boolean checkDiscarded;
+  private final boolean whyAreYouNotObfuscating;
   private final KeepAnnotationCollectionInfo annotationsInfo;
   private final KeepAnnotationCollectionInfo typeAnnotationsInfo;
 
@@ -42,6 +43,7 @@
       boolean allowShrinking,
       boolean allowSignatureRemoval,
       boolean checkDiscarded,
+      boolean whyAreYouNotObfuscating,
       KeepAnnotationCollectionInfo annotationsInfo,
       KeepAnnotationCollectionInfo typeAnnotationsInfo) {
     this.allowAccessModification = allowAccessModification;
@@ -51,6 +53,7 @@
     this.allowShrinking = allowShrinking;
     this.allowSignatureRemoval = allowSignatureRemoval;
     this.checkDiscarded = checkDiscarded;
+    this.whyAreYouNotObfuscating = whyAreYouNotObfuscating;
     this.annotationsInfo = annotationsInfo;
     this.typeAnnotationsInfo = typeAnnotationsInfo;
   }
@@ -64,6 +67,7 @@
         builder.isShrinkingAllowed(),
         builder.isSignatureRemovalAllowed(),
         builder.isCheckDiscardedEnabled(),
+        builder.isWhyAreYouNotObfuscatingEnabled(),
         builder.getAnnotationsInfo().build(),
         builder.getTypeAnnotationsInfo().build());
   }
@@ -150,6 +154,14 @@
     return checkDiscarded;
   }
 
+  public boolean isWhyAreYouNotObfuscatingEnabled() {
+    return internalIsWhyAreYouNotObfuscatingEnabled();
+  }
+
+  boolean internalIsWhyAreYouNotObfuscatingEnabled() {
+    return whyAreYouNotObfuscating;
+  }
+
   /**
    * True if an item must be present in the output.
    *
@@ -299,6 +311,7 @@
         && (allowShrinking || !other.internalIsShrinkingAllowed())
         && (allowSignatureRemoval || !other.internalIsSignatureRemovalAllowed())
         && (!checkDiscarded || other.internalIsCheckDiscardedEnabled())
+        && (!whyAreYouNotObfuscating || other.internalIsWhyAreYouNotObfuscatingEnabled())
         && annotationsInfo.isLessThanOrEqualTo(other.internalAnnotationsInfo())
         && typeAnnotationsInfo.isLessThanOrEqualTo(other.internalTypeAnnotationsInfo());
   }
@@ -312,7 +325,8 @@
         && allowOptimization == other.internalIsOptimizationAllowed()
         && allowShrinking == other.internalIsShrinkingAllowed()
         && allowSignatureRemoval == other.internalIsSignatureRemovalAllowed()
-        && checkDiscarded == other.internalIsCheckDiscardedEnabled();
+        && checkDiscarded == other.internalIsCheckDiscardedEnabled()
+        && whyAreYouNotObfuscating == other.internalIsWhyAreYouNotObfuscatingEnabled();
   }
 
   public boolean equalsWithAnnotations(K other) {
@@ -341,6 +355,7 @@
     hash += bit(allowShrinking, index++);
     hash += bit(allowSignatureRemoval, index++);
     hash += bit(checkDiscarded, index++);
+    hash += bit(whyAreYouNotObfuscating, index++);
     hash += bit(annotationsInfo.isTop(), index++);
     hash += bit(typeAnnotationsInfo.isTop(), index);
     return hash;
@@ -369,6 +384,9 @@
       case "checkDiscarded":
         builder.setCheckDiscarded(Boolean.parseBoolean(value));
         return true;
+      case "whyAreYouNotObfuscating":
+        builder.setWhyAreYouNotObfuscating(Boolean.parseBoolean(value));
+        return true;
       case "annotationsInfo":
         builder.setAnnotationInfo(KeepAnnotationCollectionInfoExported.parse(value));
         return true;
@@ -389,6 +407,7 @@
     lines.add("allowShrinking: " + allowShrinking);
     lines.add("allowSignatureRemoval: " + allowSignatureRemoval);
     lines.add("checkDiscarded: " + checkDiscarded);
+    lines.add("whyAreYouNotObfuscating: " + whyAreYouNotObfuscating);
     lines.add("annotationsInfo: " + annotationsInfo);
     lines.add("typeAnnotationsInfo: " + typeAnnotationsInfo);
     return lines;
@@ -423,6 +442,7 @@
     private boolean allowShrinking;
     private boolean allowSignatureRemoval;
     private boolean checkDiscarded;
+    private boolean whyAreYouNotObfuscating;
     private KeepAnnotationCollectionInfo.Builder annotationsInfo;
     private KeepAnnotationCollectionInfo.Builder typeAnnotationsInfo;
 
@@ -439,6 +459,7 @@
       allowShrinking = original.internalIsShrinkingAllowed();
       allowSignatureRemoval = original.internalIsSignatureRemovalAllowed();
       checkDiscarded = original.internalIsCheckDiscardedEnabled();
+      whyAreYouNotObfuscating = original.internalIsWhyAreYouNotObfuscatingEnabled();
       annotationsInfo = original.internalAnnotationsInfo().toBuilder();
       typeAnnotationsInfo = original.internalTypeAnnotationsInfo().toBuilder();
     }
@@ -453,6 +474,7 @@
       setAllowShrinking(false);
       setAllowSignatureRemoval(false);
       setCheckDiscarded(false);
+      setWhyAreYouNotObfuscating(false);
       return self();
     }
 
@@ -466,6 +488,7 @@
       setAllowShrinking(true);
       setAllowSignatureRemoval(true);
       setCheckDiscarded(false);
+      setWhyAreYouNotObfuscating(false);
       return self();
     }
 
@@ -493,6 +516,7 @@
           && isShrinkingAllowed() == other.internalIsShrinkingAllowed()
           && isSignatureRemovalAllowed() == other.internalIsSignatureRemovalAllowed()
           && isCheckDiscardedEnabled() == other.internalIsCheckDiscardedEnabled()
+          && isWhyAreYouNotObfuscatingEnabled() == other.internalIsWhyAreYouNotObfuscatingEnabled()
           && annotationsInfo.isEqualTo(other.internalAnnotationsInfo())
           && typeAnnotationsInfo.isEqualTo(other.internalTypeAnnotationsInfo());
     }
@@ -506,6 +530,15 @@
       return self();
     }
 
+    public boolean isWhyAreYouNotObfuscatingEnabled() {
+      return whyAreYouNotObfuscating;
+    }
+
+    public B setWhyAreYouNotObfuscating(boolean whyAreYouNotObfuscating) {
+      this.whyAreYouNotObfuscating = whyAreYouNotObfuscating;
+      return self();
+    }
+
     public boolean isMinificationAllowed() {
       return allowMinification;
     }
@@ -654,6 +687,10 @@
       return builder.isCheckDiscardedEnabled();
     }
 
+    public boolean isWhyAreYouNotObfuscatingEnabled() {
+      return builder.isWhyAreYouNotObfuscatingEnabled();
+    }
+
     public boolean isMinificationAllowed() {
       return builder.isMinificationAllowed();
     }
@@ -735,6 +772,11 @@
       return self();
     }
 
+    public J setWhyAreYouNotObfuscating() {
+      builder.setWhyAreYouNotObfuscating(true);
+      return self();
+    }
+
     public J merge(J joiner) {
       Builder<B, K> otherBuilder = joiner.builder;
       applyIf(!otherBuilder.isAccessModificationAllowed(), Joiner::disallowAccessModification);
@@ -746,6 +788,7 @@
       applyIf(!otherBuilder.isShrinkingAllowed(), Joiner::disallowShrinking);
       applyIf(!otherBuilder.isSignatureRemovalAllowed(), Joiner::disallowSignatureRemoval);
       applyIf(otherBuilder.isCheckDiscardedEnabled(), Joiner::setCheckDiscarded);
+      applyIf(otherBuilder.isWhyAreYouNotObfuscatingEnabled(), Joiner::setWhyAreYouNotObfuscating);
       builder.getAnnotationsInfo().destructiveJoin(otherBuilder.getAnnotationsInfo());
       builder.getTypeAnnotationsInfo().destructiveJoin(otherBuilder.getTypeAnnotationsInfo());
       reasons.addAll(joiner.reasons);
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index 11ebdfb..574dc74 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -284,7 +284,9 @@
   // Mutation interface for building up the keep info.
   public static class MutableKeepInfoCollection extends KeepInfoCollection {
 
+    private final AppView<?> appView;
     private final DexItemFactory factory;
+    private final Enqueuer.Mode mode;
 
     // These are typed at signatures but the interface should make sure never to allow access
     // directly with a signature. See the comment in KeepInfoCollection.
@@ -304,9 +306,10 @@
 
     private final KeepInfoCanonicalizer canonicalizer;
 
-    MutableKeepInfoCollection(InternalOptions options) {
+    MutableKeepInfoCollection(AppView<?> appView, Enqueuer enqueuer) {
       this(
-          options,
+          appView,
+          enqueuer.getMode(),
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
@@ -314,13 +317,14 @@
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
           MaterializedRules.empty(),
-          options.testing.enableKeepInfoCanonicalizer
+          appView.testing().enableKeepInfoCanonicalizer
               ? KeepInfoCanonicalizer.newCanonicalizer()
               : KeepInfoCanonicalizer.newNopCanonicalizer());
     }
 
     private MutableKeepInfoCollection(
-        InternalOptions options,
+        AppView<?> appView,
+        Enqueuer.Mode mode,
         Map<DexType, KeepClassInfo> keepClassInfo,
         Map<DexMethod, KeepMethodInfo> keepMethodInfo,
         Map<DexField, KeepFieldInfo> keepFieldInfo,
@@ -329,7 +333,9 @@
         Map<DexMethod, KeepMethodInfo.Joiner> methodRuleInstances,
         MaterializedRules materializedRules,
         KeepInfoCanonicalizer keepInfoCanonicalizer) {
-      this.factory = options.dexItemFactory();
+      this.appView = appView;
+      this.factory = appView.dexItemFactory();
+      this.mode = mode;
       this.keepClassInfo = keepClassInfo;
       this.keepMethodInfo = keepMethodInfo;
       this.keepFieldInfo = keepFieldInfo;
@@ -388,7 +394,8 @@
       Map<DexField, KeepFieldInfo> newFieldInfo = rewriteFieldInfo(lens, options, timing);
       MutableKeepInfoCollection result =
           new MutableKeepInfoCollection(
-              options,
+              appView,
+              mode,
               newClassInfo,
               newMethodInfo,
               newFieldInfo,
@@ -610,10 +617,12 @@
       KeepClassInfo.Joiner joiner = info.joiner();
       fn.accept(joiner);
       KeepClassInfo joined = joiner.join();
-      if (!info.equals(joined)) {
-        keepClassInfo.put(clazz.type, canonicalizer.canonicalizeKeepClassInfo(joined));
-        maybeDisallowKotlinMetadataRemoval(clazz, info, joined, joiner);
+      if (info.equals(joined)) {
+        return;
       }
+      keepClassInfo.put(clazz.type, canonicalizer.canonicalizeKeepClassInfo(joined));
+      maybeDisallowKotlinMetadataRemoval(clazz, info, joined, joiner);
+      reportWhyAreYouNotObfuscating(clazz, info, joined, joiner);
     }
 
     private void maybeDisallowKotlinMetadataRemoval(
@@ -639,6 +648,54 @@
       allowKotlinMetadataRemoval = false;
     }
 
+    private void reportWhyAreYouNotObfuscating(
+        ProgramDefinition definition,
+        KeepInfo<?, ?> previousKeepInfo,
+        KeepInfo<?, ?> joinedKeepInfo,
+        KeepInfo.Joiner<?, ?, ?> joiner) {
+      InternalOptions options = appView.options();
+      if (!mode.isFinalTreeShaking()
+          || options.getProguardConfiguration() == null
+          || !options.getProguardConfiguration().hasWhyAreYouNotObfuscatingRule()
+          || !previousKeepInfo.internalIsMinificationAllowed()
+          || joiner.isMinificationAllowed()) {
+        return;
+      }
+      MinimumKeepInfoCollection minimumKeepInfoCollection =
+          appView
+              .rootSet()
+              .getDependentMinimumKeepInfo()
+              .getUnconditionalMinimumKeepInfoOrDefault(MinimumKeepInfoCollection.empty());
+      if (!minimumKeepInfoCollection.hasMinimumKeepInfoThatMatches(
+          definition.getReference(), KeepInfo.Joiner::isWhyAreYouNotObfuscatingEnabled)) {
+        return;
+      }
+      assert !joinedKeepInfo.internalIsMinificationAllowed();
+      boolean foundRule = false;
+      for (ProguardKeepRuleBase rule : joiner.getRules()) {
+        if (!rule.getModifiers().allowsObfuscation) {
+          appView
+              .reporter()
+              .warning(
+                  definition.getReference().toSourceString() + " is not obfuscated due to " + rule);
+          foundRule = true;
+        }
+      }
+      if (!foundRule) {
+        boolean foundReason = false;
+        for (KeepReason reason : joiner.getReasons()) {
+          appView
+              .reporter()
+              .warning(
+                  definition.getReference().toSourceString()
+                      + " is not obfuscated due to "
+                      + reason);
+          foundReason = true;
+        }
+        assert foundReason;
+      }
+    }
+
     public void keepClass(DexProgramClass clazz) {
       joinClass(clazz, KeepInfo.Joiner::top);
     }
@@ -652,9 +709,11 @@
       KeepMethodInfo.Joiner joiner = info.joiner();
       fn.accept(joiner);
       KeepMethodInfo joined = joiner.join();
-      if (!info.equals(joined)) {
-        keepMethodInfo.put(method.getReference(), canonicalizer.canonicalizeKeepMethodInfo(joined));
+      if (info.equals(joined)) {
+        return;
       }
+      keepMethodInfo.put(method.getReference(), canonicalizer.canonicalizeKeepMethodInfo(joined));
+      reportWhyAreYouNotObfuscating(method, info, joined, joiner);
     }
 
     public void keepMethod(ProgramMethod method) {
@@ -670,9 +729,11 @@
       Joiner joiner = info.joiner();
       fn.accept(joiner);
       KeepFieldInfo joined = joiner.join();
-      if (!info.equals(joined)) {
-        keepFieldInfo.put(field.getReference(), canonicalizer.canonicalizeKeepFieldInfo(joined));
+      if (info.equals(joined)) {
+        return;
       }
+      keepFieldInfo.put(field.getReference(), canonicalizer.canonicalizeKeepFieldInfo(joined));
+      reportWhyAreYouNotObfuscating(field, info, joined, joiner);
     }
 
     public void keepField(ProgramField field) {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
index 597b016..a9879ad 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepMethodInfo.java
@@ -48,6 +48,7 @@
   private final boolean allowUnusedArgumentOptimization;
   private final boolean allowUnusedReturnValueOptimization;
   private final boolean allowParameterNamesRemoval;
+  private final boolean whyAreYouNotInlining;
   private final KeepAnnotationCollectionInfo parameterAnnotationsInfo;
 
   protected KeepMethodInfo(Builder builder) {
@@ -68,6 +69,7 @@
     this.allowUnusedArgumentOptimization = builder.isUnusedArgumentOptimizationAllowed();
     this.allowUnusedReturnValueOptimization = builder.isUnusedReturnValueOptimizationAllowed();
     this.allowParameterNamesRemoval = builder.isParameterNamesRemovalAllowed();
+    this.whyAreYouNotInlining = builder.isWhyAreYouNotInliningEnabled();
     this.parameterAnnotationsInfo = builder.getParameterAnnotationsInfo().build();
   }
 
@@ -293,6 +295,14 @@
     return allowParameterNamesRemoval;
   }
 
+  public boolean isWhyAreYouNotInliningEnabled() {
+    return internalIsWhyAreYouNotInliningEnabled();
+  }
+
+  boolean internalIsWhyAreYouNotInliningEnabled() {
+    return whyAreYouNotInlining;
+  }
+
   public Joiner joiner() {
     assert !isTop();
     return new Joiner(this);
@@ -326,7 +336,8 @@
         && allowUnusedArgumentOptimization == other.internalIsUnusedArgumentOptimizationAllowed()
         && allowUnusedReturnValueOptimization
             == other.internalIsUnusedReturnValueOptimizationAllowed()
-        && allowParameterNamesRemoval == other.internalIsParameterNamesRemovalAllowed();
+        && allowParameterNamesRemoval == other.internalIsParameterNamesRemovalAllowed()
+        && whyAreYouNotInlining == other.internalIsWhyAreYouNotInliningEnabled();
   }
 
   @Override
@@ -365,6 +376,7 @@
     hash += bit(allowUnusedArgumentOptimization, index++);
     hash += bit(allowUnusedReturnValueOptimization, index++);
     hash += bit(allowParameterNamesRemoval, index++);
+    hash += bit(whyAreYouNotInlining, index++);
     hash += bit(parameterAnnotationsInfo.isTop(), index);
     return hash;
   }
@@ -432,6 +444,9 @@
         case "allowParameterNamesRemoval":
           builder.setAllowParameterNamesRemoval(Boolean.parseBoolean(value));
           break;
+        case "whyAreYouNotInlining":
+          builder.setWhyAreYouNotInlining(Boolean.parseBoolean(value));
+          break;
         case "parameterAnnotationsInfo":
           builder.setParameterAnnotationInfo(KeepAnnotationCollectionInfoExported.parse(value));
           break;
@@ -462,6 +477,7 @@
     lines.add("allowUnusedArgumentOptimization: " + allowUnusedArgumentOptimization);
     lines.add("allowUnusedReturnValueOptimization: " + allowUnusedReturnValueOptimization);
     lines.add("allowParameterNamesRemoval: " + allowParameterNamesRemoval);
+    lines.add("whyAreYouNotInlining: " + whyAreYouNotInlining);
     lines.add("parameterAnnotationsInfo: " + parameterAnnotationsInfo);
     return lines;
   }
@@ -484,6 +500,7 @@
     private boolean allowUnusedArgumentOptimization;
     private boolean allowUnusedReturnValueOptimization;
     private boolean allowParameterNamesRemoval;
+    private boolean whyAreYouNotInlining;
     private KeepAnnotationCollectionInfo.Builder parameterAnnotationsInfo;
 
     public Builder() {
@@ -509,6 +526,7 @@
       allowUnusedReturnValueOptimization =
           original.internalIsUnusedReturnValueOptimizationAllowed();
       allowParameterNamesRemoval = original.internalIsParameterNamesRemovalAllowed();
+      whyAreYouNotInlining = original.internalIsWhyAreYouNotInliningEnabled();
       parameterAnnotationsInfo = original.internalParameterAnnotationsInfo().toBuilder();
     }
 
@@ -657,6 +675,15 @@
       return self();
     }
 
+    public boolean isWhyAreYouNotInliningEnabled() {
+      return whyAreYouNotInlining;
+    }
+
+    public Builder setWhyAreYouNotInlining(boolean whyAreYouNotInlining) {
+      this.whyAreYouNotInlining = whyAreYouNotInlining;
+      return self();
+    }
+
     public KeepAnnotationCollectionInfo.Builder getParameterAnnotationsInfo() {
       return parameterAnnotationsInfo;
     }
@@ -709,6 +736,7 @@
           && isUnusedReturnValueOptimizationAllowed()
               == other.internalIsUnusedReturnValueOptimizationAllowed()
           && isParameterNamesRemovalAllowed() == other.internalIsParameterNamesRemovalAllowed()
+          && isWhyAreYouNotInliningEnabled() == other.internalIsWhyAreYouNotInliningEnabled()
           && parameterAnnotationsInfo.isEqualTo(other.parameterAnnotationsInfo);
     }
 
@@ -736,6 +764,7 @@
           .setAllowUnusedArgumentOptimization(false)
           .setAllowUnusedReturnValueOptimization(false)
           .setAllowParameterNamesRemoval(false)
+          .setWhyAreYouNotInlining(false)
           .setParameterAnnotationInfo(KeepAnnotationCollectionInfo.Builder.createTop());
     }
 
@@ -758,6 +787,7 @@
           .setAllowUnusedArgumentOptimization(true)
           .setAllowUnusedReturnValueOptimization(true)
           .setAllowParameterNamesRemoval(true)
+          .setWhyAreYouNotInlining(false)
           .setParameterAnnotationInfo(KeepAnnotationCollectionInfo.Builder.createBottom());
     }
   }
@@ -862,6 +892,15 @@
       return self();
     }
 
+    public boolean isWhyAreYouNotInliningEnabled() {
+      return builder.isWhyAreYouNotInliningEnabled();
+    }
+
+    public Joiner setWhyAreYouNotInlining() {
+      builder.setWhyAreYouNotInlining(true);
+      return self();
+    }
+
     @Override
     public Joiner asMethodJoiner() {
       return this;
@@ -904,7 +943,8 @@
               Joiner::disallowUnusedReturnValueOptimization)
           .applyIf(
               !joiner.builder.isParameterNamesRemovalAllowed(),
-              Joiner::disallowParameterNamesRemoval);
+              Joiner::disallowParameterNamesRemoval)
+          .applyIf(joiner.builder.isWhyAreYouNotInliningEnabled(), Joiner::setWhyAreYouNotInlining);
     }
 
     @Override
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 5b0c3f1..96c75c0 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -471,6 +471,8 @@
   private final ProguardPathFilter keepDirectories;
   private final boolean protoShrinking;
   private final int maxRemovedAndroidLogLevel;
+  private final boolean hasWhyAreYouNotInliningRule;
+  private final boolean hasWhyAreYouNotObfuscatingRule;
 
   private ProguardConfiguration(
       String parsedConfiguration,
@@ -545,6 +547,10 @@
     this.keepDirectories = keepDirectories;
     this.protoShrinking = protoShrinking;
     this.maxRemovedAndroidLogLevel = maxRemovedAndroidLogLevel;
+    this.hasWhyAreYouNotInliningRule =
+        Iterables.any(rules, rule -> rule instanceof WhyAreYouNotInliningRule);
+    this.hasWhyAreYouNotObfuscatingRule =
+        Iterables.any(rules, rule -> rule instanceof WhyAreYouNotObfuscatingRule);
   }
 
   /**
@@ -716,6 +722,14 @@
     return Iterables.any(rules, ProguardConfigurationRule::isMaximumRemovedAndroidLogLevelRule);
   }
 
+  public boolean hasWhyAreYouNotInliningRule() {
+    return hasWhyAreYouNotInliningRule;
+  }
+
+  public boolean hasWhyAreYouNotObfuscatingRule() {
+    return hasWhyAreYouNotObfuscatingRule;
+  }
+
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
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 dddd397..6b000b9 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -484,6 +484,10 @@
       } else if (acceptString(ConvertCheckNotNullRule.RULE_NAME)) {
         configurationConsumer.addRule(parseConvertCheckNotNullRule(optionStart));
         return true;
+      } else if (acceptString(WhyAreYouNotObfuscatingRule.RULE_NAME)) {
+        configurationConsumer.addRule(
+            parseRuleWithClassSpec(optionStart, WhyAreYouNotObfuscatingRule.builder()));
+        return true;
       } else if (acceptString(WhyAreYouNotInliningRule.RULE_NAME)) {
         configurationConsumer.addRule(
             parseRuleWithClassSpec(optionStart, WhyAreYouNotInliningRule.builder()));
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 2541fcb..e1546fc 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -138,7 +138,6 @@
         DependentMinimumKeepInfoCollection.createConcurrent();
     private final LinkedHashMap<DexReference, DexReference> reasonAsked = new LinkedHashMap<>();
     private final Set<DexMethod> alwaysInline = Sets.newIdentityHashSet();
-    private final Set<DexMethod> whyAreYouNotInlining = Sets.newIdentityHashSet();
     private final Set<DexMethod> reprocess = Sets.newIdentityHashSet();
     private final PredicateSet<DexType> alwaysClassInline = new PredicateSet<>();
     private final Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule =
@@ -499,9 +498,14 @@
       } else if (rule instanceof ProguardIdentifierNameStringRule) {
         markMatchingFields(clazz, fieldKeepRules, rule, ifRulePreconditionMatch);
         markMatchingMethods(clazz, methodKeepRules, rule, ifRulePreconditionMatch);
-      } else {
-        assert rule instanceof ConvertCheckNotNullRule;
+      } else if (rule instanceof ConvertCheckNotNullRule) {
         markMatchingMethods(clazz, methodKeepRules, rule, ifRulePreconditionMatch);
+      } else if (rule instanceof WhyAreYouNotObfuscatingRule) {
+        markClass(clazz, rule, ifRulePreconditionMatch);
+        markMatchingFields(clazz, fieldKeepRules, rule, ifRulePreconditionMatch);
+        markMatchingMethods(clazz, methodKeepRules, rule, ifRulePreconditionMatch);
+      } else {
+        assert false : rule.getClass().getName();
       }
     }
 
@@ -658,7 +662,6 @@
           dependentMinimumKeepInfo,
           ImmutableList.copyOf(reasonAsked.values()),
           alwaysInline,
-          whyAreYouNotInlining,
           reprocess,
           alwaysClassInline,
           mayHaveSideEffects,
@@ -1482,7 +1485,15 @@
         if (!item.isMethod()) {
           throw new Unreachable();
         }
-        whyAreYouNotInlining.add(item.asMethod().getReference());
+        dependentMinimumKeepInfo
+            .getOrCreateUnconditionalMinimumKeepInfoFor(item.getReference())
+            .asMethodJoiner()
+            .setWhyAreYouNotInlining();
+        context.markAsUsed();
+      } else if (context instanceof WhyAreYouNotObfuscatingRule) {
+        dependentMinimumKeepInfo
+            .getOrCreateUnconditionalMinimumKeepInfoFor(item.getReference())
+            .setWhyAreYouNotObfuscating();
         context.markAsUsed();
       } else if (context.isClassInlineRule()) {
         ClassInlineRule classInlineRule = context.asClassInlineRule();
@@ -2202,7 +2213,6 @@
 
     public final ImmutableList<DexReference> reasonAsked;
     public final Set<DexMethod> alwaysInline;
-    public final Set<DexMethod> whyAreYouNotInlining;
     public final Set<DexMethod> reprocess;
     public final PredicateSet<DexType> alwaysClassInline;
     public final Map<DexReference, ProguardMemberRule> mayHaveSideEffects;
@@ -2215,7 +2225,6 @@
         DependentMinimumKeepInfoCollection dependentMinimumKeepInfo,
         ImmutableList<DexReference> reasonAsked,
         Set<DexMethod> alwaysInline,
-        Set<DexMethod> whyAreYouNotInlining,
         Set<DexMethod> reprocess,
         PredicateSet<DexType> alwaysClassInline,
         Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
@@ -2233,7 +2242,6 @@
           pendingMethodMoveInverse);
       this.reasonAsked = reasonAsked;
       this.alwaysInline = alwaysInline;
-      this.whyAreYouNotInlining = whyAreYouNotInlining;
       this.reprocess = reprocess;
       this.alwaysClassInline = alwaysClassInline;
       this.mayHaveSideEffects = mayHaveSideEffects;
@@ -2347,7 +2355,6 @@
                 getDependentMinimumKeepInfo().rewrittenWithLens(graphLens, timing),
                 reasonAsked,
                 alwaysInline,
-                whyAreYouNotInlining,
                 reprocess,
                 alwaysClassInline,
                 mayHaveSideEffects,
@@ -2648,7 +2655,6 @@
           reasonAsked,
           Collections.emptySet(),
           Collections.emptySet(),
-          Collections.emptySet(),
           PredicateSet.empty(),
           emptyMap(),
           emptyMap(),
diff --git a/src/main/java/com/android/tools/r8/shaking/WhyAreYouNotInliningRule.java b/src/main/java/com/android/tools/r8/shaking/WhyAreYouNotInliningRule.java
index 4801930..f114450 100644
--- a/src/main/java/com/android/tools/r8/shaking/WhyAreYouNotInliningRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/WhyAreYouNotInliningRule.java
@@ -77,6 +77,13 @@
     return new Builder();
   }
 
+  // The ServiceLoader rewriter emits debug information if -whyareyounotinlining matches
+  // ServiceLoader.load.
+  @Override
+  public boolean isApplicableToLibraryClasses() {
+    return true;
+  }
+
   @Override
   String typeString() {
     return RULE_NAME;
diff --git a/src/main/java/com/android/tools/r8/shaking/WhyAreYouNotObfuscatingRule.java b/src/main/java/com/android/tools/r8/shaking/WhyAreYouNotObfuscatingRule.java
new file mode 100644
index 0000000..b6b291b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/WhyAreYouNotObfuscatingRule.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2025, 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;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import java.util.List;
+
+public class WhyAreYouNotObfuscatingRule extends ProguardConfigurationRule {
+
+  public static final String RULE_NAME = "whyareyounotobfuscating";
+
+  @SuppressWarnings("NonCanonicalType")
+  public static class Builder
+      extends ProguardConfigurationRule.Builder<WhyAreYouNotObfuscatingRule, Builder> {
+
+    private Builder() {
+      super();
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+
+    @Override
+    public WhyAreYouNotObfuscatingRule build() {
+      return new WhyAreYouNotObfuscatingRule(
+          origin,
+          getPosition(),
+          source,
+          buildClassAnnotations(),
+          classAccessFlags,
+          negatedClassAccessFlags,
+          classTypeNegated,
+          classType,
+          classNames,
+          buildInheritanceAnnotations(),
+          inheritanceClassName,
+          inheritanceIsExtends,
+          memberRules);
+    }
+  }
+
+  private WhyAreYouNotObfuscatingRule(
+      Origin origin,
+      Position position,
+      String source,
+      List<ProguardTypeMatcher> classAnnotations,
+      ProguardAccessFlags classAccessFlags,
+      ProguardAccessFlags negatedClassAccessFlags,
+      boolean classTypeNegated,
+      ProguardClassType classType,
+      ProguardClassNameList classNames,
+      List<ProguardTypeMatcher> inheritanceAnnotations,
+      ProguardTypeMatcher inheritanceClassName,
+      boolean inheritanceIsExtends,
+      List<ProguardMemberRule> memberRules) {
+    super(
+        origin,
+        position,
+        source,
+        classAnnotations,
+        classAccessFlags,
+        negatedClassAccessFlags,
+        classTypeNegated,
+        classType,
+        classNames,
+        inheritanceAnnotations,
+        inheritanceClassName,
+        inheritanceIsExtends,
+        memberRules);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  String typeString() {
+    return RULE_NAME;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/whyareyounotobfuscating/WhyAreYouNotObfuscatingClassTest.java b/src/test/java/com/android/tools/r8/naming/whyareyounotobfuscating/WhyAreYouNotObfuscatingClassTest.java
new file mode 100644
index 0000000..489c552
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/whyareyounotobfuscating/WhyAreYouNotObfuscatingClassTest.java
@@ -0,0 +1,53 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.naming.whyareyounotobfuscating;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringDiagnostic;
+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 WhyAreYouNotObfuscatingClassTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultDexRuntime().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters)
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-whyareyounotobfuscating class " + Main.class.getTypeName())
+        .allowDiagnosticWarningMessages()
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics.assertWarningsMatch(
+                    allOf(
+                        diagnosticType(StringDiagnostic.class),
+                        diagnosticMessage(
+                            containsString(
+                                Main.class.getTypeName() + " is not obfuscated due to -keep")))));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {}
+  }
+}