Version 2.1.70

Cherry pick: Desugared library: GSON support
CL: https://r8-review.googlesource.com/c/r8/+/54760

Cherry pick: Desugared library: fix test expectation
CL: https://r8-review.googlesource.com/c/r8/+/54924

Cherry-pick: Base clean up of parsing generic signatures
CL: https://r8-review.googlesource.com/c/r8/+/54144

Cherry-pick: Add Generic Signature printer to convert generic signatures
CL: https://r8-review.googlesource.com/c/r8/+/54362

- L8 now clears all attributes but the signature by default
- Interface duplication now parses the signature attribute, modifies it,
  and writes it back instead of directly editing the DexClass
  classSignature field which did not exist in 2.1.

Bug: 167649682
Change-Id: Idf298097619007de382aa7051fb6da514abc0a9a
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index a496472..0a23b2c 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "2.1.69";
+  public static final String LABEL = "2.1.70";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 1c9b40c..2b75c58 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.kotlin.KotlinClassLevelInfo;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
@@ -329,6 +330,20 @@
     assert verifyNoDuplicateFields();
   }
 
+  public ClassSignature getClassSignature(AppView<?> appView) {
+    for (DexAnnotation annotation : annotations().annotations) {
+      if (DexAnnotation.isSignatureAnnotation(annotation, appView.dexItemFactory())) {
+        return GenericSignature.parseClassSignature(
+            type.getName(),
+            DexAnnotation.getSignature(annotation),
+            origin,
+            appView.dexItemFactory(),
+            appView.options().reporter);
+      }
+    }
+    return ClassSignature.NO_CLASS_SIGNATURE;
+  }
+
   private boolean verifyCorrectnessOfFieldHolder(DexEncodedField field) {
     assert field.holder() == type
         : "Expected field `"
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index a211636..b1c621c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -11,11 +11,14 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
 import com.android.tools.r8.kotlin.KotlinClassLevelInfo;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.TraversalContinuation;
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -443,33 +446,71 @@
     methodCollection.addDirectMethod(directMethod);
   }
 
