Add API for controlling empty member rules to default init conversion

Bug: b/356344563
Fixes: b/356074807
Change-Id: I356d07ebed5da56268a1235cc8e26bb5b38b41d6
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 3bd0ac5..a5caffa 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -485,6 +485,19 @@
     }
 
     /**
+     * Used to disable that keep rules with no member rules are implicitly converted into rules that
+     * keep the default instance constructor.
+     *
+     * <p>This currently defaults to true in stable versions.
+     */
+    public Builder setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+        boolean enableEmptyMemberRulesToDefaultInitRuleConversion) {
+      parserOptionsBuilder.setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+          enableEmptyMemberRulesToDefaultInitRuleConversion);
+      return this;
+    }
+
+    /**
      * Used to specify if the application is using isolated splits, i.e., if split APKs installed
      * for this application are loaded into their own Context objects.
      *
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 8cffa28..fc42207 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -846,8 +846,7 @@
           .setStart(start);
       parseRuleTypeAndModifiers(keepRuleBuilder);
       parseClassSpec(keepRuleBuilder);
-      if (configurationBuilder.isForceProguardCompatibility()
-          || options.isForceEmptyMemberRulesToDefaultInitRuleConversionEnabled()) {
+      if (options.isEmptyMemberRulesToDefaultInitRuleConversionEnabled(configurationBuilder)) {
         if (keepRuleBuilder.getMemberRules().isEmpty()
             && keepRuleBuilder.getKeepRuleType()
                 != ProguardKeepRuleType.KEEP_CLASSES_WITH_MEMBERS) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
index acdd37c..157be68 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
@@ -8,30 +8,36 @@
 
 public class ProguardConfigurationParserOptions {
 
+  private final boolean enableEmptyMemberRulesToDefaultInitRuleConversion;
   private final boolean enableExperimentalCheckEnumUnboxed;
   private final boolean enableExperimentalConvertCheckNotNull;
   private final boolean enableExperimentalWhyAreYouNotInlining;
   private final boolean enableTestingOptions;
-  private final boolean forceEnableEmptyMemberRulesToDefaultInitRuleConversion;
 
   ProguardConfigurationParserOptions(
+      boolean enableEmptyMemberRulesToDefaultInitRuleConversion,
       boolean enableExperimentalCheckEnumUnboxed,
       boolean enableExperimentalConvertCheckNotNull,
       boolean enableExperimentalWhyAreYouNotInlining,
-      boolean enableTestingOptions,
-      boolean forceEnableEmptyMemberRulesToDefaultInitRuleConversion) {
+      boolean enableTestingOptions) {
     this.enableExperimentalCheckEnumUnboxed = enableExperimentalCheckEnumUnboxed;
     this.enableExperimentalConvertCheckNotNull = enableExperimentalConvertCheckNotNull;
     this.enableExperimentalWhyAreYouNotInlining = enableExperimentalWhyAreYouNotInlining;
     this.enableTestingOptions = enableTestingOptions;
-    this.forceEnableEmptyMemberRulesToDefaultInitRuleConversion =
-        forceEnableEmptyMemberRulesToDefaultInitRuleConversion;
+    this.enableEmptyMemberRulesToDefaultInitRuleConversion =
+        enableEmptyMemberRulesToDefaultInitRuleConversion;
   }
 
   public static Builder builder() {
     return new Builder();
   }
 
+  public boolean isEmptyMemberRulesToDefaultInitRuleConversionEnabled(
+      ProguardConfiguration.Builder configurationBuilder) {
+    return enableEmptyMemberRulesToDefaultInitRuleConversion
+        || configurationBuilder.isForceProguardCompatibility();
+  }
+
   public boolean isExperimentalCheckEnumUnboxedEnabled() {
     return enableExperimentalCheckEnumUnboxed;
   }
@@ -44,23 +50,22 @@
     return enableExperimentalWhyAreYouNotInlining;
   }
 
-  public boolean isForceEmptyMemberRulesToDefaultInitRuleConversionEnabled() {
-    return forceEnableEmptyMemberRulesToDefaultInitRuleConversion;
-  }
-
   public boolean isTestingOptionsEnabled() {
     return enableTestingOptions;
   }
 
   public static class Builder {
 
+    private boolean enableEmptyMemberRulesToDefaultInitRuleConversion;
     private boolean enableExperimentalCheckEnumUnboxed;
     private boolean enableExperimentalConvertCheckNotNull;
     private boolean enableExperimentalWhyAreYouNotInlining;
     private boolean enableTestingOptions;
-    private boolean forceEnableEmptyMemberRulesToDefaultInitRuleConversion;
 
     public Builder readEnvironment() {
+      enableEmptyMemberRulesToDefaultInitRuleConversion =
+          parseSystemPropertyOrDefault(
+              "com.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversion", false);
       enableExperimentalCheckEnumUnboxed =
           parseSystemPropertyOrDefault(
               "com.android.tools.r8.experimental.enablecheckenumunboxed", false);
@@ -72,9 +77,13 @@
               "com.android.tools.r8.experimental.enablewhyareyounotinlining", false);
       enableTestingOptions =
           parseSystemPropertyOrDefault("com.android.tools.r8.allowTestProguardOptions", false);
-      forceEnableEmptyMemberRulesToDefaultInitRuleConversion =
-          parseSystemPropertyOrDefault(
-              "com.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversion", false);
+      return this;
+    }
+
+    public Builder setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+        boolean enableEmptyMemberRulesToDefaultInitRuleConversion) {
+      this.enableEmptyMemberRulesToDefaultInitRuleConversion =
+          enableEmptyMemberRulesToDefaultInitRuleConversion;
       return this;
     }
 
@@ -103,11 +112,11 @@
 
     public ProguardConfigurationParserOptions build() {
       return new ProguardConfigurationParserOptions(
+          enableEmptyMemberRulesToDefaultInitRuleConversion,
           enableExperimentalCheckEnumUnboxed,
           enableExperimentalConvertCheckNotNull,
           enableExperimentalWhyAreYouNotInlining,
-          enableTestingOptions,
-          forceEnableEmptyMemberRulesToDefaultInitRuleConversion);
+          enableTestingOptions);
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java b/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
new file mode 100644
index 0000000..bdb0a85
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2024, 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 static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+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 EmptyMemberRulesToDefaultInitRuleConversionTest extends TestBase {
+
+  @Parameter(0)
+  public boolean enableEmptyMemberRulesToDefaultInitRuleConversion;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, convert: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        getTestParameters().withDefaultRuntimes().withMinimumApiLevel().build());
+  }
+
+  @Test
+  public void testCompat() throws Exception {
+    testForR8Compat(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassRules(Main.class)
+        .enableEmptyMemberRulesToDefaultInitRuleConversion(
+            enableEmptyMemberRulesToDefaultInitRuleConversion)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(inspector -> assertThat(inspector.clazz(Main.class).init(), isPresent()));
+  }
+
+  @Test
+  public void testFull() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassRules(Main.class)
+        .enableEmptyMemberRulesToDefaultInitRuleConversion(
+            enableEmptyMemberRulesToDefaultInitRuleConversion)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(
+            inspector ->
+                assertThat(
+                    inspector.clazz(Main.class).init(),
+                    isPresentIf(enableEmptyMemberRulesToDefaultInitRuleConversion)));
+  }
+
+  static class Main {}
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
index 4d7338d..3309783 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
@@ -76,6 +76,7 @@
 
   private AllowedDiagnosticMessages allowedDiagnosticMessages = AllowedDiagnosticMessages.NONE;
   private boolean allowUnusedProguardConfigurationRules = false;
+  private boolean enableEmptyMemberRulesToDefaultInitRuleConversion = false;
   private boolean enableIsolatedSplits = false;
   private boolean enableMissingLibraryApiModeling = true;
   private boolean enableStartupLayoutOptimization = true;
@@ -139,6 +140,8 @@
     ToolHelper.addSyntheticProguardRulesConsumerForTesting(
         builder, rules -> box.syntheticProguardRules = rules);
     libraryDesugaringTestConfiguration.configure(builder);
+    builder.setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+        enableEmptyMemberRulesToDefaultInitRuleConversion);
     builder.setEnableIsolatedSplits(enableIsolatedSplits);
     builder.setEnableExperimentalMissingLibraryApiModeling(enableMissingLibraryApiModeling);
     builder.setEnableStartupLayoutOptimization(enableStartupLayoutOptimization);
@@ -882,6 +885,13 @@
     return self();
   }
 
+  public T enableEmptyMemberRulesToDefaultInitRuleConversion(
+      boolean enableEmptyMemberRulesToDefaultInitRuleConversion) {
+    this.enableEmptyMemberRulesToDefaultInitRuleConversion =
+        enableEmptyMemberRulesToDefaultInitRuleConversion;
+    return self();
+  }
+
   public T enableIsolatedSplits(boolean enableIsolatedSplits) {
     this.enableIsolatedSplits = enableIsolatedSplits;
     return self();