Desugared lib: Fix keep rules for empty classes

Bug: 162437880
Change-Id: Ic5588eb973da365553cf7247d62f0d55a7e7da08
diff --git a/src/main/java/com/android/tools/r8/dex/CodeToKeep.java b/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
index 9eb7caa..f4e85a8 100644
--- a/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
+++ b/src/main/java/com/android/tools/r8/dex/CodeToKeep.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.NamingLens;
@@ -36,6 +37,8 @@
 
   abstract void recordClassAllAccesses(DexType type);
 
+  abstract void recordHierarchyOf(DexProgramClass clazz);
+
   abstract boolean isNop();
 
   abstract void generateKeepRules(InternalOptions options);
@@ -116,6 +119,14 @@
       }
     }
 
+    @Override
+    void recordHierarchyOf(DexProgramClass clazz) {
+      recordClassAllAccesses(clazz.superType);
+      for (DexType itf : clazz.interfaces.values) {
+        recordClassAllAccesses(itf);
+      }
+    }
+
     private void keepClass(DexType type) {
       DexType baseType = type.lookupBaseType(options.itemFactory);
       toKeep.putIfAbsent(baseType, new KeepStruct());
@@ -193,6 +204,9 @@
     void recordClassAllAccesses(DexType type) {}
 
     @Override
+    void recordHierarchyOf(DexProgramClass clazz) {}
+
+    @Override
     boolean isNop() {
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 0047bf8..8d1eac7 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -467,6 +467,8 @@
   }
 
   private void writeClassDefItem(DexProgramClass clazz) {
+    desugaredLibraryCodeToKeep.recordHierarchyOf(clazz);
+
     dest.putInt(mapping.getOffsetFor(clazz.type));
     dest.putInt(clazz.accessFlags.getAsDexAccessFlags());
     dest.putInt(
@@ -659,10 +661,6 @@
 
   private void writeClassData(DexProgramClass clazz) {
     assert clazz.hasMethodsOrFields();
-    desugaredLibraryCodeToKeep.recordClassAllAccesses(clazz.superType);
-    for (DexType itf : clazz.interfaces.values) {
-      desugaredLibraryCodeToKeep.recordClassAllAccesses(itf);
-    }
     mixedSectionOffsets.setOffsetFor(clazz, dest.position());
     dest.putUleb128(clazz.staticFields().size());
     dest.putUleb128(clazz.instanceFields().size());
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibraryEmptySubclassInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibraryEmptySubclassInterfaceTest.java
new file mode 100644
index 0000000..0eab37e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibraryEmptySubclassInterfaceTest.java
@@ -0,0 +1,104 @@
+// Copyright (c) 2020, 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.desugar.desugaredlibrary;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class LibraryEmptySubclassInterfaceTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public LibraryEmptySubclassInterfaceTest(
+      boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8()
+        .addInnerClasses(LibraryEmptySubclassInterfaceTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines(getResult());
+    assertExpectedKeepRules(keepRuleConsumer);
+  }
+
+  private void assertExpectedKeepRules(KeepRuleConsumer keepRuleConsumer) {
+    if (!requiresEmulatedInterfaceCoreLibDesugaring(parameters)) {
+      return;
+    }
+    String keepRules = keepRuleConsumer.get();
+    assertThat(keepRules, containsString("-keep class j$.util.Map"));
+    assertThat(keepRules, containsString("-keep class j$.util.concurrent.ConcurrentHashMap"));
+    assertThat(keepRules, containsString("-keep class j$.util.concurrent.ConcurrentMap"));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(Backend.DEX)
+        .addInnerClasses(LibraryEmptySubclassInterfaceTest.class)
+        .addKeepMainRule(Executor.class)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines(getResult());
+    assertExpectedKeepRules(keepRuleConsumer);
+  }
+
+  private String getResult() {
+    return requiresEmulatedInterfaceCoreLibDesugaring(parameters)
+        ? "class j$.util.concurrent.ConcurrentHashMap"
+        : "class java.util.concurrent.ConcurrentHashMap";
+  }
+
+  @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
+  static class Executor {
+
+    public static void main(String[] args) {
+      System.out.println(NullableConcurrentHashMap.class.getSuperclass());
+    }
+  }
+
+  static class NullableConcurrentHashMap<K, V> extends ConcurrentHashMap<K, V> {
+    NullableConcurrentHashMap() {
+      super();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java
index 12073e3..6e300ba 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java
@@ -25,7 +25,6 @@
 import java.util.concurrent.ConcurrentMap;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -70,7 +69,6 @@
 
   @Test
   public void testCustomCollectionR8() throws Exception {
-    Assume.assumeFalse("TODO(b/162437880)", shrinkDesugaredLibrary);
     KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
     String stdOut =
         testForR8(Backend.DEX)