Desugared Library: Pay-as-you-go emulated interfaces

- Merge of the interface duplicator into the ClassProcessor since the
  boundary where interfaces should be inserted is no longer the simple
  isLibraryClass heuristic.

Bug: 161399032
Change-Id: I57891de0113f92b94d0b1a44b11b97d82dcacada
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
index f2ae9a1..758830d 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.DexMethod;
 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.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
@@ -29,9 +30,12 @@
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -79,6 +83,12 @@
     boolean isEmpty() {
       return signatures.isEmpty();
     }
+
+    public MethodSignatures withoutAll(MethodSignatures other) {
+      Set<Wrapper<DexMethod>> merged = new HashSet<>(signatures);
+      merged.removeAll(other.signatures);
+      return signatures.size() == merged.size() ? this : new MethodSignatures(merged);
+    }
   }
 
   // Collection of information known at the point of a given (non-library) class.
@@ -86,7 +96,8 @@
   // class hierarchy. Thus, in the case of additions the parent pointer will contain prior info.
   private static class ClassInfo {
 
-    static final ClassInfo EMPTY = new ClassInfo(null, ImmutableList.of());
+    static final ClassInfo EMPTY =
+        new ClassInfo(null, ImmutableList.of(), EmulatedInterfaceInfo.EMPTY);
 
     final ClassInfo parent;
 
@@ -94,17 +105,26 @@
     // class hierarchy. This set consists of the default interface methods, i.e., the targets of the
     // forwarding methods, *not* the forwarding methods themselves.
     final ImmutableList<DexEncodedMethod> forwardedMethodTargets;
+    // If the forwarding methods for the emulated interface methods have not been added yet,
+    // this contains the information to add it in the subclasses.
+    final EmulatedInterfaceInfo emulatedInterfaceInfo;
 
-    ClassInfo(ClassInfo parent, ImmutableList<DexEncodedMethod> forwardedMethodTargets) {
+    ClassInfo(
+        ClassInfo parent,
+        ImmutableList<DexEncodedMethod> forwardedMethodTargets,
+        EmulatedInterfaceInfo emulatedInterfaceInfo) {
       this.parent = parent;
       this.forwardedMethodTargets = forwardedMethodTargets;
+      this.emulatedInterfaceInfo = emulatedInterfaceInfo;
     }
 
     static ClassInfo create(
-        ClassInfo parent, ImmutableList<DexEncodedMethod> forwardedMethodTargets) {
+        ClassInfo parent,
+        ImmutableList<DexEncodedMethod> forwardedMethodTargets,
+        EmulatedInterfaceInfo emulatedInterfaceInfo) {
       return forwardedMethodTargets.isEmpty()
           ? parent
-          : new ClassInfo(parent, forwardedMethodTargets);
+          : new ClassInfo(parent, forwardedMethodTargets, emulatedInterfaceInfo);
     }
 
     public boolean isEmpty() {
@@ -117,6 +137,107 @@
     }
   }
 
+  // Collection of information on what signatures and what emulated interfaces require
+  // forwarding methods for library classes and interfaces.
+  private static class SignaturesInfo {
+
+    static final SignaturesInfo EMPTY =
+        new SignaturesInfo(MethodSignatures.EMPTY, EmulatedInterfaceInfo.EMPTY);
+
+    final MethodSignatures signatures;
+    final EmulatedInterfaceInfo emulatedInterfaceInfo;
+
+    private SignaturesInfo(
+        MethodSignatures methodsToForward, EmulatedInterfaceInfo emulatedInterfaceInfo) {
+      this.signatures = methodsToForward;
+      this.emulatedInterfaceInfo = emulatedInterfaceInfo;
+    }
+
+    public static SignaturesInfo create(MethodSignatures signatures) {
+      if (signatures.isEmpty()) {
+        return EMPTY;
+      }
+      return new SignaturesInfo(signatures, EmulatedInterfaceInfo.EMPTY);
+    }
+
+    public SignaturesInfo merge(SignaturesInfo other) {
+      if (isEmpty()) {
+        return other;
+      }
+      if (other.isEmpty()) {
+        return this;
+      }
+      return new SignaturesInfo(
+          signatures.merge(other.signatures),
+          emulatedInterfaceInfo.merge(other.emulatedInterfaceInfo));
+    }
+
+    public MethodSignatures emulatedInterfaceSignaturesToForward() {
+      return emulatedInterfaceInfo.signatures.withoutAll(signatures);
+    }
+
+    boolean isEmpty() {
+      return signatures.isEmpty() && emulatedInterfaceInfo.isEmpty();
+    }
+
+    public SignaturesInfo withSignatures(MethodSignatures additions) {
+      if (additions.isEmpty()) {
+        return this;
+      }
+      MethodSignatures newSignatures = signatures.merge(additions);
+      return new SignaturesInfo(newSignatures, emulatedInterfaceInfo);
+    }
+
+    public SignaturesInfo withEmulatedInterfaceInfo(
+        EmulatedInterfaceInfo additionalEmulatedInterfaceInfo) {
+      if (additionalEmulatedInterfaceInfo.isEmpty()) {
+        return this;
+      }
+      return new SignaturesInfo(
+          signatures, emulatedInterfaceInfo.merge(additionalEmulatedInterfaceInfo));
+    }
+  }
+
+  // List of emulated interfaces and corresponding signatures which may require forwarding methods.
+  // If one of the signatures has an override, then the class holding the override is required to
+  // add the forwarding methods for all signatures, and introduce the corresponding emulated
+  // interface in its interfaces attribute for correct emulated dispatch.
+  // If no override is present, then no forwarding methods are required, the class relies on the
+  // default behavior of the emulated dispatch.
+  private static class EmulatedInterfaceInfo {
+
+    static final EmulatedInterfaceInfo EMPTY =
+        new EmulatedInterfaceInfo(MethodSignatures.EMPTY, ImmutableSet.of());
+
+    final MethodSignatures signatures;
+    final ImmutableSet<DexType> emulatedInterfaces;
+
+    private EmulatedInterfaceInfo(
+        MethodSignatures methodsToForward, ImmutableSet<DexType> emulatedInterfaces) {
+      this.signatures = methodsToForward;
+      this.emulatedInterfaces = emulatedInterfaces;
+    }
+
+    public EmulatedInterfaceInfo merge(EmulatedInterfaceInfo other) {
+      if (isEmpty()) {
+        return other;
+      }
+      if (other.isEmpty()) {
+        return this;
+      }
+      ImmutableSet.Builder<DexType> newEmulatedInterfaces = ImmutableSet.builder();
+      newEmulatedInterfaces.addAll(emulatedInterfaces);
+      newEmulatedInterfaces.addAll(other.emulatedInterfaces);
+      return new EmulatedInterfaceInfo(
+          signatures.merge(other.signatures), newEmulatedInterfaces.build());
+    }
+
+    public boolean isEmpty() {
+      assert !emulatedInterfaces.isEmpty() || signatures.isEmpty();
+      return emulatedInterfaces.isEmpty();
+    }
+  }
+
   // Helper to keep track of the direct active subclass and nearest program subclass for reporting.
   private static class ReportingContext {
 
@@ -181,10 +302,10 @@
   private final Map<DexClass, ClassInfo> classInfo = new IdentityHashMap<>();
 
   // Mapping from library classes to their information summary.
-  private final Map<DexLibraryClass, MethodSignatures> libraryClassInfo = new IdentityHashMap<>();
+  private final Map<DexLibraryClass, SignaturesInfo> libraryClassInfo = new IdentityHashMap<>();
 
   // Mapping from arbitrary interfaces to an information summary.
-  private final Map<DexClass, MethodSignatures> interfaceInfo = new IdentityHashMap<>();
+  private final Map<DexClass, SignaturesInfo> interfaceInfo = new IdentityHashMap<>();
 
   // Mapping from actual program classes to the synthesized forwarding methods to be created.
   private final Map<DexProgramClass, ProgramMethodSet> newSyntheticMethods =
@@ -228,35 +349,113 @@
   }
 
   // Computes the set of method signatures that may need forwarding methods on derived classes.
-  private MethodSignatures computeInterfaceInfo(DexClass iface, MethodSignatures signatures) {
+  private SignaturesInfo computeInterfaceInfo(DexClass iface, SignaturesInfo interfaceInfo) {
     assert iface.isInterface();
     assert iface.superType == dexItemFactory.objectType;
+    assert !rewriter.isEmulatedInterface(iface.type);
     // Add non-library default methods as well as those for desugared library classes.
     if (!iface.isLibraryClass() || (needsLibraryInfo() && rewriter.isInDesugaredLibrary(iface))) {
-      Set<Wrapper<DexMethod>> additions =
-          new HashSet<>(iface.getMethodCollection().numberOfVirtualMethods());
-      for (DexEncodedMethod method : iface.virtualMethods(DexEncodedMethod::isDefaultMethod)) {
-        additions.add(equivalence.wrap(method.method));
-      }
-      if (!additions.isEmpty()) {
-        signatures = signatures.merge(MethodSignatures.create(additions));
-      }
+      MethodSignatures signatures = getDefaultMethods(iface);
+      interfaceInfo = interfaceInfo.withSignatures(signatures);
     }
-    return signatures;
+    return interfaceInfo;
+  }
+
+  private SignaturesInfo computeEmulatedInterfaceInfo(
+      DexClass iface, SignaturesInfo interfaceInfo) {
+    assert iface.isInterface();
+    assert iface.superType == dexItemFactory.objectType;
+    assert rewriter.isEmulatedInterface(iface.type);
+    assert needsLibraryInfo();
+    MethodSignatures signatures = getDefaultMethods(iface);
+    EmulatedInterfaceInfo emulatedInterfaceInfo =
+        new EmulatedInterfaceInfo(signatures, ImmutableSet.of(iface.type));
+    return interfaceInfo.withEmulatedInterfaceInfo(emulatedInterfaceInfo);
+  }
+
+  private MethodSignatures getDefaultMethods(DexClass iface) {
+    assert iface.isInterface();
+    Set<Wrapper<DexMethod>> defaultMethods =
+        new HashSet<>(iface.getMethodCollection().numberOfVirtualMethods());
+    for (DexEncodedMethod method : iface.virtualMethods(DexEncodedMethod::isDefaultMethod)) {
+      defaultMethods.add(equivalence.wrap(method.method));
+    }
+    return MethodSignatures.create(defaultMethods);
   }
 
   // Computes the set of signatures of that may need forwarding methods on classes that derive
   // from a library class.
-  private MethodSignatures computeLibraryClassInfo(
-      DexLibraryClass clazz, MethodSignatures signatures) {
+  private SignaturesInfo computeLibraryClassInfo(DexLibraryClass clazz, SignaturesInfo signatures) {
     // The result is the identity as the library class does not itself contribute to the set.
     return signatures;
   }
 
   // The computation of a class information and the insertions of forwarding methods.
   private ClassInfo computeClassInfo(
-      DexClass clazz, ClassInfo superInfo, MethodSignatures signatures) {
+      DexClass clazz, ClassInfo superInfo, SignaturesInfo signatureInfo) {
     Builder<DexEncodedMethod> additionalForwards = ImmutableList.builder();
+    // First we deal with non-emulated interface desugaring.
+    resolveForwardingMethods(clazz, superInfo, signatureInfo.signatures, additionalForwards);
+    // Second we deal with emulated interface, if one method has override in the current class,
+    // we resolve them, else we propagate the emulated interface info down.
+    if (shouldResolveForwardingMethodsForEmulatedInterfaces(
+        clazz, signatureInfo.emulatedInterfaceInfo)) {
+      resolveForwardingMethods(
+          clazz,
+          superInfo,
+          signatureInfo.emulatedInterfaceSignaturesToForward(),
+          additionalForwards);
+      duplicateEmulatedInterfaces(clazz, signatureInfo.emulatedInterfaceInfo.emulatedInterfaces);
+      return ClassInfo.create(superInfo, additionalForwards.build(), EmulatedInterfaceInfo.EMPTY);
+    }
+    return ClassInfo.create(
+        superInfo, additionalForwards.build(), signatureInfo.emulatedInterfaceInfo);
+  }
+
+  // All classes implementing an emulated interface and overriding a default method should now
+  // implement the interface and the emulated one for correct emulated dispatch.
+  // The class signature won't include the correct type parameters for the duplicated interfaces,
+  // i.e., there will be foo.A instead of foo.A<K,V>, but such parameters are unused.
+  private void duplicateEmulatedInterfaces(
+      DexClass clazz, ImmutableSet<DexType> emulatedInterfaces) {
+    if (clazz.isNotProgramClass()) {
+      return;
+    }
+    // We need to introduce them in deterministic order for deterministic compilation.
+    ArrayList<DexType> sortedEmulatedInterfaces = new ArrayList<>(emulatedInterfaces);
+    Collections.sort(sortedEmulatedInterfaces, DexType::slowCompareTo);
+    List<GenericSignature.ClassTypeSignature> extraInterfaceSignatures = new ArrayList<>();
+    for (DexType extraInterface : sortedEmulatedInterfaces) {
+      extraInterfaceSignatures.add(
+          new GenericSignature.ClassTypeSignature(rewriter.getEmulatedInterface(extraInterface)));
+    }
+    clazz.asProgramClass().addExtraInterfaces(extraInterfaceSignatures);
+  }
+
+  // If any of the signature would lead to a different behavior than the default method on the
+  // emulated interface, we need to resolve the forwarding methods.
+  private boolean shouldResolveForwardingMethodsForEmulatedInterfaces(
+      DexClass clazz, EmulatedInterfaceInfo emulatedInterfaceInfo) {
+    AppInfoWithClassHierarchy appInfo = appView.appInfoForDesugaring();
+    for (Wrapper<DexMethod> signature : emulatedInterfaceInfo.signatures.signatures) {
+      ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(signature.get(), clazz);
+      if (resolutionResult.isFailedResolution()) {
+        return true;
+      }
+      DexClass resolvedHolder = resolutionResult.asSingleResolution().getResolvedHolder();
+      if (!resolvedHolder.isLibraryClass()
+          && !emulatedInterfaceInfo.emulatedInterfaces.contains(resolvedHolder.type)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private void resolveForwardingMethods(
+      DexClass clazz,
+      ClassInfo superInfo,
+      MethodSignatures signatures,
+      Builder<DexEncodedMethod> additionalForwards) {
     for (Wrapper<DexMethod> wrapper : signatures.signatures) {
       resolveForwardForSignature(
           clazz,
@@ -268,7 +467,6 @@
             }
           });
     }
-    return ClassInfo.create(superInfo, additionalForwards.build());
   }
 
   // Looks up a method signature from the point of 'clazz', if it can dispatch to a default method
@@ -439,7 +637,10 @@
     // a library class or is not, but cannot be both.
     ReportingContext thisContext = context.forClass(clazz);
     ClassInfo superInfo = visitClassInfo(clazz.superType, thisContext);
-    MethodSignatures signatures = visitLibraryClassInfo(clazz.superType);
+    SignaturesInfo signatures = visitLibraryClassInfo(clazz.superType);
+    // The class may inherit emulated interface info from its program superclass if the latter
+    // did not require to resolve the forwarding methods for emualted interfaces.
+    signatures = signatures.withEmulatedInterfaceInfo(superInfo.emulatedInterfaceInfo);
     assert superInfo.isEmpty() || signatures.isEmpty();
     for (DexType iface : clazz.interfaces.values) {
       signatures = signatures.merge(visitInterfaceInfo(iface, thisContext));
@@ -447,24 +648,24 @@
     return computeClassInfo(clazz, superInfo, signatures);
   }
 
-  private MethodSignatures visitLibraryClassInfo(DexType type) {
+  private SignaturesInfo visitLibraryClassInfo(DexType type) {
     // No desugaring required, no library class analysis.
     if (ignoreLibraryInfo()) {
-      return MethodSignatures.EMPTY;
+      return SignaturesInfo.EMPTY;
     }
     DexClass clazz = definitionOrNull(type, LibraryReportingContext.LIBRARY_CONTEXT);
-    return clazz == null ? MethodSignatures.EMPTY : visitLibraryClassInfo(clazz);
+    return clazz == null ? SignaturesInfo.EMPTY : visitLibraryClassInfo(clazz);
   }
 
-  private MethodSignatures visitLibraryClassInfo(DexClass clazz) {
+  private SignaturesInfo visitLibraryClassInfo(DexClass clazz) {
     assert !clazz.isInterface();
     return clazz.isLibraryClass()
         ? libraryClassInfo.computeIfAbsent(clazz.asLibraryClass(), this::visitLibraryClassInfoRaw)
-        : MethodSignatures.EMPTY;
+        : SignaturesInfo.EMPTY;
   }
 
-  private MethodSignatures visitLibraryClassInfoRaw(DexLibraryClass clazz) {
-    MethodSignatures signatures = visitLibraryClassInfo(clazz.superType);
+  private SignaturesInfo visitLibraryClassInfoRaw(DexLibraryClass clazz) {
+    SignaturesInfo signatures = visitLibraryClassInfo(clazz.superType);
     for (DexType iface : clazz.interfaces.values) {
       signatures =
           signatures.merge(visitInterfaceInfo(iface, LibraryReportingContext.LIBRARY_CONTEXT));
@@ -472,24 +673,26 @@
     return computeLibraryClassInfo(clazz, signatures);
   }
 
-  private MethodSignatures visitInterfaceInfo(DexType iface, ReportingContext context) {
+  private SignaturesInfo visitInterfaceInfo(DexType iface, ReportingContext context) {
     DexClass definition = definitionOrNull(iface, context);
-    return definition == null ? MethodSignatures.EMPTY : visitInterfaceInfo(definition, context);
+    return definition == null ? SignaturesInfo.EMPTY : visitInterfaceInfo(definition, context);
   }
 
-  private MethodSignatures visitInterfaceInfo(DexClass iface, ReportingContext context) {
+  private SignaturesInfo visitInterfaceInfo(DexClass iface, ReportingContext context) {
     if (iface.isLibraryClass() && ignoreLibraryInfo()) {
-      return MethodSignatures.EMPTY;
+      return SignaturesInfo.EMPTY;
     }
     return interfaceInfo.computeIfAbsent(iface, key -> visitInterfaceInfoRaw(key, context));
   }
 
-  private MethodSignatures visitInterfaceInfoRaw(DexClass iface, ReportingContext context) {
+  private SignaturesInfo visitInterfaceInfoRaw(DexClass iface, ReportingContext context) {
     ReportingContext thisContext = context.forClass(iface);
-    MethodSignatures signatures = MethodSignatures.EMPTY;
+    SignaturesInfo interfaceInfo = SignaturesInfo.EMPTY;
     for (DexType superiface : iface.interfaces.values) {
-      signatures = signatures.merge(visitInterfaceInfo(superiface, thisContext));
+      interfaceInfo = interfaceInfo.merge(visitInterfaceInfo(superiface, thisContext));
     }
-    return computeInterfaceInfo(iface, signatures);
+    return rewriter.isEmulatedInterface(iface.type)
+        ? computeEmulatedInterfaceInfo(iface, interfaceInfo)
+        : computeInterfaceInfo(iface, interfaceInfo);
   }
 }
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
deleted file mode 100644
index 6f72091..0000000
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryEmulatedInterfaceDuplicator.java
+++ /dev/null
@@ -1,124 +0,0 @@
-// 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();
-    for (int i = 0; i < clazz.interfaces.size(); i++) {
-      DexType itf = clazz.interfaces.values[i];
-      assert emulatedInterfaces.containsKey(itf);
-      List<FieldTypeSignature> typeArguments;
-      if (classSignature == null) {
-        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);
-  }
-
-  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) {
-        processSuperType(itf, extraInterfaces, workList);
-      }
-    }
-    extraInterfaces = removeDuplicates(extraInterfaces);
-    List<ClassTypeSignature> extraInterfaceSignatures = new ArrayList<>();
-    for (DexType extraInterface : extraInterfaces) {
-      extraInterfaceSignatures.add(new ClassTypeSignature(extraInterface));
-    }
-    clazz.addExtraInterfaces(extraInterfaceSignatures);
-  }
-
-  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/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 61a5c56..ee05401 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
@@ -26,8 +26,10 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.GenericSignature;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -207,6 +209,10 @@
     return emulatedInterfaces.containsKey(itf);
   }
 
+  DexType getEmulatedInterface(DexType itf) {
+    return emulatedInterfaces.get(itf);
+  }
+
   // Rewrites the references to static and default interface methods.
   // NOTE: can be called for different methods concurrently.
   public void rewriteMethodReferences(DexEncodedMethod encodedMethod, IRCode code) {
@@ -430,8 +436,19 @@
           DexMethod invokedMethod = invokeMethod.getInvokedMethod();
           DexType dexType = maximallySpecificEmulatedInterfaceOrNull(invokedMethod);
           if (dexType != null) {
-            rewriteCurrentInstructionToEmulatedInterfaceCall(
-                dexType, invokedMethod, invokeMethod, instructions);
+            // The call potentially ends up in a library class, in which case we need to rewrite,
+            // since the code may be in the desugared library.
+            SingleResolutionResult resolution =
+                appView
+                    .appInfoForDesugaring()
+                    .resolveMethod(invokedMethod, invokeMethod.getInterfaceBit())
+                    .asSingleResolution();
+            if (resolution != null
+                && (resolution.getResolvedHolder().isLibraryClass()
+                    || appView.options().isDesugaredLibraryCompilation())) {
+              rewriteCurrentInstructionToEmulatedInterfaceCall(
+                  dexType, invokedMethod, invokeMethod, instructions);
+            }
           }
         }
       }
@@ -448,17 +465,6 @@
     if (dexClass == null) {
       return null;
     }
-    // TODO(b/120884788): Make sure program class are looked up before library class.
-    // Since program classes are desugared, no need to rewrite invokes which can target only
-    // program types.
-    if (!appView.options().isDesugaredLibraryCompilation() && !dexClass.isLibraryClass()) {
-      return null;
-    }
-    // Since desugared library classes are desugared, no need to rewrite invokes which can target
-    // only such classes program types.
-    if (appView.rewritePrefix.hasRewrittenType(dexClass.type, appView)) {
-      return null;
-    }
     DexEncodedMethod singleTarget = null;
     if (dexClass.isInterface()) {
       // Look for exact method on the interface.
@@ -934,11 +940,11 @@
     if (appView.options().isDesugaredLibraryCompilation()) {
       generateEmulateInterfaceLibrary(builder);
     }
-    new DesugaredLibraryEmulatedInterfaceDuplicator(appView).duplicateEmulatedInterfaces();
 
     // Process all classes first. Add missing forwarding methods to
     // replace desugared default interface methods.
     processClasses(builder, flavour, synthesizedMethods::add);
+    transformEmulatedInterfaces();
 
     // Process interfaces, create companion or dispatch class if needed, move static
     // methods to companion class, copy default interface methods to companion classes,
@@ -976,6 +982,40 @@
     clear();
   }
 
+  private void transformEmulatedInterfaces() {
+    for (DexType dexType : emulatedInterfaces.keySet()) {
+      DexClass dexClass = appView.definitionFor(dexType);
+      if (dexClass != null && dexClass.isProgramClass()) {
+        transformEmulatedInterfaces(dexClass.asProgramClass());
+      }
+    }
+  }
+
+  // The method transforms emulated interface such as they implement the rewritten version
+  // of each emulated interface they implement. Such change should have no effect on the look-up
+  // results, since each class implementing an emulated interface should also implement the
+  // rewritten one.
+  private void transformEmulatedInterfaces(DexProgramClass clazz) {
+    List<GenericSignature.ClassTypeSignature> newInterfaces = new ArrayList<>();
+    GenericSignature.ClassSignature classSignature = clazz.getClassSignature();
+    for (int i = 0; i < clazz.interfaces.size(); i++) {
+      DexType itf = clazz.interfaces.values[i];
+      assert emulatedInterfaces.containsKey(itf);
+      List<GenericSignature.FieldTypeSignature> typeArguments;
+      if (classSignature == null) {
+        typeArguments = Collections.emptyList();
+      } else {
+        GenericSignature.ClassTypeSignature classTypeSignature =
+            classSignature.superInterfaceSignatures().get(i);
+        assert itf == classTypeSignature.type();
+        typeArguments = classTypeSignature.typeArguments();
+      }
+      newInterfaces.add(
+          new GenericSignature.ClassTypeSignature(emulatedInterfaces.get(itf), typeArguments));
+    }
+    clazz.replaceInterfaces(newInterfaces);
+  }
+
   private void clear() {
     this.cache.clear();
     this.synthesizedMethods.clear();
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionForwardingTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionForwardingTest.java
index 42582e6..0689a75 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionForwardingTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionForwardingTest.java
@@ -4,15 +4,10 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary;
 
-import static junit.framework.TestCase.assertTrue;
-
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
@@ -55,7 +50,6 @@
         .setMinApi(parameters.getApiLevel())
         .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
         .compile()
-        .inspect(this::assertForwardingMethods)
         .addDesugaredCoreLibraryRunClassPath(
             this::buildDesugaredLibrary,
             parameters.getApiLevel(),
@@ -85,53 +79,6 @@
             StringUtils.lines("false", "false", "false", "false", "false", "false"));
   }
 
-  private void assertForwardingMethods(CodeInspector inspector) {
-    if (parameters.getApiLevel().getLevel() >= AndroidApiLevel.N.getLevel()) {
-      return;
-    }
-    ClassSubject cal = inspector.clazz(CustomArrayList.class);
-    MethodSubject spliteratorCal = cal.method("j$.util.Spliterator", "spliterator");
-    assertTrue(spliteratorCal.isPresent());
-    assertTrue(
-        spliteratorCal
-            .streamInstructions()
-            .anyMatch(i -> i.isInvokeStatic() && i.toString().contains("List$-CC")));
-    MethodSubject streamCal = cal.method("j$.util.stream.Stream", "stream");
-    assertTrue(streamCal.isPresent());
-    assertTrue(
-        streamCal
-            .streamInstructions()
-            .anyMatch(i -> i.isInvokeStatic() && i.toString().contains("Collection$-CC")));
-
-    ClassSubject clhs = inspector.clazz(CustomLinkedHashSet.class);
-    MethodSubject spliteratorClhs = clhs.method("j$.util.Spliterator", "spliterator");
-    assertTrue(spliteratorClhs.isPresent());
-    assertTrue(
-        spliteratorClhs
-            .streamInstructions()
-            .anyMatch(i -> i.isInvokeStatic() && i.toString().contains("DesugarLinkedHashSet")));
-    MethodSubject streamClhs = clhs.method("j$.util.stream.Stream", "stream");
-    assertTrue(streamClhs.isPresent());
-    assertTrue(
-        streamClhs
-            .streamInstructions()
-            .anyMatch(i -> i.isInvokeStatic() && i.toString().contains("Collection$-CC")));
-
-    ClassSubject cl = inspector.clazz(CustomList.class);
-    MethodSubject spliteratorCl = cl.method("j$.util.Spliterator", "spliterator");
-    assertTrue(spliteratorCl.isPresent());
-    assertTrue(
-        spliteratorCl
-            .streamInstructions()
-            .anyMatch(i -> i.isInvokeStatic() && i.toString().contains("List$-CC")));
-    MethodSubject streamCl = cl.method("j$.util.stream.Stream", "stream");
-    assertTrue(streamCl.isPresent());
-    assertTrue(
-        streamCl
-            .streamInstructions()
-            .anyMatch(i -> i.isInvokeStatic() && i.toString().contains("Collection$-CC")));
-  }
-
   static class Executor {
 
     public static void main(String[] args) {
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionTest.java
index 2330854..3df7246 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/CustomCollectionTest.java
@@ -24,7 +24,6 @@
 import java.util.stream.Stream;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -60,21 +59,18 @@
             .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
             .compile()
             .assertNoMessages()
-            .inspect(
-                inspector -> {
-                  this.assertCustomCollectionCallsCorrect(inspector, false);
-                })
+            .inspect(this::assertCustomCollectionCallsCorrect)
             .addDesugaredCoreLibraryRunClassPath(
                 this::buildDesugaredLibrary,
                 parameters.getApiLevel(),
                 keepRuleConsumer.get(),
                 shrinkDesugaredLibrary)
-            .run(parameters.getRuntime(), EXECUTOR)
+            .run(parameters.getRuntime(), Executor.class)
             .assertSuccess();
-    assertResultCorrect(d8TestRunResult.getStdOut(), d8TestRunResult.getStdErr());
+    assertResultCorrect(d8TestRunResult.getStdOut());
   }
 
-  private void assertResultCorrect(String stdOut, String stdErr) {
+  private void assertResultCorrect(String stdOut) {
     if (requiresEmulatedInterfaceCoreLibDesugaring(parameters) && !shrinkDesugaredLibrary) {
       // When shrinking the class names are not printed correctly anymore due to minification.
       // Expected output is emulated interfaces expected output.
@@ -92,29 +88,20 @@
             .addKeepClassAndMembersRules(Executor.class)
             .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
             .compile()
-            .inspect(
-                inspector -> {
-                  this.assertCustomCollectionCallsCorrect(inspector, true);
-                })
+            .inspect(this::assertCustomCollectionCallsCorrect)
             .addDesugaredCoreLibraryRunClassPath(
                 this::buildDesugaredLibrary,
                 parameters.getApiLevel(),
                 keepRuleConsumer.get(),
                 shrinkDesugaredLibrary)
-            .run(parameters.getRuntime(), EXECUTOR)
+            .run(parameters.getRuntime(), Executor.class)
             .assertSuccess();
-    assertResultCorrect(r8TestRunResult.getStdOut(), r8TestRunResult.getStdErr());
+    assertResultCorrect(r8TestRunResult.getStdOut());
   }
 
-  private void assertCustomCollectionCallsCorrect(CodeInspector inspector, boolean r8) {
+  private void assertCustomCollectionCallsCorrect(CodeInspector inspector) {
     MethodSubject direct = inspector.clazz(EXECUTOR).uniqueMethodWithName("directTypes");
-    // TODO(b/134732760): Due to memberRebinding, R8 is not as precise as D8 regarding
-    // desugaring of invokes. This will be fixed when creation of desugared method is moved
-    // ahead of R8 compilation pipeline.
-    if (!r8) {
-      Assert.assertFalse(
-          direct.streamInstructions().anyMatch(instr -> instr.toString().contains("$-EL")));
-    } else if (requiresEmulatedInterfaceCoreLibDesugaring(parameters)) {
+    if (requiresEmulatedInterfaceCoreLibDesugaring(parameters)) {
       assertTrue(
           direct
               .streamInstructions()
@@ -140,22 +127,17 @@
                 instr ->
                     instr.toString().contains("$-EL")
                         || instr.toString().contains("Comparator$-CC")));
-    inherited.streamInstructions().forEach(x -> assertInheritedDispatchCorrect(x, r8));
+    inherited.streamInstructions().forEach(this::assertInheritedDispatchCorrect);
   }
 
-  private void assertInheritedDispatchCorrect(InstructionSubject instructionSubject, boolean r8) {
+  private void assertInheritedDispatchCorrect(InstructionSubject instructionSubject) {
     if (!instructionSubject.isConstString(JumboStringMode.ALLOW)) {
-      for (String s : new String[] {"stream", "parallelStream", "spliterator", "sort"}) {
+      for (String s : new String[] {">stream", "spliterator", "sort"}) {
         if (instructionSubject.toString().contains(s)) {
-          if (!r8 || instructionSubject.isInvokeStatic()) {
             assertTrue(instructionSubject.isInvokeStatic());
             assertTrue(
                 instructionSubject.toString().contains("$-EL")
                     || instructionSubject.toString().contains("Comparator$-CC"));
-          } else {
-            // Has been devirtualized.
-            assertTrue(instructionSubject.isInvokeVirtual());
-          }
         }
       }
     }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/NoDesugaredLibraryDexFileTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/NoDesugaredLibraryDexFileTest.java
new file mode 100644
index 0000000..a6ea994
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/NoDesugaredLibraryDexFileTest.java
@@ -0,0 +1,200 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class NoDesugaredLibraryDexFileTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public NoDesugaredLibraryDexFileTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testCustomCollectionD8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8()
+        .addInnerClasses(NoDesugaredLibraryDexFileTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .inspect(this::assertNoForwardingStreamMethod)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("1", "0");
+    assertTrue(keepRuleConsumer.get().isEmpty());
+  }
+
+  @Test
+  public void testCustomCollectionR8() throws Exception {
+    Assume.assumeTrue(requiresEmulatedInterfaceCoreLibDesugaring(parameters));
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(parameters.getBackend())
+        .addInnerClasses(NoDesugaredLibraryDexFileTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepClassAndMembersRules(Executor.class)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .inspect(this::assertNoForwardingStreamMethod)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("1", "0");
+    assertTrue(keepRuleConsumer.get().isEmpty());
+  }
+
+  private void assertNoForwardingStreamMethod(CodeInspector inspector) {
+    assertTrue(inspector.clazz(CustomArrayList.class).uniqueMethodWithName("stream").isAbsent());
+    assertTrue(inspector.clazz(CustomSortedSet.class).uniqueMethodWithName("stream").isAbsent());
+  }
+
+  static class Executor {
+
+    // No method here is using any emulated interface default method, so, there is no need for
+    // the desugared library dex file despite desugared library being enabled.
+    public static void main(String[] args) {
+      ArrayList<Object> cArrayList = new CustomArrayList<>();
+      SortedSet<Object> cSortedSet = new CustomSortedSet<>();
+      cArrayList.add(1);
+      cSortedSet.add(1);
+      System.out.println(cArrayList.size());
+      System.out.println(cSortedSet.size());
+    }
+  }
+
+  // Extends directly a core library class which implements other library interfaces.
+  private static class CustomArrayList<E> extends ArrayList<E> {}
+
+  // Implements directly a core library interface which implements other library interfaces.
+  static class CustomSortedSet<E> implements SortedSet<E> {
+
+    @Nullable
+    @Override
+    public Comparator<? super E> comparator() {
+      return null;
+    }
+
+    @NotNull
+    @Override
+    public SortedSet<E> subSet(E fromElement, E toElement) {
+      return new CustomSortedSet<>();
+    }
+
+    @NotNull
+    @Override
+    public SortedSet<E> headSet(E toElement) {
+      return new CustomSortedSet<>();
+    }
+
+    @NotNull
+    @Override
+    public SortedSet<E> tailSet(E fromElement) {
+      return new CustomSortedSet<>();
+    }
+
+    @Override
+    public E first() {
+      return null;
+    }
+
+    @Override
+    public E last() {
+      return null;
+    }
+
+    @Override
+    public int size() {
+      return 0;
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return false;
+    }
+
+    @Override
+    public boolean contains(Object o) {
+      return false;
+    }
+
+    @NotNull
+    @Override
+    public Iterator<E> iterator() {
+      return Collections.emptyIterator();
+    }
+
+    @NotNull
+    @Override
+    public Object[] toArray() {
+      return new Object[0];
+    }
+
+    @NotNull
+    @Override
+    public <T> T[] toArray(@NotNull T[] a) {
+      return a;
+    }
+
+    @Override
+    public boolean add(Object o) {
+      return false;
+    }
+
+    @Override
+    public boolean remove(Object o) {
+      return false;
+    }
+
+    @Override
+    public boolean addAll(@NotNull Collection c) {
+      return false;
+    }
+
+    @Override
+    public void clear() {}
+
+    @Override
+    public boolean removeAll(@NotNull Collection c) {
+      return false;
+    }
+
+    @Override
+    public boolean retainAll(@NotNull Collection c) {
+      return false;
+    }
+
+    @Override
+    public boolean containsAll(@NotNull Collection c) {
+      return false;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/NoForwardingMethodsTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/NoForwardingMethodsTest.java
new file mode 100644
index 0000000..98ad7dc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/NoForwardingMethodsTest.java
@@ -0,0 +1,206 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.SortedSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+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 NoForwardingMethodsTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public NoForwardingMethodsTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testCustomCollectionD8() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8()
+        .addInnerClasses(NoForwardingMethodsTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .inspect(this::assertNoForwardingStreamMethod)
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("str:1", "0");
+  }
+
+  @Test
+  public void testCustomCollectionR8() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(parameters.getBackend())
+        .addInnerClasses(NoForwardingMethodsTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepClassAndMembersRules(Executor.class)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .inspect(this::assertNoForwardingStreamMethod)
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("str:1", "0");
+  }
+
+  private void assertNoForwardingStreamMethod(CodeInspector inspector) {
+    assertTrue(inspector.clazz(CustomArrayList.class).uniqueMethodWithName("stream").isAbsent());
+    assertTrue(inspector.clazz(CustomSortedSet.class).uniqueMethodWithName("stream").isAbsent());
+  }
+
+  static class Executor {
+
+    // The main method is using stream, but since there are no overrides, the classes should not
+    // have any forwarding method.
+    public static void main(String[] args) {
+      ArrayList<Object> cArrayList = new CustomArrayList<>();
+      SortedSet<Object> cSortedSet = new CustomSortedSet<>();
+      cArrayList.add(1);
+      cSortedSet.add(1);
+      System.out.println(cArrayList.stream().map(i -> "str:" + i).toArray()[0]);
+      System.out.println(cSortedSet.stream().filter(Objects::isNull).count());
+    }
+  }
+
+  // Extends directly a core library class which implements other library interfaces.
+  private static class CustomArrayList<E> extends ArrayList<E> {}
+
+  // Implements directly a core library interface which implements other library interfaces.
+  static class CustomSortedSet<E> implements SortedSet<E> {
+
+    @Nullable
+    @Override
+    public Comparator<? super E> comparator() {
+      return null;
+    }
+
+    @NotNull
+    @Override
+    public SortedSet<E> subSet(E fromElement, E toElement) {
+      return new CustomSortedSet<>();
+    }
+
+    @NotNull
+    @Override
+    public SortedSet<E> headSet(E toElement) {
+      return new CustomSortedSet<>();
+    }
+
+    @NotNull
+    @Override
+    public SortedSet<E> tailSet(E fromElement) {
+      return new CustomSortedSet<>();
+    }
+
+    @Override
+    public E first() {
+      return null;
+    }
+
+    @Override
+    public E last() {
+      return null;
+    }
+
+    @Override
+    public int size() {
+      return 0;
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return false;
+    }
+
+    @Override
+    public boolean contains(Object o) {
+      return false;
+    }
+
+    @NotNull
+    @Override
+    public Iterator<E> iterator() {
+      return Collections.emptyIterator();
+    }
+
+    @NotNull
+    @Override
+    public Object[] toArray() {
+      return new Object[0];
+    }
+
+    @NotNull
+    @Override
+    public <T> T[] toArray(@NotNull T[] a) {
+      return a;
+    }
+
+    @Override
+    public boolean add(Object o) {
+      return false;
+    }
+
+    @Override
+    public boolean remove(Object o) {
+      return false;
+    }
+
+    @Override
+    public boolean addAll(@NotNull Collection c) {
+      return false;
+    }
+
+    @Override
+    public void clear() {}
+
+    @Override
+    public boolean removeAll(@NotNull Collection c) {
+      return false;
+    }
+
+    @Override
+    public boolean retainAll(@NotNull Collection c) {
+      return false;
+    }
+
+    @Override
+    public boolean containsAll(@NotNull Collection c) {
+      return false;
+    }
+  }
+}