Merge "Build keep rules for the Proguard compatiblity actions"
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 8c01bc2..3ba3e5c 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -35,6 +35,7 @@
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.MainDexListBuilder;
 import com.android.tools.r8.shaking.ProguardClassFilter;
+import com.android.tools.r8.shaking.ProguardKeepRule;
 import com.android.tools.r8.shaking.ReasonPrinter;
 import com.android.tools.r8.shaking.RootSetBuilder;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
@@ -47,6 +48,7 @@
 import com.android.tools.r8.utils.AndroidAppOutputSink;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.CompilationFailedException;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.IOExceptionDiagnostic;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
@@ -56,15 +58,18 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.VersionProperties;
 import com.google.common.io.ByteStreams;
+import com.google.common.io.Closer;
 import java.io.ByteArrayOutputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -198,6 +203,25 @@
           new AbstractMethodRemover(appInfo).run();
           new AnnotationRemover(appInfo.withLiveness(), options).run();
         }
+
+        // TODO(69445518): This is still work in progress, and this file writing is currently used
+        // for testing.
+        if (options.forceProguardCompatibility
+            && options.proguardCompatibilityRulesOutput != null) {
+          try (Closer closer = Closer.create()) {
+            OutputStream outputStream =
+                FileUtils.openPath(
+                    closer,
+                    options.proguardCompatibilityRulesOutput,
+                    StandardOpenOption.CREATE,
+                    StandardOpenOption.TRUNCATE_EXISTING,
+                    StandardOpenOption.WRITE);
+            PrintStream ps = new PrintStream(outputStream);
+            for (ProguardKeepRule rule : appInfo.withLiveness().getProguardCompatibilityRules()) {
+              ps.println(rule);
+            }
+          }
+        }
       } finally {
         timing.end();
       }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index fda5054..cf0d4ec 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -46,6 +46,7 @@
     private boolean ignoreMissingClasses = false;
     private boolean forceProguardCompatibility = false;
     private Path proguardMapOutput = null;
+    protected Path proguardCompatibilityRulesOutput = null;
 
     private Builder() {
       setMode(CompilationMode.RELEASE);
@@ -282,7 +283,8 @@
           ignoreMissingClasses,
           forceProguardCompatibility,
           ignoreMissingClassesWhenNotShrinking,
-          proguardMapOutput);
+          proguardMapOutput,
+          proguardCompatibilityRulesOutput);
 
       failIfPendingErrors();
 
@@ -329,6 +331,7 @@
   private final boolean forceProguardCompatibility;
   private final boolean ignoreMissingClassesWhenNotShrinking;
   private final Path proguardMapOutput;
+  private final Path proguardCompatibilityRulesOutput;
 
   public static Builder builder() {
     return new Builder();
@@ -453,7 +456,8 @@
       boolean ignoreMissingClasses,
       boolean forceProguardCompatibility,
       boolean ignoreMissingClassesWhenNotShrinking,
-      Path proguardMapOutput) {
+      Path proguardMapOutput,
+      Path proguardCompatibilityRulesOutput) {
     super(inputApp, outputPath, outputMode, mode, minApiLevel, reporter,
         enableDesugaring);
     assert proguardConfiguration != null;
@@ -469,6 +473,7 @@
     this.forceProguardCompatibility = forceProguardCompatibility;
     this.ignoreMissingClassesWhenNotShrinking = ignoreMissingClassesWhenNotShrinking;
     this.proguardMapOutput = proguardMapOutput;
+    this.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
   }
 
   private R8Command(boolean printHelp, boolean printVersion) {
@@ -483,6 +488,7 @@
     forceProguardCompatibility = false;
     ignoreMissingClassesWhenNotShrinking = false;
     proguardMapOutput = null;
+    proguardCompatibilityRulesOutput = null;
   }
   public boolean useTreeShaking() {
     return useTreeShaking;
@@ -537,6 +543,7 @@
       internal.inlineAccessors = false;
     }
     internal.proguardMapOutput = proguardMapOutput;
