Replace pruned type variables with concrete instantiations

This CL will replace type variables defined in pruned outer scopes,
such as outer classes or enclosing methods, with the most precise
value we know for the type.

Bug: 184927364
Bug: 185098797
Bug: 185079478
Change-Id: I6a06683bda2213cd91495509a19c9a86e672af17
diff --git a/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java b/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
index 82f34d7..9cec3cb 100644
--- a/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
+++ b/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
@@ -55,6 +55,10 @@
     return enclosingClass;
   }
 
+  public DexType getEnclosingType() {
+    return enclosingMethod != null ? enclosingMethod.getHolderType() : enclosingClass;
+  }
+
   @Override
   public int hashCode() {
     assert (enclosingClass == null) != (enclosingMethod == null);
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 bf7c42b..9f259ba 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignature.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignature.java
@@ -139,6 +139,10 @@
       return false;
     }
 
+    default boolean isValid() {
+      return !isInvalid();
+    }
+
     DexDefinitionSignature<T> toInvalid();
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignaturePartialTypeArgumentApplier.java b/src/main/java/com/android/tools/r8/graph/GenericSignaturePartialTypeArgumentApplier.java
new file mode 100644
index 0000000..b9d7386
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignaturePartialTypeArgumentApplier.java
@@ -0,0 +1,202 @@
+// Copyright (c) 2021, 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.StarFieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.TypeSignature;
+import com.android.tools.r8.graph.GenericSignature.WildcardIndicator;
+import com.android.tools.r8.utils.ListUtils;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class GenericSignaturePartialTypeArgumentApplier implements GenericSignatureVisitor {
+
+  private final Map<String, DexType> substitutions;
+  private final DexType objectType;
+  private final Set<String> introducedClassTypeVariables = new HashSet<>();
+  private final Set<String> introducedMethodTypeVariables = new HashSet<>();
+
+  // Wildcards can only be called be used in certain positions:
+  // https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html
+  private boolean canUseWildcardInArguments = true;
+
+  private GenericSignaturePartialTypeArgumentApplier(
+      Map<String, DexType> substitutions, DexType objectType) {
+    this.substitutions = substitutions;
+    this.objectType = objectType;
+  }
+
+  public static GenericSignaturePartialTypeArgumentApplier build(
+      AppView<?> appView, ClassSignature classSignature, Map<String, DexType> substitutions) {
+    GenericSignaturePartialTypeArgumentApplier applier =
+        new GenericSignaturePartialTypeArgumentApplier(
+            substitutions, appView.dexItemFactory().objectType);
+    classSignature.formalTypeParameters.forEach(
+        parameter -> applier.introducedClassTypeVariables.add(parameter.name));
+    return applier;
+  }
+
+  @Override
+  public ClassSignature visitClassSignature(ClassSignature classSignature) {
+    return classSignature.visit(this);
+  }
+
+  @Override
+  public MethodTypeSignature visitMethodSignature(MethodTypeSignature methodSignature) {
+    assert introducedMethodTypeVariables.isEmpty();
+    methodSignature.formalTypeParameters.forEach(
+        parameter -> introducedMethodTypeVariables.add(parameter.name));
+    MethodTypeSignature rewritten = methodSignature.visit(this);
+    introducedMethodTypeVariables.clear();
+    return rewritten;
+  }
+
+  @Override
+  public DexType visitType(DexType type) {
+    return type;
+  }
+
+  @Override
+  public TypeSignature visitTypeSignature(TypeSignature typeSignature) {
+    if (typeSignature.isBaseTypeSignature()) {
+      return typeSignature;
+    }
+    return visitFieldTypeSignature(typeSignature.asFieldTypeSignature());
+  }
+
+  @Override
+  public FormalTypeParameter visitFormalTypeParameter(FormalTypeParameter formalTypeParameter) {
+    return formalTypeParameter.visit(this);
+  }
+
+  @Override
+  public List<FieldTypeSignature> visitInterfaceBounds(List<FieldTypeSignature> fieldSignatures) {
+    if (fieldSignatures == null || fieldSignatures.isEmpty()) {
+      return fieldSignatures;
+    }
+    return ListUtils.mapOrElse(fieldSignatures, this::visitFieldTypeSignature);
+  }
+
+  @Override
+  public List<ClassTypeSignature> visitSuperInterfaces(
+      List<ClassTypeSignature> interfaceSignatures) {
+    if (interfaceSignatures.isEmpty()) {
+      return interfaceSignatures;
+    }
+    canUseWildcardInArguments = false;
+    List<ClassTypeSignature> map =
+        ListUtils.mapOrElse(interfaceSignatures, this::visitSuperInterface);
+    canUseWildcardInArguments = true;
+    return map;
+  }
+
+  @Override
+  public List<FieldTypeSignature> visitTypeArguments(List<FieldTypeSignature> typeArguments) {
+    if (typeArguments.isEmpty()) {
+      return typeArguments;
+    }
+    return ListUtils.mapOrElse(typeArguments, this::visitFieldTypeSignature);
+  }
+
+  @Override
+  public ClassTypeSignature visitSuperInterface(ClassTypeSignature classTypeSignature) {
+    return classTypeSignature.visit(this);
+  }
+
+  @Override
+  public FieldTypeSignature visitClassBound(FieldTypeSignature fieldSignature) {
+    return visitFieldTypeSignature(fieldSignature);
+  }
+
+  @Override
+  public FieldTypeSignature visitInterfaceBound(FieldTypeSignature fieldSignature) {
+    return visitFieldTypeSignature(fieldSignature);
+  }
+
+  @Override
+  public ClassTypeSignature visitSimpleClass(ClassTypeSignature classTypeSignature) {
+    return classTypeSignature.visit(this);
+  }
+
+  @Override
+  public List<TypeSignature> visitThrowsSignatures(List<TypeSignature> typeSignatures) {
+    if (typeSignatures.isEmpty()) {
+      return typeSignatures;
+    }
+    return ListUtils.mapOrElse(typeSignatures, this::visitTypeSignature);
+  }
+
+  @Override
+  public ReturnType visitReturnType(ReturnType returnType) {
+    if (returnType.isVoidDescriptor()) {
+      return returnType;
+    }
+    TypeSignature originalSignature = returnType.typeSignature;
+    TypeSignature rewrittenSignature = visitTypeSignature(originalSignature);
+    if (originalSignature == rewrittenSignature) {
+      return returnType;
+    }
+    return new ReturnType(rewrittenSignature);
+  }
+
+  @Override
+  public List<FormalTypeParameter> visitFormalTypeParameters(
+      List<FormalTypeParameter> formalTypeParameters) {
+    if (formalTypeParameters.isEmpty()) {
+      return formalTypeParameters;
+    }
+    return ListUtils.mapOrElse(formalTypeParameters, this::visitFormalTypeParameter);
+  }
+
+  @Override
+  public List<TypeSignature> visitMethodTypeSignatures(List<TypeSignature> typeSignatures) {
+    if (typeSignatures.isEmpty()) {
+      return typeSignatures;
+    }
+    return ListUtils.mapOrElse(typeSignatures, this::visitTypeSignature);
+  }
+
+  @Override
+  public ClassTypeSignature visitSuperClass(ClassTypeSignature classTypeSignature) {
+    canUseWildcardInArguments = false;
+    ClassTypeSignature visit = classTypeSignature.visit(this);
+    canUseWildcardInArguments = true;
+    return visit;
+  }
+
+  @Override
+  public FieldTypeSignature visitFieldTypeSignature(FieldTypeSignature fieldSignature) {
+    if (fieldSignature.isStar()) {
+      return fieldSignature;
+    } else if (fieldSignature.isClassTypeSignature()) {
+      return fieldSignature.asClassTypeSignature().visit(this);
+    } else if (fieldSignature.isArrayTypeSignature()) {
+      return fieldSignature.asArrayTypeSignature().visit(this);
+    } else {
+      assert fieldSignature.isTypeVariableSignature();
+      String typeVariableName = fieldSignature.asTypeVariableSignature().typeVariable();
+      if (substitutions.containsKey(typeVariableName)
+          && !introducedClassTypeVariables.contains(typeVariableName)
+          && !introducedMethodTypeVariables.contains(typeVariableName)) {
+        DexType substitution = substitutions.get(typeVariableName);
+        if (substitution == null) {
+          substitution = objectType;
+        }
+        return substitution == objectType && canUseWildcardInArguments
+            ? StarFieldTypeSignature.getStarFieldTypeSignature()
+            : new ClassTypeSignature(substitution).asArgument(WildcardIndicator.NONE);
+      }
+      return fieldSignature;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
index 6d31cca..0a0124d 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
@@ -4,10 +4,7 @@
 
 package com.android.tools.r8.graph;
 
-import static com.android.tools.r8.graph.GenericSignature.EMPTY_SUPER_INTERFACES;
 import static com.android.tools.r8.graph.GenericSignature.EMPTY_TYPE_ARGUMENTS;
-import static com.android.tools.r8.graph.GenericSignature.EMPTY_TYPE_PARAMS;
-import static com.android.tools.r8.graph.GenericSignature.EMPTY_TYPE_SIGNATURES;
 import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
@@ -120,7 +117,7 @@
     public List<FormalTypeParameter> visitFormalTypeParameters(
         List<FormalTypeParameter> formalTypeParameters) {
       if (formalTypeParameters.isEmpty()) {
-        return EMPTY_TYPE_PARAMS;
+        return formalTypeParameters;
       }
       return ListUtils.mapOrElse(formalTypeParameters, this::visitFormalTypeParameter);
     }
@@ -142,7 +139,7 @@
     public List<ClassTypeSignature> visitSuperInterfaces(
         List<ClassTypeSignature> interfaceSignatures) {
       if (interfaceSignatures.isEmpty()) {
-        return EMPTY_SUPER_INTERFACES;
+        return interfaceSignatures;
       }
       return ListUtils.mapOrElse(interfaceSignatures, this::visitSuperInterface);
     }
@@ -156,7 +153,7 @@
     @Override
     public List<TypeSignature> visitMethodTypeSignatures(List<TypeSignature> typeSignatures) {
       if (typeSignatures.isEmpty()) {
-        return EMPTY_TYPE_SIGNATURES;
+        return typeSignatures;
       }
       return ListUtils.mapOrElse(
           typeSignatures,
@@ -186,7 +183,7 @@
     @Override
     public List<TypeSignature> visitThrowsSignatures(List<TypeSignature> typeSignatures) {
       if (typeSignatures.isEmpty()) {
-        return EMPTY_TYPE_SIGNATURES;
+        return typeSignatures;
       }
       // If a throwing type is no longer found we remove it from the signature.
       return ListUtils.mapOrElse(typeSignatures, this::visitTypeSignature);
@@ -199,11 +196,8 @@
 
     @Override
     public List<FieldTypeSignature> visitInterfaceBounds(List<FieldTypeSignature> fieldSignatures) {
-      if (fieldSignatures == null) {
-        return null;
-      }
-      if (fieldSignatures.isEmpty()) {
-        return EMPTY_TYPE_ARGUMENTS;
+      if (fieldSignatures == null || fieldSignatures.isEmpty()) {
+        return fieldSignatures;
       }
       return ListUtils.mapOrElse(fieldSignatures, this::visitFieldTypeSignature);
     }
@@ -221,7 +215,7 @@
     @Override
     public List<FieldTypeSignature> visitTypeArguments(List<FieldTypeSignature> typeArguments) {
       if (typeArguments.isEmpty()) {
-        return EMPTY_TYPE_ARGUMENTS;
+        return typeArguments;
       }
       return ListUtils.mapOrElse(
           typeArguments,
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java
new file mode 100644
index 0000000..4e674fb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java
@@ -0,0 +1,123 @@
+// Copyright (c) 2021, 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.FieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.FormalTypeParameter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+public class GenericSignatureTypeVariableRemover {
+
+  private final AppView<?> appView;
+  private final Predicate<InnerClassAttribute> innerClassPruned;
+  private final Predicate<EnclosingMethodAttribute> enclosingClassOrMethodPruned;
+
+  public GenericSignatureTypeVariableRemover(
+      AppView<?> appView,
+      Predicate<InnerClassAttribute> innerClassPruned,
+      Predicate<EnclosingMethodAttribute> enclosingClassOrMethodPruned) {
+    this.appView = appView;
+    this.innerClassPruned = innerClassPruned;
+    this.enclosingClassOrMethodPruned = enclosingClassOrMethodPruned;
+  }
+
+  public void removeDeadGenericSignatureTypeVariables(DexProgramClass clazz) {
+    if (clazz.getClassSignature().hasNoSignature() || clazz.getClassSignature().isInvalid()) {
+      return;
+    }
+    Map<String, DexType> substitutions = new HashMap<>();
+    getPrunedTypeParameters(clazz, substitutions, false);
+    if (substitutions.isEmpty()) {
+      return;
+    }
+    GenericSignaturePartialTypeArgumentApplier genericSignatureTypeArgumentApplier =
+        GenericSignaturePartialTypeArgumentApplier.build(
+            appView, clazz.getClassSignature(), substitutions);
+    clazz.setClassSignature(
+        genericSignatureTypeArgumentApplier.visitClassSignature(clazz.getClassSignature()));
+    clazz
+        .methods()
+        .forEach(
+            method -> {
+              if (method.getGenericSignature().hasSignature()
+                  && method.getGenericSignature().isValid()
+                  && method.isVirtualMethod()) {
+                method.setGenericSignature(
+                    genericSignatureTypeArgumentApplier.visitMethodSignature(
+                        method.getGenericSignature()));
+              }
+            });
+    clazz
+        .instanceFields()
+        .forEach(
+            field -> {
+              if (field.getGenericSignature().hasSignature()
+                  && field.getGenericSignature().isValid()) {
+                field.setGenericSignature(
+                    genericSignatureTypeArgumentApplier.visitFieldTypeSignature(
+                        field.getGenericSignature()));
+              }
+            });
+  }
+
+  private void getPrunedTypeParameters(
+      DexClass clazz, Map<String, DexType> substitutions, boolean seenPruned) {
+    InnerClassAttribute innerClassAttribute = clazz.getInnerClassAttributeForThisClass();
+    if (innerClassAttribute != null
+        && innerClassAttribute.getOuter() != null
+        && (seenPruned || innerClassPruned.test(innerClassAttribute))) {
+      DexClass outerClass = appView.definitionFor(innerClassAttribute.getOuter());
+      if (outerClass != null && outerClass.getClassSignature().isValid()) {
+        updateMap(outerClass.getClassSignature().getFormalTypeParameters(), substitutions);
+        getPrunedTypeParameters(outerClass, substitutions, true);
+      }
+    }
+    if (clazz.getEnclosingMethodAttribute() != null
+        && (seenPruned || enclosingClassOrMethodPruned.test(clazz.getEnclosingMethodAttribute()))) {
+      DexClass outerClass =
+          appView.definitionFor(clazz.getEnclosingMethodAttribute().getEnclosingType());
+      if (outerClass == null) {
+        return;
+      }
+      if (clazz.getEnclosingMethodAttribute().getEnclosingMethod() != null) {
+        DexEncodedMethod enclosingMethod =
+            outerClass.lookupMethod(clazz.getEnclosingMethodAttribute().getEnclosingMethod());
+        if (enclosingMethod != null) {
+          updateMap(enclosingMethod.getGenericSignature().getFormalTypeParameters(), substitutions);
+          if (enclosingMethod.isStatic()) {
+            return;
+          }
+        }
+      }
+      if (outerClass.getClassSignature().isValid()) {
+        updateMap(outerClass.getClassSignature().getFormalTypeParameters(), substitutions);
+      }
+      getPrunedTypeParameters(outerClass, substitutions, true);
+    }
+  }
+
+  private void updateMap(
+      List<FormalTypeParameter> formalTypeParameters, Map<String, DexType> substitutions) {
+    // We are updating the map going from inner most to outer, thus the any overriding formal type
+    // parameters will be in the substitution map already.
+    formalTypeParameters.forEach(
+        parameter -> {
+          if (substitutions.containsKey(parameter.getName())) {
+            return;
+          }
+          // The null substitution will use the wildcard as argument, which is smaller than using
+          // Ljava/lang/Object;
+          DexType substitution = null;
+          FieldTypeSignature classBound = parameter.getClassBound();
+          if (classBound != null && classBound.isClassTypeSignature()) {
+            substitution = classBound.asClassTypeSignature().type();
+          }
+          substitutions.put(parameter.getName(), substitution);
+        });
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
index 2c57aa4..3893e32 100644
--- a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
+++ b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.graph.GenericSignatureTypeRewriter;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.ThreadUtils;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -20,13 +19,11 @@
   private final AppView<?> appView;
   private final NamingLens namingLens;
   private final InternalOptions options;
-  private final Reporter reporter;
 
   public GenericSignatureRewriter(AppView<?> appView, NamingLens namingLens) {
     this.appView = appView;
     this.namingLens = namingLens;
     this.options = appView.options();
-    this.reporter = options.reporter;
   }
 
   public void run(Iterable<? extends DexProgramClass> classes, ExecutorService executorService)
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 6974a61..8f606c1 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.EnclosingMethodAttribute;
+import com.android.tools.r8.graph.GenericSignatureTypeVariableRemover;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
 import com.android.tools.r8.logging.Log;
@@ -38,6 +39,7 @@
   private final UnusedItemsPrinter unusedItemsPrinter;
   private final Set<DexType> prunedTypes = Sets.newIdentityHashSet();
   private final Set<DexMethod> methodsToKeepForConfigurationDebugging = Sets.newIdentityHashSet();
+  private final GenericSignatureTypeVariableRemover typeVariableRemover;
 
   public TreePruner(AppView<AppInfoWithLiveness> appView) {
     this(appView, DefaultTreePrunerConfiguration.getInstance());
@@ -54,6 +56,11 @@
                     ExceptionUtils.withConsumeResourceHandler(
                         options.reporter, options.usageInformationConsumer, s))
             : UnusedItemsPrinter.DONT_PRINT;
+    this.typeVariableRemover =
+        new GenericSignatureTypeVariableRemover(
+            appView,
+            this::isAttributeReferencingMissingOrPrunedType,
+            this::isAttributeReferencingPrunedItem);
   }
 
   public DirectMappedDexApplication run() {
@@ -183,6 +190,7 @@
     if (reachableStaticFields != null) {
       clazz.setStaticFields(reachableStaticFields);
     }
+    typeVariableRemover.removeDeadGenericSignatureTypeVariables(clazz);
     clazz.removeInnerClasses(this::isAttributeReferencingMissingOrPrunedType);
     clazz.removeEnclosingMethodAttribute(this::isAttributeReferencingPrunedItem);
     rewriteNestAttributes(clazz);
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedOuterTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedOuterTest.java
new file mode 100644
index 0000000..608b8d3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedOuterTest.java
@@ -0,0 +1,125 @@
+// Copyright (c) 2021, 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.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
+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 GenericSignaturePrunedOuterTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final boolean isCompat;
+
+  @Parameters(name = "{0}, isCompat: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
+  }
+
+  public GenericSignaturePrunedOuterTest(TestParameters parameters, boolean isCompat) {
+    this.parameters = parameters;
+    this.isCompat = isCompat;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    (isCompat ? testForR8Compat(parameters.getBackend()) : testForR8(parameters.getBackend()))
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Foo.class)
+        .addKeepMainRule(Main.class)
+        .addKeepAttributeSignature()
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(options -> options.horizontalClassMergerOptions().disable())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "Bar::enclosingMethod", "Hello World", "Bar::enclosingMethod2", "Hello World")
+        .inspect(this::checkSignatures);
+  }
+
+  public void checkSignatures(CodeInspector inspector) {
+    checkSignature(
+        inspector.clazz(Bar.class.getTypeName() + "$1"),
+        "L"
+            + binaryName(Foo.class)
+            + "<"
+            + descriptor(Object.class)
+            + descriptor(Main.class)
+            + ">;");
+    checkSignature(
+        inspector.clazz(Bar.class.getTypeName() + "$2"),
+        "L"
+            + binaryName(Foo.class)
+            + "<"
+            + descriptor(Object.class)
+            + descriptor(Object.class)
+            + ">;");
+  }
+
+  private void checkSignature(ClassSubject classSubject, String expectedSignature) {
+    assertThat(classSubject, isPresent());
+    // TODO(b/185098797): Make sure to work for full mode.
+    if (!isCompat) {
+      return;
+    }
+    assertEquals(expectedSignature, classSubject.getFinalSignatureAttribute());
+  }
+
+  public abstract static class Foo<T, R> {
+
+    R foo(T r) {
+      System.out.println("Hello World");
+      return null;
+    }
+  }
+
+  public static class Bar {
+
+    public static <T, R extends Main> Foo<T, R> enclosingMethod() {
+      return new Foo<T, R>() {
+        @Override
+        R foo(T r) {
+          System.out.println("Bar::enclosingMethod");
+          return super.foo(r);
+        }
+      };
+    }
+
+    public static <T, R> Foo<T, R> enclosingMethod2() {
+      return new Foo<T, R>() {
+        @Override
+        R foo(T r) {
+          System.out.println("Bar::enclosingMethod2");
+          return super.foo(r);
+        }
+      };
+    }
+
+    public static void run() {
+      enclosingMethod().foo(null);
+      enclosingMethod2().foo(null);
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Bar.run();
+    }
+  }
+}