-  public void addExtraInterfaces(List<DexType> extraInterfaces, DexItemFactory factory) {
+  public void replaceInterfaces(List<ClassTypeSignature> newInterfaces, AppView<?> appView) {
+    if (newInterfaces.isEmpty()) {
+      return;
+    }
+    clearInterfaces(appView);
+    addExtraInterfaces(newInterfaces, appView);
+  }
+
+  private void clearInterfaces(AppView<?> appView) {
+    interfaces = DexTypeList.empty();
+    ClassSignature classSignature = getClassSignature(appView);
+    if (classSignature.hasNoSignature()) {
+      return;
+    }
+    replaceSignatureAnnotation(
+        new ClassSignature(
+            classSignature.formalTypeParameters,
+            classSignature.superClassSignature,
+            ImmutableList.of()),
+        appView.dexItemFactory());
+  }
+
+  public void addExtraInterfaces(List<ClassTypeSignature> extraInterfaces, AppView<?> appView) {
     if (extraInterfaces.isEmpty()) {
       return;
     }
     addExtraInterfacesToInterfacesArray(extraInterfaces);
-    addExtraInterfacesToSignatureAnnotationIfPresent(extraInterfaces, factory);
+    addExtraInterfacesToSignatureAnnotationIfPresent(extraInterfaces, appView);
   }
 
-  private void addExtraInterfacesToInterfacesArray(List<DexType> extraInterfaces) {
+  private void addExtraInterfacesToInterfacesArray(List<ClassTypeSignature> extraInterfaces) {
     DexType[] newInterfaces =
         Arrays.copyOf(interfaces.values, interfaces.size() + extraInterfaces.size());
     for (int i = interfaces.size(); i < newInterfaces.length; i++) {
-      newInterfaces[i] = extraInterfaces.get(i - interfaces.size());
+      newInterfaces[i] = extraInterfaces.get(i - interfaces.size()).type();
     }
     interfaces = new DexTypeList(newInterfaces);
   }
 
   private void addExtraInterfacesToSignatureAnnotationIfPresent(
-      List<DexType> extraInterfaces, DexItemFactory factory) {
+      List<ClassTypeSignature> extraInterfaces, AppView<?> appView) {
     // We need to introduce in the dalvik.annotation.Signature annotation the extra interfaces.
-    // At this point we cheat and pretend the extraInterfaces simply don't use any generic types.
+    ClassSignature classSignature = getClassSignature(appView);
+    if (classSignature.hasNoSignature()) {
+      return;
+    }
+    ImmutableList.Builder<ClassTypeSignature> interfacesBuilder =
+        ImmutableList.<ClassTypeSignature>builder().addAll(classSignature.superInterfaceSignatures);
+    interfacesBuilder.addAll(extraInterfaces);
+    replaceSignatureAnnotation(
+        new ClassSignature(
+            classSignature.formalTypeParameters,
+            classSignature.superClassSignature,
+            interfacesBuilder.build()),
+        appView.dexItemFactory());
+  }
+
+  private void replaceSignatureAnnotation(ClassSignature classSignature, DexItemFactory factory) {
     DexAnnotation[] annotations = annotations().annotations;
     for (int i = 0; i < annotations.length; i++) {
       DexAnnotation annotation = annotations[i];
       if (DexAnnotation.isSignatureAnnotation(annotation, factory)) {
         DexAnnotation[] rewrittenAnnotations = annotations.clone();
-        rewrittenAnnotations[i] = rewriteSignatureAnnotation(annotation, extraInterfaces, factory);
+        rewrittenAnnotations[i] =
+            DexAnnotation.createSignatureAnnotation(classSignature.toString(), factory);
         setAnnotations(new DexAnnotationSet(rewrittenAnnotations));
         // There is at most one signature annotation, so we can return here.
         return;
@@ -477,14 +518,16 @@
     }
   }
 
-  private DexAnnotation rewriteSignatureAnnotation(
-      DexAnnotation annotation, List<DexType> extraInterfaces, DexItemFactory factory) {
-    String signature = DexAnnotation.getSignature(annotation);
-    StringBuilder newSignatureBuilder = new StringBuilder(signature);
-    for (DexType extraInterface : extraInterfaces) {
-      newSignatureBuilder.append(extraInterface.descriptor.toString());
+  public void clearAnnotationsButSignature(DexItemFactory factory) {
+    DexAnnotation[] annotations = annotations().annotations;
+    for (DexAnnotation annotation : annotations) {
+      if (DexAnnotation.isSignatureAnnotation(annotation, factory)) {
+        setAnnotations(new DexAnnotationSet(new DexAnnotation[] {annotation}));
+        // There is at most one signature annotation, so we can return here.
+        return;
+      }
     }
-    return DexAnnotation.createSignatureAnnotation(newSignatureBuilder.toString(), factory);
+    clearAnnotations();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignature.java b/src/main/java/com/android/tools/r8/graph/GenericSignature.java
index 3b4d284..9c34474 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignature.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignature.java
@@ -5,15 +5,18 @@
 
 import static com.android.tools.r8.utils.DescriptorUtils.getClassBinaryNameFromDescriptor;
 import static com.android.tools.r8.utils.DescriptorUtils.getDescriptorFromClassBinaryName;
+import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.ImmutableList;
 import java.lang.reflect.GenericSignatureFormatError;
 import java.nio.CharBuffer;
 import java.util.List;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * Internal encoding of the generics signature attribute as defined by JVMS 7 $ 4.3.4.
@@ -95,32 +98,42 @@
  */
 public class GenericSignature {
 
-  private static final List<FormalTypeParameter> EMPTY_TYPE_PARAMS = ImmutableList.of();
+  static final List<FormalTypeParameter> EMPTY_TYPE_PARAMS = ImmutableList.of();
+  static final List<FieldTypeSignature> EMPTY_TYPE_ARGUMENTS = ImmutableList.of();
+  static final List<ClassTypeSignature> EMPTY_SUPER_INTERFACES = ImmutableList.of();
+  static final List<TypeSignature> EMPTY_TYPE_SIGNATURES = ImmutableList.of();
 
   interface DexDefinitionSignature<T extends DexDefinition> {
+
     default boolean isClassSignature() {
       return false;
     }
 
+    default boolean isFieldTypeSignature() {
+      return false;
+    }
+
+    default boolean isMethodTypeSignature() {
+      return false;
+    }
+
     default ClassSignature asClassSignature() {
       return null;
     }
 
-    default boolean isFieldTypeSignature() {
-      return false;
-    }
-
     default FieldTypeSignature asFieldTypeSignature() {
       return null;
     }
 
-    default boolean isMethodTypeSignature() {
-      return false;
-    }
-
     default MethodTypeSignature asMethodTypeSignature() {
       return null;
     }
+
+    boolean hasSignature();
+
+    default boolean hasNoSignature() {
+      return !hasSignature();
+    }
   }
 
   public static class FormalTypeParameter {
@@ -147,14 +160,22 @@
     public List<FieldTypeSignature> getInterfaceBounds() {
       return interfaceBounds;
     }
+
+    public void visit(GenericSignatureVisitor visitor) {
+      visitor.visitClassBound(classBound);
+      if (interfaceBounds == null) {
+        return;
+      }
+      for (FieldTypeSignature interfaceBound : interfaceBounds) {
+        visitor.visitInterfaceBound(interfaceBound);
+      }
+    }
   }
 
   public static class ClassSignature implements DexDefinitionSignature<DexClass> {
-    static final ClassSignature UNKNOWN_CLASS_SIGNATURE =
-        new ClassSignature(
-            ImmutableList.of(),
-            ClassTypeSignature.UNKNOWN_CLASS_TYPE_SIGNATURE,
-            ImmutableList.of());
+
+    public static final ClassSignature NO_CLASS_SIGNATURE =
+        new ClassSignature(EMPTY_TYPE_PARAMS, NO_FIELD_TYPE_SIGNATURE, EMPTY_SUPER_INTERFACES);
 
     final List<FormalTypeParameter> formalTypeParameters;
     final ClassTypeSignature superClassSignature;
@@ -181,6 +202,11 @@
     }
 
     @Override
+    public boolean hasSignature() {
+      return this != NO_CLASS_SIGNATURE;
+    }
+
+    @Override
     public boolean isClassSignature() {
       return true;
     }
@@ -189,9 +215,37 @@
     public ClassSignature asClassSignature() {
       return this;
     }
+
+    public void visit(GenericSignatureVisitor visitor) {
+      visitor.visitFormalTypeParameters(formalTypeParameters);
+      visitor.visitSuperClass(superClassSignature);
+      for (ClassTypeSignature superInterface : superInterfaceSignatures) {
+        visitor.visitSuperInterface(superInterface);
+      }
+    }
+
+    public String toRenamedString(NamingLens namingLens, Predicate<DexType> isTypeMissing) {
+      if (hasNoSignature()) {
+        return null;
+      }
+      GenericSignaturePrinter genericSignaturePrinter =
+          new GenericSignaturePrinter(namingLens, isTypeMissing);
+      genericSignaturePrinter.visitClassSignature(this);
+      return genericSignaturePrinter.toString();
+    }
+
+    @Override
+    public String toString() {
+      return toRenamedString(NamingLens.getIdentityLens(), alwaysTrue());
+    }
+
+    public static ClassSignature noSignature() {
+      return NO_CLASS_SIGNATURE;
+    }
   }
 
   public abstract static class TypeSignature {
+
     public boolean isFieldTypeSignature() {
       return false;
     }
@@ -208,11 +262,7 @@
       return null;
     }
 
-    public TypeSignature toArrayTypeSignature(AppView<?> appView) {
-      return null;
-    }
-
-    public TypeSignature toArrayElementTypeSignature(AppView<?> appView) {
+    public ArrayTypeSignature toArrayTypeSignature() {
       return null;
     }
   }
@@ -275,8 +325,9 @@
       return null;
     }
 
-    public boolean isUnknown() {
-      return this == ClassTypeSignature.UNKNOWN_CLASS_TYPE_SIGNATURE;
+    @Override
+    public boolean hasSignature() {
+      return this != GenericSignature.NO_FIELD_TYPE_SIGNATURE;
     }
 
     public abstract FieldTypeSignature asArgument(WildcardIndicator indicator);
@@ -284,12 +335,30 @@
     public boolean isStar() {
       return false;
     }
+
+    public String toRenamedString(NamingLens namingLens, Predicate<DexType> isTypeMissing) {
+      if (hasNoSignature()) {
+        return null;
+      }
+      GenericSignaturePrinter genericSignaturePrinter =
+          new GenericSignaturePrinter(namingLens, isTypeMissing);
+      genericSignaturePrinter.visitTypeSignature(this);
+      return genericSignaturePrinter.toString();
+    }
+
+    @Override
+    public String toString() {
+      return toRenamedString(NamingLens.getIdentityLens(), alwaysTrue());
+    }
+
+    public static FieldTypeSignature noSignature() {
+      return NO_FIELD_TYPE_SIGNATURE;
+    }
   }
 
-  private static final class StarFieldTypeSignature extends FieldTypeSignature {
+  static final class StarFieldTypeSignature extends FieldTypeSignature {
 
-    private static final StarFieldTypeSignature STAR_FIELD_TYPE_SIGNATURE =
-        new StarFieldTypeSignature();
+    static final StarFieldTypeSignature STAR_FIELD_TYPE_SIGNATURE = new StarFieldTypeSignature();
 
     private StarFieldTypeSignature() {
       super(WildcardIndicator.NONE);
@@ -306,11 +375,13 @@
     }
   }
 
+  private static final ClassTypeSignature NO_FIELD_TYPE_SIGNATURE =
+      new ClassTypeSignature(DexItemFactory.nullValueType, EMPTY_TYPE_ARGUMENTS);
+
   public static class ClassTypeSignature extends FieldTypeSignature {
-    static final ClassTypeSignature UNKNOWN_CLASS_TYPE_SIGNATURE =
-        new ClassTypeSignature(DexItemFactory.nullValueType, ImmutableList.of());
 
     final DexType type;
+
     // E.g., for Map<K, V>, a signature will indicate what types are for K and V.
     // Note that this could be nested, e.g., Map<K, Consumer<V>>.
     final List<FieldTypeSignature> typeArguments;
@@ -320,7 +391,11 @@
     ClassTypeSignature enclosingTypeSignature;
     ClassTypeSignature innerTypeSignature;
 
-    ClassTypeSignature(DexType type, List<FieldTypeSignature> typeArguments) {
+    public ClassTypeSignature(DexType type) {
+      this(type, EMPTY_TYPE_ARGUMENTS);
+    }
+
+    public ClassTypeSignature(DexType type, List<FieldTypeSignature> typeArguments) {
       this(type, typeArguments, WildcardIndicator.NOT_AN_ARGUMENT);
     }
 
@@ -361,8 +436,12 @@
       return argument;
     }
 
+    public boolean isNoSignature() {
+      return this == NO_FIELD_TYPE_SIGNATURE;
+    }
+
     @Override
-    public ArrayTypeSignature toArrayTypeSignature(AppView<?> appView) {
+    public ArrayTypeSignature toArrayTypeSignature() {
       return new ArrayTypeSignature(this);
     }
 
@@ -371,6 +450,13 @@
       outer.innerTypeSignature = inner;
       inner.enclosingTypeSignature = outer;
     }
+
+    public void visit(GenericSignatureVisitor visitor) {
+      visitor.visitTypeArguments(typeArguments);
+      if (innerTypeSignature != null) {
+        visitor.visitSimpleClass(innerTypeSignature);
+      }
+    }
   }
 
   public static class ArrayTypeSignature extends FieldTypeSignature {
@@ -408,13 +494,12 @@
     }
 
     @Override
-    public TypeSignature toArrayTypeSignature(AppView<?> appView) {
+    public ArrayTypeSignature toArrayTypeSignature() {
       return new ArrayTypeSignature(this);
     }
 
-    @Override
-    public TypeSignature toArrayElementTypeSignature(AppView<?> appView) {
-      return elementSignature;
+    public void visit(GenericSignatureVisitor visitor) {
+      visitor.visitTypeSignature(elementSignature);
     }
   }
 
@@ -449,7 +534,7 @@
     }
 
     @Override
-    public ArrayTypeSignature toArrayTypeSignature(AppView<?> appView) {
+    public ArrayTypeSignature toArrayTypeSignature() {
       return new ArrayTypeSignature(this);
     }
 
@@ -479,7 +564,7 @@
     }
 
     @Override
-    public ArrayTypeSignature toArrayTypeSignature(AppView<?> appView) {
+    public ArrayTypeSignature toArrayTypeSignature() {
       assert !type.isVoidType();
       return new ArrayTypeSignature(this);
     }
@@ -505,15 +590,20 @@
   }
 
   public static class MethodTypeSignature implements DexDefinitionSignature<DexEncodedMethod> {
-    static final MethodTypeSignature UNKNOWN_METHOD_TYPE_SIGNATURE =
+
+    private static final MethodTypeSignature NO_METHOD_TYPE_SIGNATURE =
         new MethodTypeSignature(
-            ImmutableList.of(), ImmutableList.of(), ReturnType.VOID, ImmutableList.of());
+            EMPTY_TYPE_PARAMS, EMPTY_TYPE_SIGNATURES, ReturnType.VOID, EMPTY_TYPE_SIGNATURES);
 
     final List<FormalTypeParameter> formalTypeParameters;
     final List<TypeSignature> typeSignatures;
     final ReturnType returnType;
     final List<TypeSignature> throwsSignatures;
 
+    public static MethodTypeSignature noSignature() {
+      return NO_METHOD_TYPE_SIGNATURE;
+    }
+
     MethodTypeSignature(
         final List<FormalTypeParameter> formalTypeParameters,
         List<TypeSignature> typeSignatures,
@@ -550,109 +640,100 @@
     }
 
     @Override
+    public boolean hasSignature() {
+      return this != NO_METHOD_TYPE_SIGNATURE;
+    }
+
+    @Override
     public MethodTypeSignature asMethodTypeSignature() {
       return this;
     }
 
+    public void visit(GenericSignatureVisitor visitor) {
+      visitor.visitFormalTypeParameters(formalTypeParameters);
+      visitor.visitMethodTypeSignatures(typeSignatures);
+      visitor.visitReturnType(returnType);
+      visitor.visitThrowsSignatures(throwsSignatures);
+    }
+
     public List<FormalTypeParameter> getFormalTypeParameters() {
       return formalTypeParameters;
     }
-  }
 
-  enum Kind {
-    CLASS, FIELD, METHOD;
-
-    static Kind fromDexDefinition(DexDefinition definition) {
-      if (definition.isDexClass()) {
-        return CLASS;
+    public String toRenamedString(NamingLens namingLens, Predicate<DexType> isTypeMissing) {
+      if (hasNoSignature()) {
+        return null;
       }
-      if (definition.isDexEncodedField()) {
-        return FIELD;
-      }
-      if (definition.isDexEncodedMethod()) {
-        return METHOD;
-      }
-      throw new Unreachable("Unexpected kind of DexDefinition: " + definition);
+      GenericSignaturePrinter genericSignaturePrinter =
+          new GenericSignaturePrinter(namingLens, isTypeMissing);
+      genericSignaturePrinter.visitMethodSignature(this);
+      return genericSignaturePrinter.toString();
     }
 
-    Function<String, ? extends DexDefinitionSignature<? extends DexDefinition>>
-        parserMethod(Parser parser) {
-      switch (this) {
-        case CLASS:
-          return parser::parseClassSignature;
-        case FIELD:
-          return parser::parseFieldTypeSignature;
-        case METHOD:
-          return parser::parseMethodTypeSignature;
-      }
-      throw new Unreachable("Unexpected kind: " + this);
+    @Override
+    public String toString() {
+      return toRenamedString(NamingLens.getIdentityLens(), alwaysTrue());
+    }
+  }
+
+  public static ClassSignature parseClassSignature(
+      String className,
+      String signature,
+      Origin origin,
+      DexItemFactory factory,
+      Reporter reporter) {
+    if (signature == null || signature.isEmpty()) {
+      return ClassSignature.NO_CLASS_SIGNATURE;
+    }
+    Parser parser = new Parser(factory);
+    try {
+      return parser.parseClassSignature(signature);
+    } catch (GenericSignatureFormatError e) {
+      reporter.warning(
+          GenericSignatureDiagnostic.invalidClassSignature(signature, className, origin, e));
+      return ClassSignature.NO_CLASS_SIGNATURE;
+    }
+  }
+
+  public static FieldTypeSignature parseFieldTypeSignature(
+      String fieldName,
+      String signature,
+      Origin origin,
+      DexItemFactory factory,
+      Reporter reporter) {
+    if (signature == null || signature.isEmpty()) {
+      return NO_FIELD_TYPE_SIGNATURE;
+    }
+    Parser parser = new Parser(factory);
+    try {
+      return parser.parseFieldTypeSignature(signature);
+    } catch (GenericSignatureFormatError e) {
+      reporter.warning(
+          GenericSignatureDiagnostic.invalidFieldSignature(signature, fieldName, origin, e));
+      return GenericSignature.NO_FIELD_TYPE_SIGNATURE;
+    }
+  }
+
+  public static MethodTypeSignature parseMethodSignature(
+      String methodName,
+      String signature,
+      Origin origin,
+      DexItemFactory factory,
+      Reporter reporter) {
+    if (signature == null || signature.isEmpty()) {
+      return MethodTypeSignature.NO_METHOD_TYPE_SIGNATURE;
+    }
+    Parser parser = new Parser(factory);
+    try {
+      return parser.parseMethodTypeSignature(signature);
+    } catch (GenericSignatureFormatError e) {
+      reporter.warning(
+          GenericSignatureDiagnostic.invalidMethodSignature(signature, methodName, origin, e));
+      return MethodTypeSignature.NO_METHOD_TYPE_SIGNATURE;
     }
   }
 
   public static class Parser {
-    // TODO(b/129925954): Can we merge variants of to*Signature below and just expose
-    //  type-parameterized version of this, like
-    //    <T extends DexDefinitionSignature<?>> T toGenericSignature
-    //  without unchecked cast?
-    private static DexDefinitionSignature<? extends DexDefinition> toGenericSignature(
-        DexClass currentClassContext,
-        DexDefinition definition,
-        AppView<AppInfoWithLiveness> appView) {
-      DexAnnotationSet annotations = definition.annotations();
-      if (annotations.annotations.length == 0) {
-        return null;
-      }
-      for (int i = 0; i < annotations.annotations.length; i++) {
-        DexAnnotation annotation = annotations.annotations[i];
-        if (!DexAnnotation.isSignatureAnnotation(annotation, appView.dexItemFactory())) {
-          continue;
-        }
-        Kind kind = Kind.fromDexDefinition(definition);
-        Parser parser = new Parser(currentClassContext, appView);
-        String signature = DexAnnotation.getSignature(annotation);
-        try {
-          return kind.parserMethod(parser).apply(signature);
-        } catch (GenericSignatureFormatError e) {
-          appView.options().warningInvalidSignature(
-              definition, currentClassContext.getOrigin(), signature, e);
-        }
-      }
-      return null;
-    }
-
-    public static ClassSignature toClassSignature(
-        DexClass clazz, AppView<AppInfoWithLiveness> appView) {
-      DexDefinitionSignature<?> signature = toGenericSignature(clazz, clazz, appView);
-      if (signature != null) {
-        assert signature.isClassSignature();
-        return signature.asClassSignature();
-      }
-      return ClassSignature.UNKNOWN_CLASS_SIGNATURE;
-    }
-
-    public static FieldTypeSignature toFieldTypeSignature(
-        DexEncodedField field, AppView<AppInfoWithLiveness> appView) {
-      DexClass currentClassContext = appView.definitionFor(field.holder());
-      DexDefinitionSignature<?> signature =
-          toGenericSignature(currentClassContext, field, appView);
-      if (signature != null) {
-        assert signature.isFieldTypeSignature();
-        return signature.asFieldTypeSignature();
-      }
-      return ClassTypeSignature.UNKNOWN_CLASS_TYPE_SIGNATURE;
-    }
-
-    public static MethodTypeSignature toMethodTypeSignature(
-        DexEncodedMethod method, AppView<AppInfoWithLiveness> appView) {
-      DexClass currentClassContext = appView.definitionFor(method.holder());
-      DexDefinitionSignature<?> signature =
-          toGenericSignature(currentClassContext, method, appView);
-      if (signature != null) {
-        assert signature.isMethodTypeSignature();
-        return signature.asMethodTypeSignature();
-      }
-      return MethodTypeSignature.UNKNOWN_METHOD_TYPE_SIGNATURE;
-    }
 
     /*
      * Parser:
@@ -672,9 +753,8 @@
 
     private int pos;
 
-    private Parser(DexClass currentClassContext, AppView<AppInfoWithLiveness> appView) {
-      this.currentClassContext = currentClassContext;
-      this.appView = appView;
+    private Parser(DexItemFactory factory) {
+      this.factory = factory;
     }
 
     ClassSignature parseClassSignature(String signature) {
@@ -708,7 +788,7 @@
     FieldTypeSignature parseFieldTypeSignature(String signature) {
       try {
         setInput(signature);
-        return parseFieldTypeSignature(ParserPosition.MEMBER_ANNOTATION);
+        return parseFieldTypeSignature();
       } catch (GenericSignatureFormatError e) {
         throw e;
       } catch (Throwable t) {
@@ -732,40 +812,11 @@
     // Action:
     //
 
-    enum ParserPosition {
-      CLASS_SUPER_OR_INTERFACE_ANNOTATION,
-      ENCLOSING_INNER_OR_TYPE_ANNOTATION,
-      MEMBER_ANNOTATION
-    }
+    private final DexItemFactory factory;
 
-    private final AppView<AppInfoWithLiveness> appView;
-    private final DexClass currentClassContext;
-    private DexType lastWrittenType = null;
-
-    private DexType parsedTypeName(String name, ParserPosition parserPosition) {
-      if (parserPosition == ParserPosition.ENCLOSING_INNER_OR_TYPE_ANNOTATION
-          && lastWrittenType == null) {
-        // We are writing type-arguments for a merged class.
-        return null;
-      }
+    private DexType parsedTypeName(String name) {
       String originalDescriptor = getDescriptorFromClassBinaryName(name);
-      DexType type =
-          appView.graphLense().lookupType(appView.dexItemFactory().createType(originalDescriptor));
-      if (appView.appInfo().wasPruned(type)) {
-        type = appView.dexItemFactory().objectType;
-      }
-      if (parserPosition == ParserPosition.CLASS_SUPER_OR_INTERFACE_ANNOTATION
-          && currentClassContext != null) {
-        // We may have merged the type down to the current class type.
-        DexString classDescriptor = currentClassContext.type.descriptor;
-        if (!originalDescriptor.equals(classDescriptor.toString())
-            && type.descriptor.equals(classDescriptor)) {
-          lastWrittenType = null;
-          return type;
-        }
-      }
-      lastWrittenType = type;
-      return type;
+      return factory.createType(originalDescriptor);
     }
 
     private DexType parsedInnerTypeName(DexType enclosingType, String name) {
@@ -775,15 +826,11 @@
       }
       assert enclosingType.isClassType();
       String enclosingDescriptor = enclosingType.toDescriptorString();
-      DexType type =
-          appView
-              .dexItemFactory()
-              .createType(
-                  getDescriptorFromClassBinaryName(
-                      getClassBinaryNameFromDescriptor(enclosingDescriptor)
-                          + DescriptorUtils.INNER_CLASS_SEPARATOR
-                          + name));
-      return appView.graphLense().lookupType(type);
+      return factory.createType(
+          getDescriptorFromClassBinaryName(
+              getClassBinaryNameFromDescriptor(enclosingDescriptor)
+                  + DescriptorUtils.INNER_CLASS_SEPARATOR
+                  + name));
     }
 
     //
@@ -796,13 +843,12 @@
       List<FormalTypeParameter> formalTypeParameters = parseOptFormalTypeParameters();
 
       // SuperclassSignature ::= ClassTypeSignature.
-      ClassTypeSignature superClassSignature =
-          parseClassTypeSignature(ParserPosition.CLASS_SUPER_OR_INTERFACE_ANNOTATION);
+      ClassTypeSignature superClassSignature = parseClassTypeSignature();
 
       ImmutableList.Builder<ClassTypeSignature> builder = ImmutableList.builder();
       while (symbol > 0) {
         // SuperinterfaceSignature ::= ClassTypeSignature.
-        builder.add(parseClassTypeSignature(ParserPosition.CLASS_SUPER_OR_INTERFACE_ANNOTATION));
+        builder.add(parseClassTypeSignature());
       }
 
       return new ClassSignature(formalTypeParameters, superClassSignature, builder.build());
@@ -833,9 +879,9 @@
       // ClassBound ::= ":" FieldTypeSignature?.
       expect(':');
 
-      FieldTypeSignature classBound = ClassTypeSignature.UNKNOWN_CLASS_TYPE_SIGNATURE;
+      FieldTypeSignature classBound = GenericSignature.NO_FIELD_TYPE_SIGNATURE;
       if (symbol == 'L' || symbol == '[' || symbol == 'T') {
-        classBound = parseFieldTypeSignature(ParserPosition.MEMBER_ANNOTATION);
+        classBound = parseFieldTypeSignature();
       }
 
       // Only build the interfacebound builder, which is uncommon, if we actually see an interface.
@@ -846,7 +892,7 @@
           builder = ImmutableList.builder();
         }
         scanSymbol();
-        builder.add(parseFieldTypeSignature(ParserPosition.MEMBER_ANNOTATION));
+        builder.add(parseFieldTypeSignature());
       }
       if (builder == null) {
         return new FormalTypeParameter(typeParameterIdentifier, classBound, null);
@@ -854,16 +900,16 @@
       return new FormalTypeParameter(typeParameterIdentifier, classBound, builder.build());
     }
 
-    private FieldTypeSignature parseFieldTypeSignature(ParserPosition parserPosition) {
+    private FieldTypeSignature parseFieldTypeSignature() {
       // FieldTypeSignature ::= ClassTypeSignature | ArrayTypeSignature | TypeVariableSignature.
       switch (symbol) {
         case 'L':
-          return parseClassTypeSignature(parserPosition);
+          return parseClassTypeSignature();
         case '[':
           // ArrayTypeSignature ::= "[" TypeSignature.
           scanSymbol();
-          TypeSignature baseTypeSignature = updateTypeSignature(parserPosition);
-          return baseTypeSignature.toArrayTypeSignature(appView).asFieldTypeSignature();
+          TypeSignature baseTypeSignature = updateTypeSignature();
+          return baseTypeSignature.toArrayTypeSignature().asFieldTypeSignature();
         case 'T':
           return updateTypeVariableSignature();
         default:
@@ -872,7 +918,7 @@
       throw new Unreachable("Either FieldTypeSignature is returned or a parse error is thrown.");
     }
 
-    private ClassTypeSignature parseClassTypeSignature(ParserPosition parserPosition) {
+    private ClassTypeSignature parseClassTypeSignature() {
       // ClassTypeSignature ::=
       //   "L" (Identifier "/")* Identifier TypeArguments? ("." Identifier TypeArguments?)* ";".
       expect('L');
@@ -888,11 +934,12 @@
       }
 
       qualIdent.append(this.identifier);
-      DexType parsedEnclosingType = parsedTypeName(qualIdent.toString(), parserPosition);
+      DexType parsedEnclosingType = parsedTypeName(qualIdent.toString());
 
       List<FieldTypeSignature> typeArguments = updateOptTypeArguments();
       ClassTypeSignature outerMostTypeSignature =
-          new ClassTypeSignature(parsedEnclosingType, typeArguments);
+          new ClassTypeSignature(
+              parsedEnclosingType, typeArguments.isEmpty() ? EMPTY_TYPE_ARGUMENTS : typeArguments);
 
       ClassTypeSignature outerTypeSignature = outerMostTypeSignature;
       ClassTypeSignature innerTypeSignature;
@@ -903,7 +950,10 @@
         assert identifier != null;
         parsedEnclosingType = parsedInnerTypeName(parsedEnclosingType, identifier);
         typeArguments = updateOptTypeArguments();
-        innerTypeSignature = new ClassTypeSignature(parsedEnclosingType, typeArguments);
+        innerTypeSignature =
+            new ClassTypeSignature(
+                parsedEnclosingType,
+                typeArguments.isEmpty() ? EMPTY_TYPE_ARGUMENTS : typeArguments);
         ClassTypeSignature.link(outerTypeSignature, innerTypeSignature);
         outerTypeSignature = innerTypeSignature;
       }
@@ -935,15 +985,12 @@
         return StarFieldTypeSignature.STAR_FIELD_TYPE_SIGNATURE;
       } else if (symbol == '+') {
         scanSymbol();
-        return parseFieldTypeSignature(ParserPosition.ENCLOSING_INNER_OR_TYPE_ANNOTATION)
-            .asArgument(WildcardIndicator.POSITIVE);
+        return parseFieldTypeSignature().asArgument(WildcardIndicator.POSITIVE);
       } else if (symbol == '-') {
         scanSymbol();
-        return parseFieldTypeSignature(ParserPosition.ENCLOSING_INNER_OR_TYPE_ANNOTATION)
-            .asArgument(WildcardIndicator.NEGATIVE);
+        return parseFieldTypeSignature().asArgument(WildcardIndicator.NEGATIVE);
       } else {
-        return parseFieldTypeSignature(ParserPosition.ENCLOSING_INNER_OR_TYPE_ANNOTATION)
-            .asArgument(WildcardIndicator.NONE);
+        return parseFieldTypeSignature().asArgument(WildcardIndicator.NONE);
       }
     }
 
@@ -958,7 +1005,7 @@
       return new TypeVariableSignature(identifier);
     }
 
-    private TypeSignature updateTypeSignature(ParserPosition parserPosition) {
+    private TypeSignature updateTypeSignature() {
       switch (symbol) {
         case 'B':
         case 'C':
@@ -968,13 +1015,13 @@
         case 'J':
         case 'S':
         case 'Z':
-          DexType type = appView.dexItemFactory().createType(String.valueOf(symbol));
+          DexType type = factory.createType(String.valueOf(symbol));
           BaseTypeSignature baseTypeSignature = new BaseTypeSignature(type);
           scanSymbol();
           return baseTypeSignature;
         default:
           // Not an elementary type, but a FieldTypeSignature.
-          return parseFieldTypeSignature(parserPosition);
+          return parseFieldTypeSignature();
       }
     }
 
@@ -987,7 +1034,7 @@
 
       ImmutableList.Builder<TypeSignature> parameterSignatureBuilder = ImmutableList.builder();
       while (symbol != ')' && (symbol > 0)) {
-        parameterSignatureBuilder.add(updateTypeSignature(ParserPosition.MEMBER_ANNOTATION));
+        parameterSignatureBuilder.add(updateTypeSignature());
       }
 
       expect(')');
@@ -998,12 +1045,11 @@
       if (symbol == '^') {
         do {
           scanSymbol();
-
           // ThrowsSignature ::= ("^" ClassTypeSignature) | ("^" TypeVariableSignature).
           if (symbol == 'T') {
             throwsSignatureBuilder.add(updateTypeVariableSignature());
           } else {
-            throwsSignatureBuilder.add(parseClassTypeSignature(ParserPosition.MEMBER_ANNOTATION));
+            throwsSignatureBuilder.add(parseClassTypeSignature());
           }
         } while (symbol == '^');
       }
@@ -1018,7 +1064,7 @@
     private ReturnType updateReturnType() {
       // ReturnType ::= TypeSignature | "V".
       if (symbol != 'V') {
-        return new ReturnType(updateTypeSignature(ParserPosition.MEMBER_ANNOTATION));
+        return new ReturnType(updateTypeSignature());
       } else {
         scanSymbol();
         return ReturnType.VOID;
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureDiagnostic.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureDiagnostic.java
new file mode 100644
index 0000000..fa95bce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureDiagnostic.java
@@ -0,0 +1,75 @@
+// 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.graph;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import java.lang.reflect.GenericSignatureFormatError;
+
+public class GenericSignatureDiagnostic implements Diagnostic {
+
+  private final Origin origin;
+  private final Position position;
+  private final String message;
+
+  GenericSignatureDiagnostic(Origin origin, Position position, String message) {
+    this.origin = origin;
+    this.position = position;
+    this.message = message;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return position;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+
+  static GenericSignatureDiagnostic invalidClassSignature(
+      String signature, String name, Origin origin, GenericSignatureFormatError error) {
+    return invalidSignature(signature, "class", name, origin, error);
+  }
+
+  static GenericSignatureDiagnostic invalidMethodSignature(
+      String signature, String name, Origin origin, GenericSignatureFormatError error) {
+    return invalidSignature(signature, "method", name, origin, error);
+  }
+
+  static GenericSignatureDiagnostic invalidFieldSignature(
+      String signature, String name, Origin origin, GenericSignatureFormatError error) {
+    return invalidSignature(signature, "field", name, origin, error);
+  }
+
+  private static GenericSignatureDiagnostic invalidSignature(
+      String signature,
+      String kind,
+      String name,
+      Origin origin,
+      GenericSignatureFormatError error) {
+    String message =
+        "Invalid signature '"
+            + signature
+            + "' for "
+            + kind
+            + " "
+            + name
+            + "."
+            + System.lineSeparator()
+            + "Signature is ignored and will not be present in the output."
+            + System.lineSeparator()
+            + "Parser error: "
+            + error.getMessage();
+    return new GenericSignatureDiagnostic(origin, Position.UNKNOWN, message);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignaturePrinter.java b/src/main/java/com/android/tools/r8/graph/GenericSignaturePrinter.java
new file mode 100644
index 0000000..f68675f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignaturePrinter.java
@@ -0,0 +1,184 @@
+// 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.graph;
+
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FormalTypeParameter;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.ReturnType;
+import com.android.tools.r8.graph.GenericSignature.TypeSignature;
+import com.android.tools.r8.graph.GenericSignature.WildcardIndicator;
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.util.List;
+import java.util.function.Predicate;
+
+public class GenericSignaturePrinter implements GenericSignatureVisitor {
+
+  private final NamingLens namingLens;
+  private final Predicate<DexType> isTypeMissing;
+
+  public GenericSignaturePrinter(NamingLens namingLens, Predicate<DexType> isTypeMissing) {
+    this.namingLens = namingLens;
+    this.isTypeMissing = isTypeMissing;
+  }
+
+  private final StringBuilder sb = new StringBuilder();
+
+  @Override
+  public void visitClassSignature(ClassSignature classSignature) {
+    classSignature.visit(this);
+  }
+
+  @Override
+  public void visitFieldTypeSignature(FieldTypeSignature fieldSignature) {
+    printFieldTypeSignature(fieldSignature, false);
+  }
+
+  @Override
+  public void visitMethodSignature(MethodTypeSignature methodSignature) {
+    methodSignature.visit(this);
+  }
+
+  @Override
+  public void visitMethodTypeSignatures(List<TypeSignature> typeSignatures) {
+    sb.append("(");
+    typeSignatures.forEach(this::visitTypeSignature);
+    sb.append(")");
+  }
+
+  @Override
+  public void visitReturnType(ReturnType returnType) {
+    if (returnType.isVoidDescriptor()) {
+      sb.append("V");
+    } else {
+      visitTypeSignature(returnType.typeSignature);
+    }
+  }
+
+  @Override
+  public void visitThrowsSignatures(List<TypeSignature> typeSignatures) {
+    for (TypeSignature typeSignature : typeSignatures) {
+      sb.append("^");
+      visitTypeSignature(typeSignature);
+    }
+  }
+
+  @Override
+  public void visitFormalTypeParameters(List<FormalTypeParameter> formalTypeParameters) {
+    if (formalTypeParameters.isEmpty()) {
+      return;
+    }
+    sb.append("<");
+    for (FormalTypeParameter formalTypeParameter : formalTypeParameters) {
+      sb.append(formalTypeParameter.name);
+      formalTypeParameter.visit(this);
+    }
+    sb.append(">");
+  }
+
+  @Override
+  public void visitClassBound(FieldTypeSignature fieldSignature) {
+    sb.append(":");
+    printFieldTypeSignature(fieldSignature, false);
+  }
+
+  @Override
+  public void visitInterfaceBound(FieldTypeSignature fieldSignature) {
+    sb.append(":");
+    printFieldTypeSignature(fieldSignature, false);
+  }
+
+  @Override
+  public void visitSuperClass(ClassTypeSignature classTypeSignature) {
+    printFieldTypeSignature(classTypeSignature, false);
+  }
+
+  @Override
+  public void visitSuperInterface(ClassTypeSignature classTypeSignature) {
+    printFieldTypeSignature(classTypeSignature, false);
+  }
+
+  @Override
+  public void visitTypeSignature(TypeSignature typeSignature) {
+    if (typeSignature.isBaseTypeSignature()) {
+      DexType type = typeSignature.asBaseTypeSignature().type;
+      sb.append(type.toDescriptorString());
+    } else {
+      printFieldTypeSignature(typeSignature.asFieldTypeSignature(), false);
+    }
+  }
+
+  @Override
+  public void visitSimpleClass(ClassTypeSignature classTypeSignature) {
+    printFieldTypeSignature(classTypeSignature, true);
+  }
+
+  @Override
+  public void visitTypeArguments(List<FieldTypeSignature> typeArguments) {
+    if (typeArguments.isEmpty()) {
+      return;
+    }
+    sb.append("<");
+    for (FieldTypeSignature typeArgument : typeArguments) {
+      WildcardIndicator wildcardIndicator = typeArgument.getWildcardIndicator();
+      if (wildcardIndicator != WildcardIndicator.NONE) {
+        assert wildcardIndicator != WildcardIndicator.NOT_AN_ARGUMENT;
+        sb.append(wildcardIndicator == WildcardIndicator.POSITIVE ? "+" : "-");
+      }
+      visitTypeSignature(typeArgument);
+    }
+    sb.append(">");
+  }
+
+  private void printFieldTypeSignature(
+      FieldTypeSignature fieldTypeSignature, boolean printingInner) {
+    // For inner member classes we only print the inner name and the type-arguments.
+    if (fieldTypeSignature.isStar()) {
+      sb.append("*");
+    } else if (fieldTypeSignature.isTypeVariableSignature()) {
+      sb.append("T").append(fieldTypeSignature.asTypeVariableSignature().typeVariable).append(";");
+    } else if (fieldTypeSignature.isArrayTypeSignature()) {
+      sb.append("[");
+      fieldTypeSignature.asArrayTypeSignature().visit(this);
+    } else {
+      assert fieldTypeSignature.isClassTypeSignature();
+      ClassTypeSignature classTypeSignature = fieldTypeSignature.asClassTypeSignature();
+      if (classTypeSignature.isNoSignature()) {
+        return;
+      }
+      String renamedString = namingLens.lookupDescriptor(classTypeSignature.type).toString();
+      if (!printingInner) {
+        sb.append("L").append(DescriptorUtils.getBinaryNameFromDescriptor(renamedString));
+      } else {
+        assert classTypeSignature.enclosingTypeSignature != null;
+        DexType enclosingType = classTypeSignature.enclosingTypeSignature.type;
+        String outerDescriptor = namingLens.lookupDescriptor(enclosingType).toString();
+        String innerClassName = DescriptorUtils.getInnerClassName(outerDescriptor, renamedString);
+        if (innerClassName == null && isTypeMissing.test(classTypeSignature.type)) {
+          assert renamedString.equals(classTypeSignature.type.toDescriptorString());
+          innerClassName =
+              DescriptorUtils.getInnerClassName(enclosingType.toDescriptorString(), renamedString);
+        }
+        if (innerClassName == null) {
+          // We can no longer encode the inner name in the generic signature.
+          return;
+        }
+        sb.append(".").append(innerClassName);
+      }
+      classTypeSignature.visit(this);
+      if (!printingInner) {
+        sb.append(";");
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    return sb.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureVisitor.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureVisitor.java
new file mode 100644
index 0000000..d3840dd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureVisitor.java
@@ -0,0 +1,74 @@
+// 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.graph;
+
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FormalTypeParameter;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.ReturnType;
+import com.android.tools.r8.graph.GenericSignature.TypeSignature;
+import java.util.List;
+
+public interface GenericSignatureVisitor {
+
+  default void visitClassSignature(ClassSignature classSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitMethodSignature(MethodTypeSignature methodSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitFieldTypeSignature(FieldTypeSignature fieldSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitFormalTypeParameters(List<FormalTypeParameter> formalTypeParameters) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitClassBound(FieldTypeSignature fieldSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitInterfaceBound(FieldTypeSignature fieldSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitSuperClass(ClassTypeSignature classTypeSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitSuperInterface(ClassTypeSignature classTypeSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitTypeSignature(TypeSignature typeSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitSimpleClass(ClassTypeSignature classTypeSignature) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitReturnType(ReturnType returnType) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitMethodTypeSignatures(List<TypeSignature> typeSignatures) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitThrowsSignatures(List<TypeSignature> typeSignatures) {
+    throw new Unreachable("Implement if visited");
+  }
+
+  default void visitTypeArguments(List<FieldTypeSignature> typeArguments) {
+    throw new Unreachable("Implement if visited");
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryEmulatedInterfaceDuplicator.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryEmulatedInterfaceDuplicator.java
new file mode 100644
index 0000000..7651e51
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryEmulatedInterfaceDuplicator.java
@@ -0,0 +1,124 @@
+// 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.ir.desugar;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GenericSignature;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class DesugaredLibraryEmulatedInterfaceDuplicator {
+
+  final AppView<?> appView;
+  final Map<DexType, DexType> emulatedInterfaces;
+
+  public DesugaredLibraryEmulatedInterfaceDuplicator(AppView<?> appView) {
+    this.appView = appView;
+    emulatedInterfaces =
+        appView.options().desugaredLibraryConfiguration.getEmulateLibraryInterface();
+  }
+
+  public void duplicateEmulatedInterfaces() {
+    // All classes implementing an emulated interface now implements the interface and the
+    // emulated one, as well as hidden overrides, for correct emulated dispatch.
+    // We not that duplicated interfaces won't feature the correct type parameters in the
+    // class signature since such signature is expected to be unused.
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      if (clazz.type == appView.dexItemFactory().objectType) {
+        continue;
+      }
+      if (emulatedInterfaces.containsKey(clazz.type)) {
+        transformEmulatedInterfaces(clazz);
+      } else {
+        duplicateEmulatedInterfaces(clazz);
+      }
+    }
+  }
+
+  private void transformEmulatedInterfaces(DexProgramClass clazz) {
+    List<ClassTypeSignature> newInterfaces = new ArrayList<>();
+    GenericSignature.ClassSignature classSignature = clazz.getClassSignature(appView);
+    for (int i = 0; i < clazz.interfaces.size(); i++) {
+      DexType itf = clazz.interfaces.values[i];
+      assert emulatedInterfaces.containsKey(itf);
+      List<FieldTypeSignature> typeArguments;
+      if (classSignature.hasNoSignature()) {
+        typeArguments = Collections.emptyList();
+      } else {
+        ClassTypeSignature classTypeSignature = classSignature.superInterfaceSignatures().get(i);
+        assert itf == classTypeSignature.type();
+        typeArguments = classTypeSignature.typeArguments();
+      }
+      newInterfaces.add(new ClassTypeSignature(emulatedInterfaces.get(itf), typeArguments));
+    }
+    clazz.replaceInterfaces(newInterfaces, appView);
+  }
+
+  private void duplicateEmulatedInterfaces(DexProgramClass clazz) {
+    List<DexType> extraInterfaces = new ArrayList<>();
+    LinkedList<DexClass> workList = new LinkedList<>();
+    Set<DexType> processed = Sets.newIdentityHashSet();
+    workList.add(clazz);
+    while (!workList.isEmpty()) {
+      DexClass dexClass = workList.removeFirst();
+      if (processed.contains(dexClass.type)) {
+        continue;
+      }
+      processed.add(dexClass.type);
+      if (dexClass.superType != appView.dexItemFactory().objectType) {
+        processSuperType(clazz.superType, extraInterfaces, workList);
+      }
+      for (DexType itf : dexClass.interfaces.values) {
+        processSuperType(itf, extraInterfaces, workList);
+      }
+    }
+    extraInterfaces = removeDuplicates(extraInterfaces);
+    List<ClassTypeSignature> extraInterfaceSignatures = new ArrayList<>();
+    for (DexType extraInterface : extraInterfaces) {
+      extraInterfaceSignatures.add(new ClassTypeSignature(extraInterface));
+    }
+    clazz.addExtraInterfaces(extraInterfaceSignatures, appView);
+  }
+
+  private List<DexType> removeDuplicates(List<DexType> extraInterfaces) {
+    if (extraInterfaces.size() <= 1) {
+      return extraInterfaces;
+    }
+    // TODO(b/161399032): It would be nice to remove duplicate based on inheritance, i.e.,
+    //  if there is ConcurrentMap<K,V> and Map<K,V>, Map<K,V> can be removed.
+    return new ArrayList<>(new HashSet<>(extraInterfaces));
+  }
+
+  void processSuperType(
+      DexType superType, List<DexType> extraInterfaces, LinkedList<DexClass> workList) {
+    if (emulatedInterfaces.containsKey(superType)) {
+      extraInterfaces.add(emulatedInterfaces.get(superType));
+    } else {
+      DexClass superClass = appView.definitionFor(superType);
+      if (shouldProcessSuperclass(superClass)) {
+        workList.add(superClass);
+      }
+    }
+  }
+
+  private boolean shouldProcessSuperclass(DexClass superclazz) {
+    if (appView.options().isDesugaredLibraryCompilation()) {
+      return false;
+    }
+    // TODO(b/161399032): Pay-as-you-go design: stop duplication on library boundaries.
+    return superclazz != null && superclazz.isLibraryClass();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
index 408b246..d81df77 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.ResolutionResult;
@@ -324,7 +325,8 @@
       // applies up to 24.
       for (DexMethod method : methods) {
         clazz.addExtraInterfaces(
-            Collections.singletonList(dispatchInterfaceTypeFor(method)), appView.dexItemFactory());
+            Collections.singletonList(new ClassTypeSignature(dispatchInterfaceTypeFor(method))),
+            appView);
         if (clazz.lookupVirtualMethod(method) == null) {
           DexEncodedMethod newMethod = createForwardingMethod(method, clazz);
           clazz.addVirtualMethod(newMethod);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index b963160..20f7a7a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -53,7 +53,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.IdentityHashMap;
-import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
@@ -917,63 +916,6 @@
     return newMethods;
   }
 
-  private void duplicateEmulatedInterfaces() {
-    // All classes implementing an emulated interface now implements the interface and the
-    // emulated one, as well as hidden overrides, for correct emulated dispatch.
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (clazz.type == appView.dexItemFactory().objectType) {
-        continue;
-      }
-      List<DexType> extraInterfaces = new ArrayList<>();
-      for (DexType type : clazz.interfaces.values) {
-        if (emulatedInterfaces.containsKey(type)) {
-          extraInterfaces.add(emulatedInterfaces.get(type));
-        }
-      }
-      if (!appView.options().isDesugaredLibraryCompilation()) {
-        assert clazz.superType != null;
-        DexClass superClazz = appView.definitionFor(clazz.superType);
-        if (superClazz != null && superClazz.isLibraryClass()) {
-          List<DexType> itfs = emulatedInterfacesOf(superClazz);
-          for (DexType itf : itfs) {
-            extraInterfaces.add(emulatedInterfaces.get(itf));
-          }
-        }
-        // Remove duplicates.
-        if (extraInterfaces.size() > 1) {
-          extraInterfaces = new ArrayList<>(new LinkedHashSet<>(extraInterfaces));
-        }
-      }
-      clazz.addExtraInterfaces(extraInterfaces, appView.dexItemFactory());
-    }
-  }
-
-  private List<DexType> emulatedInterfacesOf(DexClass superClazz) {
-    if (superClazz.type == factory.objectType) {
-      return Collections.emptyList();
-    }
-    ArrayList<DexType> itfs = new ArrayList<>();
-    LinkedList<DexType> workList = new LinkedList<>();
-    workList.add(superClazz.type);
-    while (!workList.isEmpty()) {
-      DexType dexType = workList.removeFirst();
-      DexClass dexClass = appView.definitionFor(dexType);
-      if (dexClass != null) {
-        if (dexClass.superType != factory.objectType) {
-          workList.add(dexClass.superType);
-        }
-        for (DexType itf : dexClass.interfaces.values) {
-          if (emulatedInterfaces.containsKey(itf)) {
-            itfs.add(itf);
-          } else {
-            workList.add(itf);
-          }
-        }
-      }
-    }
-    return itfs;
-  }
-
   /**
    * Move static and default interface methods to companion classes, add missing methods to forward
    * to moved default methods implementation.
@@ -986,7 +928,7 @@
     if (appView.options().isDesugaredLibraryCompilation()) {
       generateEmulateInterfaceLibrary(builder);
     }
-    duplicateEmulatedInterfaces();
+    new DesugaredLibraryEmulatedInterfaceDuplicator(appView).duplicateEmulatedInterfaces();
 
     // Process all classes first. Add missing forwarding methods to
     // replace desugared default interface methods.
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 584b84b..fa73933 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -329,7 +329,7 @@
 
   public static void clearAnnotations(AppView<?> appView) {
     for (DexProgramClass clazz : appView.appInfo().classes()) {
-      clazz.clearAnnotations();
+      clazz.clearAnnotationsButSignature(appView.dexItemFactory());
       clazz.members().forEach(DexDefinition::clearAnnotations);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index 1173ef5..3138d0c 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -517,6 +517,27 @@
         binaryName.replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR));
   }
 
+  /**
+   * Computes the inner name from the outer- and inner descriptors. If outer is not a prefix of the
+   * inner descriptor null is returned. Do not use this method if the relationship between inner and
+   * outer is not reflected in the name.
+   *
+   * @param outerDescriptor the outer descriptor, such as Lfoo/bar/Baz;
+   * @param innerDescriptor the inner descriptor, such as Lfoo/bar/Baz$Qux;
+   * @return the inner name or null, i.e. Qux in the example above
+   */
+  public static String getInnerClassName(String outerDescriptor, String innerDescriptor) {
+    if (innerDescriptor.length() <= outerDescriptor.length()) {
+      return null;
+    }
+    String prefix =
+        outerDescriptor.substring(0, outerDescriptor.length() - 1) + INNER_CLASS_SEPARATOR;
+    if (innerDescriptor.startsWith(prefix)) {
+      return innerDescriptor.substring(prefix.length(), innerDescriptor.length() - 1);
+    }
+    return null;
+  }
+
   public static class ModuleAndDescriptor {
     private final String module;
     private final String descriptor;
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
index 0eab37e..ced1c31 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibraryEmptySubclassInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibraryEmptySubclassInterfaceTest.java
@@ -57,9 +57,7 @@
       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
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/AllMapsTestClass.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/AllMapsTestClass.java
new file mode 100644
index 0000000..4c15672
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/AllMapsTestClass.java
@@ -0,0 +1,347 @@
+// 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.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class AllMapsTestClass {
+  // Program class extending ConcurrentHashMap.
+  static class NullableConcurrentHashMap<K, V> extends ConcurrentHashMap<K, V> {
+    NullableConcurrentHashMap() {
+      super();
+    }
+
+    @SuppressWarnings("NullableProblems")
+    @Override
+    public V put(K key, V value) {
+      if (key == null || value == null) {
+        return null;
+      }
+      return super.put(key, value);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+        put(entry.getKey(), entry.getValue());
+      }
+    }
+  }
+  // Program class extending the library class HashMap, implementing Map.
+  static class NullableHashMap<K, V> extends HashMap<K, V> {
+    NullableHashMap() {
+      super();
+    }
+
+    @SuppressWarnings("NullableProblems")
+    @Override
+    public V put(K key, V value) {
+      if (key == null || value == null) {
+        return null;
+      }
+      return super.put(key, value);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+        put(entry.getKey(), entry.getValue());
+      }
+    }
+  }
+  // Program class implementing Map.
+  static class NullableMap<K, V> implements Map<K, V> {
+    private HashMap<K, V> map = new HashMap<>();
+
+    NullableMap() {
+      super();
+    }
+
+    @Override
+    public int size() {
+      return map.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return map.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+      return map.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+      return map.containsValue(value);
+    }
+
+    @Override
+    public V get(Object key) {
+      return map.get(key);
+    }
+
+    @Override
+    public V put(K key, V value) {
+      return map.put(key, value);
+    }
+
+    @Override
+    public V remove(Object key) {
+      return map.remove(key);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+        put(entry.getKey(), entry.getValue());
+      }
+    }
+
+    @Override
+    public void clear() {
+      map.clear();
+    }
+
+    @Override
+    public Set<K> keySet() {
+      return map.keySet();
+    }
+
+    @Override
+    public Collection<V> values() {
+      return map.values();
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+      return map.entrySet();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (!(o instanceof NullableMap)) return false;
+      NullableMap<?, ?> that = (NullableMap<?, ?>) o;
+      return map.equals(that.map);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(map);
+    }
+  }
+  // Program class implementing ConcurrentMap.
+  static class NullableConcurrentMap<K, V> implements ConcurrentMap<K, V> {
+    private HashMap<K, V> map = new HashMap<>();
+
+    NullableConcurrentMap() {
+      super();
+    }
+
+    @Override
+    public int size() {
+      return map.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return map.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+      return map.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+      return map.containsValue(value);
+    }
+
+    @Override
+    public V get(Object key) {
+      return map.get(key);
+    }
+
+    @Override
+    public V put(K key, V value) {
+      return map.put(key, value);
+    }
+
+    @Override
+    public V remove(Object key) {
+      return map.remove(key);
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+      for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
+        put(entry.getKey(), entry.getValue());
+      }
+    }
+
+    @Override
+    public void clear() {
+      map.clear();
+    }
+
+    @Override
+    public Set<K> keySet() {
+      return map.keySet();
+    }
+
+    @Override
+    public Collection<V> values() {
+      return map.values();
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+      return map.entrySet();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (!(o instanceof NullableConcurrentMap)) return false;
+      NullableConcurrentMap<?, ?> that = (NullableConcurrentMap<?, ?>) o;
+      return map.equals(that.map);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(map);
+    }
+
+    @Override
+    public V putIfAbsent(K key, V value) {
+      return null;
+    }
+
+    @Override
+    public boolean remove(Object key, Object value) {
+      return false;
+    }
+
+    @Override
+    public boolean replace(K key, V oldValue, V newValue) {
+      return false;
+    }
+
+    @Override
+    public V replace(K key, V value) {
+      return null;
+    }
+  }
+
+  static class Data {
+    final int id;
+    final String name;
+
+    Data(int id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (!(o instanceof Data)) return false;
+      Data data = (Data) o;
+      return id == data.id && name.equals(data.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, name);
+    }
+
+    @Override
+    public String toString() {
+      return "Data{" + "id=" + id + ", name='" + name + '\'' + '}';
+    }
+  }
+
+  public static void main(String[] args) {
+    Gson gson = new Gson();
+
+    HashMap<Integer, Data> hashMap = new HashMap<>();
+    NullableHashMap<Integer, Data> nullableHashMap = new NullableHashMap<>();
+    NullableMap<Integer, Data> nullableMap = new NullableMap<>();
+    NullableConcurrentMap<Integer, Data> nullableConcurrentMap = new NullableConcurrentMap<>();
+    ConcurrentHashMap<Integer, Data> concurrentHashMap = new ConcurrentHashMap<>();
+    NullableConcurrentHashMap<Integer, Data> nullableConcurrentHashMap =
+        new NullableConcurrentHashMap<>();
+
+    fillMap(hashMap);
+    fillMap(nullableHashMap);
+    fillMap(nullableMap);
+    fillMap(nullableConcurrentMap);
+    fillMap(concurrentHashMap);
+    fillMap(nullableConcurrentHashMap);
+
+    // Serialization.
+    String hashMapJson = gson.toJson(hashMap);
+    String nullableHashMapJson = gson.toJson(nullableHashMap);
+    String nullableMapJson = gson.toJson(nullableMap);
+    String nullableConcurrentMapJson = gson.toJson(nullableConcurrentMap);
+    String concurrentHashMapJson = gson.toJson(concurrentHashMap);
+    String nullableConcurrentHashMapJson = gson.toJson(nullableConcurrentHashMap);
+
+    // Deserialization.
+    Type hashMapType = new TypeToken<HashMap<Integer, Data>>() {}.getType();
+    HashMap<Integer, Data> hashMapDeserialized = gson.fromJson(hashMapJson, hashMapType);
+    Type nullableHashMapType = new TypeToken<NullableHashMap<Integer, Data>>() {}.getType();
+    NullableHashMap<Integer, Data> nullableHashMapDeserialized =
+        gson.fromJson(nullableHashMapJson, nullableHashMapType);
+    Type nullableMapType = new TypeToken<NullableMap<Integer, Data>>() {}.getType();
+    NullableMap<Integer, Data> nullableMapDeserialized =
+        gson.fromJson(nullableMapJson, nullableMapType);
+    Type nullableConcurrentMapType =
+        new TypeToken<NullableConcurrentMap<Integer, Data>>() {}.getType();
+    NullableConcurrentMap<Integer, Data> nullableConcurrentMapDeserialized =
+        gson.fromJson(nullableConcurrentMapJson, nullableConcurrentMapType);
+    Type concurrentHashMapType = new TypeToken<ConcurrentHashMap<Integer, Data>>() {}.getType();
+    ConcurrentHashMap<Integer, Data> concurrentHashMapDeserialized =
+        gson.fromJson(concurrentHashMapJson, concurrentHashMapType);
+    Type nullableConcurrentHashMapType =
+        new TypeToken<NullableConcurrentHashMap<Integer, Data>>() {}.getType();
+    NullableConcurrentHashMap<Integer, Data> nullableConcurrentHashMapDeserialized =
+        gson.fromJson(nullableConcurrentHashMapJson, nullableConcurrentHashMapType);
+
+    // Printing.
+    System.out.println(hashMap.getClass() == hashMapDeserialized.getClass());
+    System.out.println(hashMap.equals(hashMapDeserialized));
+    System.out.println(nullableHashMap.getClass() == nullableHashMapDeserialized.getClass());
+    System.out.println(nullableHashMap.equals(nullableHashMapDeserialized));
+    System.out.println(nullableMap.getClass() == nullableMapDeserialized.getClass());
+    System.out.println(nullableMap.equals(nullableMapDeserialized));
+    System.out.println(
+        nullableConcurrentMap.getClass() == nullableConcurrentMapDeserialized.getClass());
+    System.out.println(nullableConcurrentMap.equals(nullableConcurrentMapDeserialized));
+    System.out.println(concurrentHashMap.getClass() == concurrentHashMapDeserialized.getClass());
+    System.out.println(concurrentHashMap.equals(concurrentHashMapDeserialized));
+    System.out.println(
+        nullableConcurrentHashMap.getClass() == nullableConcurrentHashMapDeserialized.getClass());
+    System.out.println(nullableConcurrentHashMap.equals(nullableConcurrentHashMapDeserialized));
+  }
+
+  public static void fillMap(Map<Integer, Data> map) {
+    map.put(1, new Data(1, "a"));
+    map.put(2, new Data(2, "b"));
+    map.put(3, new Data(3, "c"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GetGenericInterfaceTest.java
similarity index 97%
rename from src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java
rename to src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GetGenericInterfaceTest.java
index 6e300ba..249f26f 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/LibrarySubclassInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GetGenericInterfaceTest.java
@@ -1,13 +1,14 @@
 // 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
+// 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;
+package com.android.tools.r8.desugar.desugaredlibrary.gson;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
 import com.android.tools.r8.utils.BooleanUtils;
 import dalvik.system.PathClassLoader;
 import java.sql.SQLDataException;
@@ -31,7 +32,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class LibrarySubclassInterfaceTest extends DesugaredLibraryTestBase {
+public class GetGenericInterfaceTest extends DesugaredLibraryTestBase {
 
   private final TestParameters parameters;
   private final boolean shrinkDesugaredLibrary;
@@ -42,7 +43,7 @@
         BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
   }
 
-  public LibrarySubclassInterfaceTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+  public GetGenericInterfaceTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
     this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
     this.parameters = parameters;
   }
@@ -52,7 +53,7 @@
     KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
     String stdOut =
         testForD8()
-            .addInnerClasses(LibrarySubclassInterfaceTest.class)
+            .addInnerClasses(GetGenericInterfaceTest.class)
             .setMinApi(parameters.getApiLevel())
             .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
             .compile()
@@ -72,7 +73,7 @@
     KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
     String stdOut =
         testForR8(Backend.DEX)
-            .addInnerClasses(LibrarySubclassInterfaceTest.class)
+            .addInnerClasses(GetGenericInterfaceTest.class)
             .addKeepMainRule(Executor.class)
             .noMinification()
             .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonAllMapsTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonAllMapsTest.java
new file mode 100644
index 0000000..7938465
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonAllMapsTest.java
@@ -0,0 +1,77 @@
+// 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.gson;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Assume;
+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 GsonAllMapsTest extends GsonDesugaredLibraryTestBase {
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+  private static final String[] EXPECTED_RESULT =
+      new String[] {
+        "true", "true", "true", "true", "true", "true", "true", "true", "true", "true", "true",
+        "true"
+      };
+
+  @Parameters(name = "shrinkDesugaredLibrary: {0}, runtime: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public GsonAllMapsTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testGsonMapD8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(AllMapsTestClass.class)
+        .addProgramFiles(GSON_2_8_1_JAR)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get() + GSON_LIBRARY_KEEP_RULES,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), AllMapsTestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testGsonMapR8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(AllMapsTestClass.class)
+        .addProgramFiles(GSON_2_8_1_JAR)
+        .addKeepMainRule(AllMapsTestClass.class)
+        .addKeepRuleFiles(GSON_CONFIGURATION)
+        .allowUnusedProguardConfigurationRules()
+        .allowDiagnosticMessages()
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get() + GSON_LIBRARY_KEEP_RULES,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), AllMapsTestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_RESULT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonDesugaredLibraryTestBase.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonDesugaredLibraryTestBase.java
new file mode 100644
index 0000000..e761248
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonDesugaredLibraryTestBase.java
@@ -0,0 +1,20 @@
+// 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.gson;
+
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public abstract class GsonDesugaredLibraryTestBase extends DesugaredLibraryTestBase {
+  protected static final Path GSON_CONFIGURATION =
+      Paths.get("src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg");
+  protected static final Path GSON_2_8_1_JAR = Paths.get("third_party/iosched_2019/gson-2.8.1.jar");
+  // We only need here, for all subclasses of java.util.Collection and java.util.Map,
+  // to keep the signature attribute.
+  protected static final String GSON_LIBRARY_KEEP_RULES =
+      "-keepattributes Signature\n"
+          + "-keepattributes EnclosingMethod\n"
+          + "-keepattributes InnerClasses\n";
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonOptionalTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonOptionalTest.java
new file mode 100644
index 0000000..6b75eb9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonOptionalTest.java
@@ -0,0 +1,73 @@
+// 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.gson;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class GsonOptionalTest extends GsonDesugaredLibraryTestBase {
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+
+  @Parameterized.Parameters(name = "shrinkDesugaredLibrary: {0}, runtime: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public GsonOptionalTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testGsonOptionalD8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(OptionalTestClass.class)
+        .addProgramFiles(GSON_2_8_1_JAR)
+        .addOptionsModification(opt -> opt.ignoreMissingClasses = true)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get() + GSON_LIBRARY_KEEP_RULES,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), OptionalTestClass.class)
+        .assertSuccessWithOutputLines("true", "true");
+  }
+
+  @Test
+  public void testGsonOptionalR8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(parameters.getBackend())
+        .addProgramClassesAndInnerClasses(OptionalTestClass.class)
+        .addProgramFiles(GSON_2_8_1_JAR)
+        .addKeepMainRule(OptionalTestClass.class)
+        .addKeepRuleFiles(GSON_CONFIGURATION)
+        .allowUnusedProguardConfigurationRules()
+        .addOptionsModification(opt -> opt.ignoreMissingClasses = true)
+        .allowDiagnosticMessages()
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get() + GSON_LIBRARY_KEEP_RULES,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), OptionalTestClass.class)
+        .assertSuccessWithOutputLines("true", "true");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/OptionalTestClass.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/OptionalTestClass.java
new file mode 100644
index 0000000..fbc2c31
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/OptionalTestClass.java
@@ -0,0 +1,95 @@
+// 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.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Objects;
+import java.util.Optional;
+
+public class OptionalTestClass {
+  static class Data {
+    final int id;
+    final String name;
+
+    Data(int id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (!(o instanceof Data)) return false;
+      Data data = (Data) o;
+      return id == data.id && name.equals(data.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, name);
+    }
+
+    @Override
+    public String toString() {
+      return "Data{" + "id=" + id + ", name='" + name + '\'' + '}';
+    }
+  }
+
+  static class OptionalAdapter<T> extends TypeAdapter<Optional<T>> {
+    private final TypeAdapter<T> delegate;
+
+    public OptionalAdapter(TypeAdapter<T> delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public void write(JsonWriter out, Optional<T> value) throws IOException {
+      if (!value.isPresent()) {
+        out.nullValue();
+        return;
+      }
+      delegate.write(out, value.get());
+    }
+
+    @Override
+    public Optional<T> read(JsonReader in) throws IOException {
+      if (in.peek() == JsonToken.NULL) {
+        in.nextNull();
+        return Optional.empty();
+      }
+      return Optional.of(delegate.read(in));
+    }
+
+    @SuppressWarnings("unchecked")
+    public static OptionalAdapter getInstance(TypeToken typeToken) {
+      TypeAdapter delegate;
+      Type type = typeToken.getType();
+      assert type instanceof ParameterizedType;
+      Type innerType = ((ParameterizedType) type).getActualTypeArguments()[0];
+      delegate = new Gson().getAdapter(TypeToken.get(innerType));
+      return new OptionalAdapter<>(delegate);
+    }
+  }
+
+  public static void main(String[] args) {
+    GsonBuilder builder = new GsonBuilder();
+    builder.registerTypeAdapter(
+        Optional.class, OptionalAdapter.getInstance(new TypeToken<Optional<Data>>() {}));
+    Gson gson = builder.create();
+    Optional<Data> optionalData = Optional.of(new Data(1, "a"));
+    String optionalDataSerialized = gson.toJson(optionalData);
+    Optional<Data> optionalDataDeserialized = gson.fromJson(optionalDataSerialized, Optional.class);
+    System.out.println(optionalData.getClass() == optionalDataDeserialized.getClass());
+    System.out.println(optionalData.equals(optionalDataDeserialized));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg
new file mode 100644
index 0000000..da7bcba
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg
@@ -0,0 +1,34 @@
+# 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.
+# Gson uses generic type information stored in a class file when working with fields.
+# R8 removes such information by default, so configure it to keep all of it.
+-keepattributes Signature
+-keepattributes EnclosingMethod
+-keepattributes InnerClasses
+
+# For using GSON @Expose annotation
+-keepattributes AnnotationDefault,RuntimeVisibleAnnotations
+
+# Gson specific classes
+-dontwarn sun.misc.Unsafe
+
+# Application classes that will be serialized/deserialized over Gson
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$Data { <fields>; }
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableConcurrentHashMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableHashMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableConcurrentMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.OptionalTestClass$Data { <fields>; }
+
+# Prevent R8 from stripping interface information from TypeAdapter, TypeAdapterFactory,
+# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
+-keep class * extends com.google.gson.TypeAdapter
+-keep class * implements com.google.gson.TypeAdapterFactory
+-keep class * implements com.google.gson.JsonSerializer
+-keep class * implements com.google.gson.JsonDeserializer
+
+# Prevent R8 from leaving Data object members always null
+-keepclassmembers,allowobfuscation class * {
+  @com.google.gson.annotations.SerializedName <fields>;
+}
diff --git a/src/test/java/com/android/tools/r8/graph/GenericSignatureIdentityTest.java b/src/test/java/com/android/tools/r8/graph/GenericSignatureIdentityTest.java
new file mode 100644
index 0000000..b94f484
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/GenericSignatureIdentityTest.java
@@ -0,0 +1,91 @@
+// 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.graph;
+
+import static org.junit.Assert.assertEquals;
+import static org.objectweb.asm.Opcodes.ASM7;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.Reporter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+
+@RunWith(Parameterized.class)
+public class GenericSignatureIdentityTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public GenericSignatureIdentityTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testAllClassSignature() throws Exception {
+    testParseSignaturesInJar(ToolHelper.R8_WITH_DEPS_JAR);
+  }
+
+  public static void testParseSignaturesInJar(Path jar) throws Exception {
+    GenericSignatureReader genericSignatureReader = new GenericSignatureReader();
+    ZipInputStream inputStream = new ZipInputStream(Files.newInputStream(jar));
+    ZipEntry next = inputStream.getNextEntry();
+    while (next != null) {
+      if (next.getName().endsWith(".class")) {
+        ClassReader classReader = new ClassReader(inputStream);
+        classReader.accept(genericSignatureReader, 0);
+      }
+      next = inputStream.getNextEntry();
+    }
+  }
+
+  private static class GenericSignatureReader extends ClassVisitor {
+
+    public final Set<String> signatures = new HashSet<>();
+    private final DexItemFactory factory = new DexItemFactory();
+
+    private GenericSignatureReader() {
+      super(ASM7);
+    }
+
+    @Override
+    public void visit(
+        int version,
+        int access,
+        String name,
+        String signature,
+        String superName,
+        String[] interfaces) {
+      if (signature == null) {
+        return;
+      }
+      TestDiagnosticMessagesImpl testDiagnosticMessages = new TestDiagnosticMessagesImpl();
+      ClassSignature classSignature =
+          GenericSignature.parseClassSignature(
+              name, signature, Origin.unknown(), factory, new Reporter(testDiagnosticMessages));
+      assertEquals(signature, classSignature.toString());
+      testDiagnosticMessages.assertNoMessages();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/GenericSignatureTest.java b/src/test/java/com/android/tools/r8/graph/GenericSignatureTest.java
index 5001645..366e98b 100644
--- a/src/test/java/com/android/tools/r8/graph/GenericSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/graph/GenericSignatureTest.java
@@ -19,11 +19,11 @@
 import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.FormalTypeParameter;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
-import com.android.tools.r8.graph.GenericSignature.Parser;
 import com.android.tools.r8.graph.GenericSignature.ReturnType;
 import com.android.tools.r8.graph.GenericSignature.TypeSignature;
 import com.android.tools.r8.graph.GenericSignature.WildcardIndicator;
 import com.android.tools.r8.graph.GenericSignatureTestClassA.I;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -103,7 +103,13 @@
     // class <T:GenericSignatureTestClassA<T>.Y>CYY<T extends A<T>.Y> extends CY<T>
     DexClass clazz = cyy.getDexProgramClass();
     assertNotNull(clazz);
-    classSignature = Parser.toClassSignature(clazz, appView);
+    classSignature =
+        GenericSignature.parseClassSignature(
+            clazz.getType().getName(),
+            getGenericSignature(clazz, appView),
+            clazz.origin,
+            appView.dexItemFactory(),
+            appView.options().reporter);
     assertNotNull(classSignature);
 
     assertEquals(1, classSignature.formalTypeParameters.size());
@@ -135,7 +141,13 @@
     DexEncodedField field = yyInZZ.getField();
     assertNotNull(field);
 
-    fieldTypeSignature = Parser.toFieldTypeSignature(field, appView);
+    fieldTypeSignature =
+        GenericSignature.parseFieldTypeSignature(
+            field.field.qualifiedName(),
+            getGenericSignature(field, appView),
+            Origin.unknown(),
+            appView.dexItemFactory(),
+            appView.options().reporter);
     assertNotNull(fieldTypeSignature);
 
     // field type: A$Y$YY
@@ -152,7 +164,13 @@
     method = newYY.getMethod();
     assertNotNull(method);
 
-    methodTypeSignature = Parser.toMethodTypeSignature(method, appView);
+    methodTypeSignature =
+        GenericSignature.parseMethodSignature(
+            method.qualifiedName(),
+            getGenericSignature(method, appView),
+            Origin.unknown(),
+            appView.dexItemFactory(),
+            appView.options().reporter);
     assertNotNull(methodTypeSignature);
 
     assertEquals(1, methodTypeSignature.formalTypeParameters.size());
@@ -194,7 +212,13 @@
     method = convertToYY.getMethod();
     assertNotNull(method);
 
-    methodTypeSignature = GenericSignature.Parser.toMethodTypeSignature(method, appView);
+    methodTypeSignature =
+        GenericSignature.parseMethodSignature(
+            method.qualifiedName(),
+            getGenericSignature(method, appView),
+            Origin.unknown(),
+            appView.dexItemFactory(),
+            appView.options().reporter);
     assertNotNull(methodTypeSignature);
 
     // return type: Function<A$Y$ZZ<TT>, A$Y$YY>
@@ -231,7 +255,13 @@
     assertNotNull(method);
 
     // return type: void
-    methodTypeSignature = Parser.toMethodTypeSignature(method, appView);
+    methodTypeSignature =
+        GenericSignature.parseMethodSignature(
+            method.qualifiedName(),
+            getGenericSignature(method, appView),
+            Origin.unknown(),
+            appView.dexItemFactory(),
+            appView.options().reporter);
     assertNotNull(methodTypeSignature);
     returnType = methodTypeSignature.returnType();
     assertTrue(returnType.isVoidDescriptor());
@@ -304,7 +334,12 @@
       AppView<AppInfoWithLiveness> appView,
       Consumer<FieldTypeSignature> fieldTypeConsumer) {
     MethodTypeSignature methodTypeSignature =
-        Parser.toMethodTypeSignature(methodSubject.getMethod(), appView);
+        GenericSignature.parseMethodSignature(
+            methodSubject.getOriginalName(),
+            getGenericSignature(methodSubject.getMethod(), appView),
+            Origin.unknown(),
+            appView.dexItemFactory(),
+            appView.options().reporter);
     TypeSignature typeSignature = methodTypeSignature.returnType.typeSignature;
     FieldTypeSignature fieldTypeSignature = typeSignature.asFieldTypeSignature();
     assertTrue(fieldTypeSignature.isClassTypeSignature());
@@ -334,6 +369,22 @@
     assertTrue(typeArgument.isClassTypeSignature());
     check_A_Y_ZZ(a, y, zz, typeArgument.asClassTypeSignature());
   }
+
+  private static String getGenericSignature(
+      DexDefinition definition, AppView<AppInfoWithLiveness> appView) {
+    DexAnnotationSet annotations = definition.annotations();
+    if (annotations.annotations.length == 0) {
+      return null;
+    }
+    for (int i = 0; i < annotations.annotations.length; i++) {
+      DexAnnotation annotation = annotations.annotations[i];
+      if (!DexAnnotation.isSignatureAnnotation(annotation, appView.dexItemFactory())) {
+        continue;
+      }
+      return DexAnnotation.getSignature(annotation);
+    }
+    return null;
+  }
 }
 
 //
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/ClassSignatureTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/ClassSignatureTest.java
new file mode 100644
index 0000000..cd057d2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/ClassSignatureTest.java
@@ -0,0 +1,177 @@
+// 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.graph.genericsignature;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.graph.GenericSignature.ClassSignature.NO_CLASS_SIGNATURE;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.GenericSignature;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.graph.GenericSignaturePrinter;
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.Reporter;
+import com.google.common.base.Predicates;
+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 ClassSignatureTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public ClassSignatureTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testSuperClass() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz;");
+  }
+
+  @Test
+  public void testSuperClassWithInnerClasses() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz.Foo.Bar;");
+  }
+
+  @Test
+  public void testSuperClassWithInterfaces1() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz;Lfoo/bar/qux;");
+  }
+
+  @Test
+  public void testSuperClassWithInterfaces2() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz;Lfoo/bar/qux;Lfoo/bar/quux;");
+  }
+
+  @Test
+  public void testSuperClassWithInterfacesInnerClassesSeparatedByDollar() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz$Foo$Bar;Lfoo/bar/qux;Lfoo/bar/quux$Foo$Bar;");
+  }
+
+  @Test
+  public void testSuperClassWithInterfacesInnerClassesSeparatedByPeriod() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz.Foo.Bar;Lfoo/bar/qux;Lfoo/bar/quux.Foo.Bar;");
+  }
+
+  @Test
+  public void testInnerClassesWithSeparatorInName1() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz<*>.Foo$$$<*>.Bar;");
+  }
+
+  @Test
+  public void testInnerClassesWithSeparatorInName2() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz<*>.Foo$Bar$$<*>.Qux;");
+  }
+
+  @Test
+  public void testSuperClassWithInnerClassesGenericArguments2() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz<LFoo;>.Foo<LFoo;>.Bar<LFoo;>;");
+  }
+
+  @Test
+  public void testSuperClassWithInterfacesInnerClassesGenericArguments2() {
+    testParsingAndPrintingEqual(
+        "Lfoo/bar/baz<LFoo;>.Foo<LFoo;>.Bar<LFoo;>;Lfoo/bar/qux<LFoo;>;Lfoo/bar/quux<LFoo;>.Foo<LFoo;>.Bar<LFoo;>;");
+  }
+
+  @Test
+  public void testSuperClassWithInterfacesInnerClassesGenericArguments3() {
+    testParsingAndPrintingEqual(
+        "Lfoo/bar/baz<LFoo;[[I[[[LBar<LFoo;>;>.Foo<LFoo;>.Bar<LFoo;>;Lfoo/bar/qux<LFoo;>;Lfoo/bar/quux<LFoo;>.Foo<LFoo;>.Bar<LFoo;>;");
+  }
+
+  @Test
+  public void testWildCards() {
+    testParsingAndPrintingEqual("Lfoo/Bar<*>;");
+  }
+
+  @Test
+  public void testWildCardsPositiveNegative() {
+    testParsingAndPrintingEqual("Lfoo/Bar<*+[I-LFoo<+LBar;>;>;");
+  }
+
+  @Test
+  public void testSuperClassError() {
+    TestDiagnosticMessages testDiagnosticMessages = testParsingAndPrintingError("Lfoo/bar/baz");
+    testDiagnosticMessages.assertAllWarningsMatch(
+        diagnosticMessage(containsString("Invalid signature 'Lfoo/bar/baz'")));
+  }
+
+  @Test
+  public void testReferenceToTypeVariable() {
+    testParsingAndPrintingEqual("Lfoo/bar/baz<TT;>;");
+  }
+
+  @Test
+  public void testFormalTypeParameters() {
+    testParsingAndPrintingEqual("<T:Ljava/lang/Object;>Lfoo/bar/baz<TT;>;");
+  }
+
+  @Test
+  public void testFormalTypeWithEmpty() {
+    testParsingAndPrintingEqual("<T:>Lfoo/bar/baz<TT;>;");
+  }
+
+  @Test
+  public void testFormalTypeParametersWithInterfaces() {
+    testParsingAndPrintingEqual("<T:Ljava/lang/Object;:LI;>Lfoo/bar/baz<TT;>;");
+  }
+
+  @Test
+  public void testFormalTypeParametersArguments() {
+    testParsingAndPrintingEqual(
+        "<T:Ljava/lang/Object;:LI;R:LFoo<TT;[Lfoo/bar<TT;>.Baz<TT;[I>;>;>Lfoo/bar/baz<TT;>;");
+  }
+
+  @Test
+  public void testFormalTypeParameters2() {}
+
+  @Test
+  public void testFormalTypeParametersEmptyError() {
+    // TODO(b/169716723): This should throw an error
+    assertThrows(AssertionError.class, () -> testParsingAndPrintingError("<>Lfoo/bar/baz<TT;>;"));
+  }
+
+  private void testParsingAndPrintingEqual(String signature) {
+    ClassSignature parsed =
+        GenericSignature.parseClassSignature(
+            "A", signature, Origin.unknown(), new DexItemFactory(), new Reporter());
+    GenericSignaturePrinter genericSignaturePrinter =
+        new GenericSignaturePrinter(NamingLens.getIdentityLens(), Predicates.alwaysFalse());
+    genericSignaturePrinter.visitClassSignature(parsed);
+    String outSignature = genericSignaturePrinter.toString();
+    assertEquals(signature, outSignature);
+  }
+
+  private TestDiagnosticMessages testParsingAndPrintingError(String signature) {
+    TestDiagnosticMessagesImpl testDiagnosticMessages = new TestDiagnosticMessagesImpl();
+    ClassSignature parsed =
+        GenericSignature.parseClassSignature(
+            "A",
+            signature,
+            Origin.unknown(),
+            new DexItemFactory(),
+            new Reporter(testDiagnosticMessages));
+    assertEquals(NO_CLASS_SIGNATURE, parsed);
+    return testDiagnosticMessages;
+  }
+}