+    internal.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
 
     // EXPERIMENTAL flags.
     assert !internal.forceProguardCompatibility;
diff --git a/src/main/java/com/android/tools/r8/compatproguard/CompatProguardCommandBuilder.java b/src/main/java/com/android/tools/r8/compatproguard/CompatProguardCommandBuilder.java
index b7faf07..bc44ff4 100644
--- a/src/main/java/com/android/tools/r8/compatproguard/CompatProguardCommandBuilder.java
+++ b/src/main/java/com/android/tools/r8/compatproguard/CompatProguardCommandBuilder.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.origin.EmbeddedOrigin;
 import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
 import java.util.List;
 
 public class CompatProguardCommandBuilder extends R8Command.Builder {
@@ -24,4 +25,8 @@
     setEnableDesugaring(false);
     addProguardConfiguration(CLASS_FOR_NAME, EmbeddedOrigin.INSTANCE);
   }
+
+  public void setProguardCompatibilityRulesOutput(Path path) {
+    proguardCompatibilityRulesOutput = path;
+  }
 }
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 29e0c44..ad00c86 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -139,6 +139,11 @@
   private Queue<Action> workList = Queues.newArrayDeque();
 
   /**
+   * A queue of items that have been added to try to keep Proguard compatibility.
+   */
+  private Queue<Action> proguardCompatibilityWorkList = Queues.newArrayDeque();
+
+  /**
    * A cache for DexMethod that have been marked reachable.
    */
   private Set<DexMethod> virtualTargetsMarkedAsReachable = Sets.newIdentityHashSet();
@@ -160,6 +165,11 @@
    */
   private final Map<DexType, Set<DexAnnotation>> deferredAnnotations = new IdentityHashMap<>();
 
+  /**
+   * Set of keep rules generated for Proguard compatibility in Proguard compatibility mode.
+   */
+  private final Set<ProguardKeepRule> proguardCompatibilityRules = Sets.newHashSet();
+
   public Enqueuer(AppInfoWithSubtyping appInfo, InternalOptions options) {
     this.appInfo = appInfo;
     this.options = options;
@@ -368,14 +378,16 @@
       // Add all dependent static members to the workqueue.
       enqueueRootItems(rootSet.getDependentStaticMembers(type));
 
-      // For Proguard compatibility mark default initializer for live type as live.
+      // For Proguard compatibility keep the default initializer for live types.
       if (options.forceProguardCompatibility) {
-        if (holder.hasDefaultInitializer()) {
+        if (holder.isProgramClass() && holder.hasDefaultInitializer()) {
           DexEncodedMethod init = holder.getDefaultInitializer();
-          // TODO(68246915): For now just register the default constructor as the source of
-          // instantiation.
-          markInstantiated(type, init);
-          markDirectStaticOrConstructorMethodAsLive(init, KeepReason.reachableFromLiveType(type));
+          ProguardKeepRule rule =
+              ProguardConfigurationUtils.buildDefaultInitializerKeepRule(holder);
+          proguardCompatibilityWorkList.add(
+              Action.markInstantiated(holder, KeepReason.dueToProguardCompatibilityKeepRule(rule)));
+          proguardCompatibilityWorkList.add(
+              Action.markMethodLive(init, KeepReason.dueToProguardCompatibilityKeepRule(rule)));
         }
       }
     }
@@ -452,6 +464,7 @@
     if (!instantiatedTypes.add(clazz.type, reason)) {
       return;
     }
+    collectProguardCompatibilityRule(reason);
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Class `%s` is instantiated, processing...", clazz);
     }
@@ -815,44 +828,53 @@
     this.rootSet = rootSet;
     // Translate the result of root-set computation into enqueuer actions.
     enqueueRootItems(rootSet.noShrinking);
-    appInfo.libraryClasses().forEach(this::markAllVirtualMethodsReachable);
+    appInfo.libraryClasses().forEach(this::markAllLibraryVirtualMethodsReachable);
     return trace(timing);
   }
 
   private AppInfoWithLiveness trace(Timing timing) {
     timing.begin("Grow the tree.");
     try {
-      while (!workList.isEmpty()) {
-        Action action = workList.poll();
-        switch (action.kind) {
-          case MARK_INSTANTIATED:
-            processNewlyInstantiatedClass((DexClass) action.target, action.reason);
-            break;
-          case MARK_REACHABLE_FIELD:
-            markInstanceFieldAsReachable((DexField) action.target, action.reason);
-            break;
-          case MARK_REACHABLE_VIRTUAL:
-            markVirtualMethodAsReachable((DexMethod) action.target, false, action.reason);
-            break;
-          case MARK_REACHABLE_INTERFACE:
-            markVirtualMethodAsReachable((DexMethod) action.target, true, action.reason);
-            break;
-          case MARK_REACHABLE_SUPER:
-            markSuperMethodAsReachable((DexMethod) action.target,
-                (DexEncodedMethod) action.context);
-            break;
-          case MARK_METHOD_KEPT:
-            markMethodAsKept((DexEncodedMethod) action.target, action.reason);
-            break;
-          case MARK_FIELD_KEPT:
-            markFieldAsKept((DexEncodedField) action.target, action.reason);
-            break;
-          case MARK_METHOD_LIVE:
-            processNewlyLiveMethod(((DexEncodedMethod) action.target), action.reason);
-            break;
-          default:
-            throw new IllegalArgumentException(action.kind.toString());
+      while (true) {
+        while (!workList.isEmpty()) {
+          Action action = workList.poll();
+          switch (action.kind) {
+            case MARK_INSTANTIATED:
+              processNewlyInstantiatedClass((DexClass) action.target, action.reason);
+              break;
+            case MARK_REACHABLE_FIELD:
+              markInstanceFieldAsReachable((DexField) action.target, action.reason);
+              break;
+            case MARK_REACHABLE_VIRTUAL:
+              markVirtualMethodAsReachable((DexMethod) action.target, false, action.reason);
+              break;
+            case MARK_REACHABLE_INTERFACE:
+              markVirtualMethodAsReachable((DexMethod) action.target, true, action.reason);
+              break;
+            case MARK_REACHABLE_SUPER:
+              markSuperMethodAsReachable((DexMethod) action.target,
+                  (DexEncodedMethod) action.context);
+              break;
+            case MARK_METHOD_KEPT:
+              markMethodAsKept((DexEncodedMethod) action.target, action.reason);
+              break;
+            case MARK_FIELD_KEPT:
+              markFieldAsKept((DexEncodedField) action.target, action.reason);
+              break;
+            case MARK_METHOD_LIVE:
+              processNewlyLiveMethod(((DexEncodedMethod) action.target), action.reason);
+              break;
+            default:
+              throw new IllegalArgumentException(action.kind.toString());
+          }
         }
+        // Continue fix-point processing while there are additional work items to ensure
+        // Proguard compatibility.
+        if (proguardCompatibilityWorkList.isEmpty()) {
+          break;
+        }
+        workList.addAll(proguardCompatibilityWorkList);
+        proguardCompatibilityWorkList.clear();
       }
       if (Log.ENABLED) {
         Set<DexEncodedMethod> allLive = Sets.newIdentityHashSet();
@@ -911,7 +933,8 @@
     }
   }
 
-  private void markAllVirtualMethodsReachable(DexClass clazz) {
+  private void markAllLibraryVirtualMethodsReachable(DexClass clazz) {
+    assert clazz.isLibraryClass();
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Marking all methods of library class `%s` as reachable.",
           clazz.type);
@@ -925,6 +948,7 @@
 
   private void processNewlyLiveMethod(DexEncodedMethod method, KeepReason reason) {
     if (liveMethods.add(method, reason)) {
+      collectProguardCompatibilityRule(reason);
       DexClass holder = appInfo.definitionFor(method.method.holder);
       assert holder != null;
       if (holder.isLibraryClass()) {
@@ -966,6 +990,12 @@
     }
   }
 
+  private void collectProguardCompatibilityRule(KeepReason reason) {
+    if (reason.isDueToProguardCompatibility()) {
+      proguardCompatibilityRules.add(reason.getProguardKeepRule());
+    }
+  }
+
   private Set<DexField> collectFields(Map<DexType, Set<DexField>> map) {
     return map.values().stream().flatMap(Collection::stream)
         .collect(Collectors.toCollection(Sets::newIdentityHashSet));
@@ -1195,6 +1225,11 @@
      */
     final Set<DexType> prunedTypes;
 
+    /**
+     * Set of keep rules generated for Proguard compatibility in Proguard compatibility mode.
+     */
+    final Set<ProguardKeepRule> proguardCompatibilityRules;
+
     private AppInfoWithLiveness(AppInfoWithSubtyping appInfo, Enqueuer enqueuer) {
       super(appInfo);
       this.liveTypes = ImmutableSortedSet.copyOf(
@@ -1222,6 +1257,7 @@
       this.identifierNameStrings = enqueuer.rootSet.identifierNameStrings;
       this.extensions = enqueuer.extensionsState;
       this.prunedTypes = Collections.emptySet();
+      this.proguardCompatibilityRules = ImmutableSet.copyOf(enqueuer.proguardCompatibilityRules);
       assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
       assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
     }
@@ -1254,6 +1290,7 @@
       this.alwaysInline = previous.alwaysInline;
       this.identifierNameStrings = previous.identifierNameStrings;
       this.prunedTypes = mergeSets(previous.prunedTypes, removedClasses);
+      this.proguardCompatibilityRules = previous.proguardCompatibilityRules;
       assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
       assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
     }
@@ -1280,6 +1317,7 @@
       this.directInvokes = rewriteItems(previous.directInvokes, lense::lookupMethod);
       this.staticInvokes = rewriteItems(previous.staticInvokes, lense::lookupMethod);
       this.prunedTypes = rewriteItems(previous.prunedTypes, lense::lookupType);
+      this.proguardCompatibilityRules = previous.proguardCompatibilityRules;
       // TODO(herhut): Migrate these to Descriptors, as well.
       assert assertNotModifiedByLense(previous.noSideEffects.keySet(), lense);
       this.noSideEffects = previous.noSideEffects;
@@ -1444,6 +1482,9 @@
       return pinnedItems;
     }
 
+    public Set<ProguardKeepRule> getProguardCompatibilityRules() {
+      return proguardCompatibilityRules;
+    }
     /**
      * Returns a copy of this AppInfoWithLiveness where the set of classes is pruned using the
      * given DexApplication object.
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepReason.java b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
index a6ce411..7308181 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepReason.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
@@ -15,6 +15,10 @@
     return new DueToKeepRule(rule);
   }
 
+  static KeepReason dueToProguardCompatibilityKeepRule(ProguardKeepRule rule) {
+    return new DueToProguardCompatibilityKeepRule(rule);
+  }
+
   static KeepReason instantiatedIn(DexEncodedMethod method) {
     return new InstatiatedIn(method);
   }
@@ -45,15 +49,28 @@
 
   public abstract void print(ReasonFormatter formatter);
 
+  public boolean isDueToProguardCompatibility() {
+    return false;
+  }
+
+  public ProguardKeepRule getProguardKeepRule() {
+    return null;
+  }
+
   private static class DueToKeepRule extends KeepReason {
 
-    private final ProguardKeepRule keepRule;
+    final ProguardKeepRule keepRule;
 
     private DueToKeepRule(ProguardKeepRule keepRule) {
       this.keepRule = keepRule;
     }
 
     @Override
+    public ProguardKeepRule getProguardKeepRule() {
+      return keepRule;
+    }
+
+    @Override
     public void print(ReasonFormatter formatter) {
       formatter.addReason("referenced in keep rule:");
       formatter.addMessage("  " + keepRule + " {");
@@ -69,6 +86,17 @@
     }
   }
 
+  private static class DueToProguardCompatibilityKeepRule extends DueToKeepRule {
+    private DueToProguardCompatibilityKeepRule(ProguardKeepRule keepRule) {
+      super(keepRule);
+    }
+
+    @Override
+    public boolean isDueToProguardCompatibility() {
+      return true;
+    }
+  }
+
   private abstract static class BasedOnOtherMethod extends KeepReason {
 
     private final DexEncodedMethod method;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java b/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
index 8e981a7..efe294a 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardClassSpecification.java
@@ -251,9 +251,9 @@
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
-    StringUtils.appendNonEmpty(builder, " @", classAnnotation, null);
-    StringUtils.appendNonEmpty(builder, " ", classAccessFlags, null);
-    StringUtils.appendNonEmpty(builder, " !", negatedClassAccessFlags.toString().replace(" ", " !"),
+    StringUtils.appendNonEmpty(builder, "@", classAnnotation, null);
+    StringUtils.appendNonEmpty(builder, "", classAccessFlags, null);
+    StringUtils.appendNonEmpty(builder, "!", negatedClassAccessFlags.toString().replace(" ", " !"),
         null);
     if (builder.length() > 0) {
       builder.append(' ');
@@ -262,8 +262,8 @@
     builder.append(' ');
     classNames.writeTo(builder);
     if (hasInheritanceClassName()) {
-      builder.append(inheritanceIsExtends ? " extends" : " implements");
-      StringUtils.appendNonEmpty(builder, " @", inheritanceAnnotation, null);
+      builder.append(inheritanceIsExtends ? "extends" : "implements");
+      StringUtils.appendNonEmpty(builder, "@", inheritanceAnnotation, null);
       builder.append(' ');
       builder.append(inheritanceClassName);
     }
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 f8aa3e2..1f80bb1 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -185,22 +185,7 @@
       this.overloadAggressively = overloadAggressively;
     }
 
-    public ProguardConfiguration build() throws CompilationException {
-      boolean rulesWasEmpty = rules.isEmpty();
-      if (rulesWasEmpty) {
-        setObfuscating(false);
-        setShrinking(false);
-        addRule(ProguardKeepRule.defaultKeepAllRule());
-      }
-
-      ProguardKeepAttributes keepAttributes;
-      if (keepAttributePatterns.isEmpty()
-          && (rulesWasEmpty || (forceProguardCompatibility && !isObfuscating()))) {
-        keepAttributes = ProguardKeepAttributes.fromPatterns(ProguardKeepAttributes.KEEP_ALL);
-      } else {
-        keepAttributes = ProguardKeepAttributes.fromPatterns(keepAttributePatterns);
-      }
-
+    ProguardConfiguration buildRaw() throws CompilationException {
       return new ProguardConfiguration(
           dexItemFactory,
           injars,
@@ -219,7 +204,7 @@
           applyMappingFile,
           verbose,
           renameSourceFileAttribute,
-          keepAttributes,
+          ProguardKeepAttributes.fromPatterns(keepAttributePatterns),
           dontWarnPatterns.build(),
           rules,
           printSeeds,
@@ -232,6 +217,21 @@
           keepParameterNames,
           adaptClassStrings.build());
     }
+
+    public ProguardConfiguration build() throws CompilationException {
+      boolean rulesWasEmpty = rules.isEmpty();
+      if (rules.isEmpty()) {
+        setObfuscating(false);
+        setShrinking(false);
+        addRule(ProguardKeepRule.defaultKeepAllRule());
+      }
+      if (keepAttributePatterns.isEmpty()
+            && (rulesWasEmpty || (forceProguardCompatibility && !isObfuscating()))) {
+        keepAttributePatterns.addAll(ProguardKeepAttributes.KEEP_ALL);
+      }
+
+      return buildRaw();
+    }
   }
 
   private final DexItemFactory dexItemFactory;
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 40747d7..4574b26 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -85,7 +85,7 @@
     return configurationBuilder;
   }
 
-  public ProguardConfiguration getConfig()
+  private void validate()
       throws ProguardRuleParserException, CompilationException {
     if (configurationBuilder.isKeepParameterNames()
         && configurationBuilder.isObfuscating()) {
@@ -93,10 +93,27 @@
       // are not.
       throw new ProguardRuleParserException("-keepparameternames is not supported");
     }
+  }
 
+  /**
+   * Returns the Proguard configuration with default rules derived from empty rules added.
+   */
+  public ProguardConfiguration getConfig()
+      throws ProguardRuleParserException, CompilationException {
+    validate();
     return configurationBuilder.build();
   }
 
+  /**
+   * Returns the Proguard configuration from exactly the rules parsed, without any
+   * defaults derived from empty rules.
+   */
+  public ProguardConfiguration getConfigRawForTesting()
+      throws ProguardRuleParserException, CompilationException {
+    validate();
+    return configurationBuilder.buildRaw();
+  }
+
   public void parse(Path path) throws ProguardRuleParserException, IOException {
     parse(ImmutableList.of(new ProguardConfigurationSourceFile(path)));
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
index 223307c..5fd62df 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
@@ -34,7 +34,7 @@
 
   @Override
   public String toString() {
-    StringBuilder builder = new StringBuilder();
+    StringBuilder builder = new StringBuilder("-");
     builder.append(typeString());
     StringUtils.appendNonEmpty(builder, ",", modifierString(), null);
     builder.append(' ');
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java
new file mode 100644
index 0000000..e00d36a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java
@@ -0,0 +1,28 @@
+// Copyright (c) 2017, 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.graph.DexClass;
+import com.google.common.collect.ImmutableList;
+
+public class ProguardConfigurationUtils {
+  public static ProguardKeepRule buildDefaultInitializerKeepRule(DexClass clazz) {
+    ProguardKeepRule.Builder builder = ProguardKeepRule.builder();
+    builder.setType(ProguardKeepRuleType.KEEP);
+    builder.getModifiersBuilder().allowsObfuscation = true;
+    builder.getModifiersBuilder().allowsOptimization = true;
+    builder.getClassAccessFlags().setPublic();
+    builder.setClassType(ProguardClassType.CLASS);
+    ProguardClassNameList.Builder classNameListBuilder = ProguardClassNameList.builder();
+    classNameListBuilder.addClassName(false, ProguardTypeMatcher.create(clazz.type));
+    builder.setClassNames(classNameListBuilder.build());
+    ProguardMemberRule.Builder memberRuleBuilder = ProguardMemberRule.builder();
+    memberRuleBuilder.setRuleType(ProguardMemberType.INIT);
+    memberRuleBuilder.setName("<init>");
+    memberRuleBuilder.setArguments(ImmutableList.of());
+    builder.getMemberRules().add(memberRuleBuilder.build());
+    return builder.build();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java b/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
index ef149f7..809278f 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
@@ -57,6 +57,10 @@
     }
   }
 
+  public static ProguardTypeMatcher create(DexType type) {
+    return new MatchSpecificType(type);
+  }
+
   public static ProguardTypeMatcher defaultAllMatcher() {
     return MatchAllTypes.MATCH_ALL_TYPES;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 2f08db1..85bb29e 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -99,6 +99,15 @@
     return path;
   }
 
+  public static OutputStream openPath(
+      Closer closer,
+      Path file,
+      OpenOption... openOptions)
+      throws IOException {
+    assert file != null;
+    return openPathWithDefault(closer, file, null, openOptions);
+  }
+
   public static OutputStream openPathWithDefault(
       Closer closer,
       Path file,
@@ -107,6 +116,7 @@
       throws IOException {
     OutputStream mapOut;
     if (file == null) {
+      assert defaultOutput != null;
       mapOut = defaultOutput;
     } else {
       mapOut = Files.newOutputStream(file, openOptions);
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 0840393..76ad1d6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -173,6 +173,8 @@
 
   public Path proguardMapOutput = null;
 
+  public Path proguardCompatibilityRulesOutput = null;
+
   public void warningMissingEnclosingMember(DexType clazz, Origin origin, int version) {
     if (missingEnclosingMembers == null) {
       missingEnclosingMembers = new HashMap<>();
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
index 8214bc7..62ca811 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
@@ -11,7 +11,13 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.compatproguard.CompatProguardCommandBuilder;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.shaking.ProguardClassNameList;
+import com.android.tools.r8.shaking.ProguardConfiguration;
+import com.android.tools.r8.shaking.ProguardConfigurationParser;
+import com.android.tools.r8.shaking.ProguardMemberRule;
+import com.android.tools.r8.shaking.ProguardMemberType;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
@@ -101,19 +107,41 @@
 
   private void runDefaultConstructorTest(boolean forceProguardCompatibility,
       Class<?> testClass, boolean hasDefaultConstructor) throws Exception {
-    R8Command.Builder builder = new CompatProguardCommandBuilder(forceProguardCompatibility, false);
+    CompatProguardCommandBuilder builder =
+        new CompatProguardCommandBuilder(forceProguardCompatibility, false);
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(testClass));
     List<String> proguardConfig = ImmutableList.of(
         "-keep class " + testClass.getCanonicalName() + " {",
         "  public void method();",
         "}");
     builder.addProguardConfiguration(proguardConfig, Origin.unknown());
+    Path proguardCompatibilityRules = temp.newFile().toPath();
+    builder.setProguardCompatibilityRulesOutput(proguardCompatibilityRules);
+
     DexInspector inspector = new DexInspector(ToolHelper.runR8(builder.build()));
     ClassSubject clazz = inspector.clazz(getJavacGeneratedClassName(testClass));
     assertTrue(clazz.isPresent());
     assertEquals(forceProguardCompatibility && hasDefaultConstructor,
         clazz.init(ImmutableList.of()).isPresent());
 
+    // Check the Proguard compatibility rules generated.
+    ProguardConfigurationParser parser =
+        new ProguardConfigurationParser(new DexItemFactory(), null);
+    parser.parse(proguardCompatibilityRules);
+    ProguardConfiguration configuration = parser.getConfigRawForTesting();
+    if (forceProguardCompatibility && hasDefaultConstructor) {
+      assertEquals(1, configuration.getRules().size());
+      ProguardClassNameList classNames = configuration.getRules().get(0).getClassNames();
+      assertEquals(1, classNames.size());
+      assertEquals(testClass.getCanonicalName(),
+          classNames.asSpecificDexTypes().get(0).toSourceString());
+      Set<ProguardMemberRule> memberRules = configuration.getRules().get(0).getMemberRules();
+      assertEquals(1, memberRules.size());
+      assertEquals(ProguardMemberType.INIT, memberRules.iterator().next().getRuleType());
+    } else {
+      assertEquals(0, configuration.getRules().size());
+    }
+
     if (RUN_PROGUARD) {
       Path proguardedJar = File.createTempFile("proguarded", ".jar", temp.getRoot()).toPath();
       Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
@@ -133,7 +161,8 @@
   public void testCheckCast(boolean forceProguardCompatibility, Class mainClass,
       Class instantiatedClass, boolean containsCheckCast)
       throws Exception {
-    R8Command.Builder builder = new CompatProguardCommandBuilder(forceProguardCompatibility, false);
+    R8Command.Builder builder =
+        new CompatProguardCommandBuilder(forceProguardCompatibility, false);
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(mainClass));
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(instantiatedClass));
     List<String> proguardConfig = ImmutableList.of(