Merge pie-platform-release to aosp-master - DO NOT MERGE

Change-Id: Ibbec996b0c2308d425f6a60a833521735c5433ad
diff --git a/Android.bp b/Android.bp
index f770906..0f05c97 100644
--- a/Android.bp
+++ b/Android.bp
@@ -28,8 +28,8 @@
     },
 
     // Use Dagger2 annotation processor
-    annotation_processors: ["dagger2-auto-value-host"],
-    libs: ["dagger2-auto-value-host"],
+    annotation_processors: ["dagger2-auto-value"],
+    libs: ["dagger2-auto-value"],
     // b/25860419: annotation processors must be explicitly specified for grok
     annotation_processor_classes: [
         "com.google.auto.value.processor.AutoValueProcessor",
diff --git a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
index 783069f..ce36071 100644
--- a/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
+++ b/java/com/google/devtools/build/android/desugar/BytecodeTypeInference.java
@@ -19,6 +19,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.BitFlags;
 import java.util.ArrayList;
 import java.util.Optional;
 import javax.annotation.Nullable;
diff --git a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
index bae5251..aff9bab 100644
--- a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
+++ b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
@@ -13,6 +13,9 @@
 // limitations under the License.
 package com.google.devtools.build.android.desugar;
 
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
 import java.io.IOException;
 import java.io.InputStream;
 import javax.annotation.Nullable;
diff --git a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
index cb62deb..2724454 100644
--- a/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
+++ b/java/com/google/devtools/build/android/desugar/ClassVsInterface.java
@@ -16,6 +16,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.devtools.build.android.desugar.io.BitFlags;
 import java.util.HashMap;
 import javax.annotation.Nullable;
 import org.objectweb.asm.ClassReader;
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
new file mode 100644
index 0000000..381a344
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
@@ -0,0 +1,96 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Rewriter of default and static interface methods defined in some core libraries.
+ *
+ * <p>This is conceptually similar to call site rewriting in {@link InterfaceDesugaring} but here
+ * we're doing it for certain bootclasspath methods and in particular for invokeinterface and
+ * invokevirtual, which are ignored in regular {@link InterfaceDesugaring}.
+ */
+public class CoreLibraryInvocationRewriter extends ClassVisitor {
+
+  private final CoreLibrarySupport support;
+
+  public CoreLibraryInvocationRewriter(ClassVisitor cv, CoreLibrarySupport support) {
+    super(Opcodes.ASM6, cv);
+    this.support = support;
+  }
+
+  @Override
+  public MethodVisitor visitMethod(
+      int access, String name, String desc, String signature, String[] exceptions) {
+    MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions);
+    return result != null ? new CoreLibraryMethodInvocationRewriter(result) : null;
+  }
+
+  private class CoreLibraryMethodInvocationRewriter extends MethodVisitor {
+    public CoreLibraryMethodInvocationRewriter(MethodVisitor mv) {
+      super(Opcodes.ASM6, mv);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      Class<?> coreInterface =
+          support.getCoreInterfaceRewritingTarget(opcode, owner, name, desc, itf);
+
+      if (coreInterface != null) {
+        String coreInterfaceName = coreInterface.getName().replace('.', '/');
+        name =
+            InterfaceDesugaring.normalizeInterfaceMethodName(
+                name, name.startsWith("lambda$"), opcode == Opcodes.INVOKESTATIC);
+        if (opcode == Opcodes.INVOKESTATIC) {
+          checkState(owner.equals(coreInterfaceName));
+        } else {
+          desc = InterfaceDesugaring.companionDefaultMethodDescriptor(coreInterfaceName, desc);
+        }
+
+        if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) {
+          checkArgument(itf || opcode == Opcodes.INVOKESPECIAL,
+              "Expected interface to rewrite %s.%s : %s", owner, name, desc);
+          owner = coreInterface.isInterface()
+              ? InterfaceDesugaring.getCompanionClassName(coreInterfaceName)
+              : checkNotNull(support.getMoveTarget(coreInterfaceName, name));
+        } else {
+          checkState(coreInterface.isInterface());
+          owner = coreInterfaceName + "$$Dispatch";
+        }
+
+        opcode = Opcodes.INVOKESTATIC;
+        itf = false;
+      } else {
+        String newOwner = support.getMoveTarget(owner, name);
+        if (newOwner != null) {
+          if (opcode != Opcodes.INVOKESTATIC) {
+            // assuming a static method
+            desc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc);
+            opcode = Opcodes.INVOKESTATIC;
+          }
+          owner = newOwner;
+          itf = false; // assuming a class
+        }
+      }
+      super.visitMethodInsn(opcode, owner, name, desc, itf);
+    }
+  }
+}
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
new file mode 100644
index 0000000..f247074
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
@@ -0,0 +1,521 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.stream.Stream.concat;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.errorprone.annotations.Immutable;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Helper that keeps track of which core library classes and methods we want to rewrite.
+ */
+class CoreLibrarySupport {
+
+  private static final Object[] EMPTY_FRAME = new Object[0];
+  private static final String[] EMPTY_LIST = new String[0];
+
+  private final CoreLibraryRewriter rewriter;
+  private final ClassLoader targetLoader;
+  /** Internal name prefixes that we want to move to a custom package. */
+  private final ImmutableSet<String> renamedPrefixes;
+  private final ImmutableSet<String> excludeFromEmulation;
+  /** Internal names of interfaces whose default and static interface methods we'll emulate. */
+  private final ImmutableSet<Class<?>> emulatedInterfaces;
+  /** Map from {@code owner#name} core library members to their new owners. */
+  private final ImmutableMap<String, String> memberMoves;
+
+  /** For the collection of definitions of emulated default methods (deterministic iteration). */
+  private final Multimap<String, EmulatedMethod> emulatedDefaultMethods =
+      LinkedHashMultimap.create();
+
+  public CoreLibrarySupport(
+      CoreLibraryRewriter rewriter,
+      ClassLoader targetLoader,
+      List<String> renamedPrefixes,
+      List<String> emulatedInterfaces,
+      List<String> memberMoves,
+      List<String> excludeFromEmulation) {
+    this.rewriter = rewriter;
+    this.targetLoader = targetLoader;
+    checkArgument(
+        renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes);
+    this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes);
+    this.excludeFromEmulation = ImmutableSet.copyOf(excludeFromEmulation);
+
+    ImmutableSet.Builder<Class<?>> classBuilder = ImmutableSet.builder();
+    for (String itf : emulatedInterfaces) {
+      checkArgument(itf.startsWith("java/util/"), itf);
+      Class<?> clazz = loadFromInternal(rewriter.getPrefix() + itf);
+      checkArgument(clazz.isInterface(), itf);
+      classBuilder.add(clazz);
+    }
+    this.emulatedInterfaces = classBuilder.build();
+
+    // We can call isRenamed and rename below b/c we initialized the necessary fields above
+    // Use LinkedHashMap to tolerate identical duplicates
+    LinkedHashMap<String, String> movesBuilder = new LinkedHashMap<>();
+    Splitter splitter = Splitter.on("->").trimResults().omitEmptyStrings();
+    for (String move : memberMoves) {
+      List<String> pair = splitter.splitToList(move);
+      checkArgument(pair.size() == 2, "Doesn't split as expected: %s", move);
+      checkArgument(pair.get(0).startsWith("java/"), "Unexpected member: %s", move);
+      int sep = pair.get(0).indexOf('#');
+      checkArgument(sep > 0 && sep == pair.get(0).lastIndexOf('#'), "invalid member: %s", move);
+      checkArgument(!isRenamedCoreLibrary(pair.get(0).substring(0, sep)),
+          "Original renamed, no need to move it: %s", move);
+      checkArgument(isRenamedCoreLibrary(pair.get(1)), "Target not renamed: %s", move);
+      checkArgument(!this.excludeFromEmulation.contains(pair.get(0)),
+          "Retargeted invocation %s shouldn't overlap with excluded", move);
+
+      String value = renameCoreLibrary(pair.get(1));
+      String existing = movesBuilder.put(pair.get(0), value);
+      checkArgument(existing == null || existing.equals(value),
+          "Two move destinations %s and %s configured for %s", existing, value, pair.get(0));
+    }
+    this.memberMoves = ImmutableMap.copyOf(movesBuilder);
+  }
+
+  public boolean isRenamedCoreLibrary(String internalName) {
+    String unprefixedName = rewriter.unprefix(internalName);
+    if (!unprefixedName.startsWith("java/") || renamedPrefixes.isEmpty()) {
+      return false; // shortcut
+    }
+    // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as
+    // configured prefixes
+    return looksGenerated(unprefixedName)
+        || renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix));
+  }
+
+  public String renameCoreLibrary(String internalName) {
+    internalName = rewriter.unprefix(internalName);
+    return (internalName.startsWith("java/"))
+        ? "j$/" + internalName.substring(/* cut away "java/" prefix */ 5)
+        : internalName;
+  }
+
+  @Nullable
+  public String getMoveTarget(String owner, String name) {
+    return memberMoves.get(rewriter.unprefix(owner) + '#' + name);
+  }
+
+  /**
+   * Returns {@code true} for java.* classes or interfaces that are subtypes of emulated interfaces.
+   * Note that implies that this method always returns {@code false} for user-written classes.
+   */
+  public boolean isEmulatedCoreClassOrInterface(String internalName) {
+    return getEmulatedCoreClassOrInterface(internalName) != null;
+  }
+
+  /** Includes the given method definition in any applicable core interface emulation logic. */
+  public void registerIfEmulatedCoreInterface(
+      int access,
+      String owner,
+      String name,
+      String desc,
+      String[] exceptions) {
+    Class<?> emulated = getEmulatedCoreClassOrInterface(owner);
+    if (emulated == null) {
+      return;
+    }
+    checkArgument(emulated.isInterface(), "Shouldn't be called for a class: %s.%s", owner, name);
+    checkArgument(
+        BitFlags.noneSet(
+            access,
+            Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE),
+        "Should only be called for default methods: %s.%s", owner, name);
+    emulatedDefaultMethods.put(
+        name + ":" + desc, EmulatedMethod.create(access, emulated, name, desc, exceptions));
+  }
+
+  /**
+   * If the given invocation needs to go through a companion class of an emulated or renamed
+   * core interface, this methods returns that interface.  This is a helper method for
+   * {@link CoreLibraryInvocationRewriter}.
+   *
+   * <p>This method can only return non-{@code null} if {@code owner} is a core library type.
+   * It usually returns an emulated interface, unless the given invocation is a super-call to a
+   * core class's implementation of an emulated method that's being moved (other implementations
+   * of emulated methods in core classes are ignored). In that case the class is returned and the
+   * caller can use {@link #getMoveTarget} to find out where to redirect the invokespecial to.
+   */
+  // TODO(kmb): Rethink this API and consider combining it with getMoveTarget().
+  @Nullable
+  public Class<?> getCoreInterfaceRewritingTarget(
+      int opcode, String owner, String name, String desc, boolean itf) {
+    if (looksGenerated(owner)) {
+      // Regular desugaring handles generated classes, no emulation is needed
+      return null;
+    }
+    if (!itf && opcode == Opcodes.INVOKESTATIC) {
+      // Ignore static invocations on classes--they never need rewriting (unless moved but that's
+      // handled separately).
+      return null;
+    }
+    if ("<init>".equals(name)) {
+      return null; // Constructors aren't rewritten
+    }
+
+    Class<?> clazz;
+    if (isRenamedCoreLibrary(owner)) {
+      // For renamed invocation targets we just need to do what InterfaceDesugaring does, that is,
+      // only worry about invokestatic and invokespecial interface invocations; nothing to do for
+      // classes and invokeinterface.  InterfaceDesugaring ignores bootclasspath interfaces,
+      // so we have to do its work here for renamed interfaces.
+      if (itf
+          && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) {
+        clazz = loadFromInternal(owner);
+      } else {
+        return null;
+      }
+    } else {
+      // If not renamed, see if the owner needs emulation.
+      clazz = getEmulatedCoreClassOrInterface(owner);
+      if (clazz == null) {
+        return null;
+      }
+    }
+    checkArgument(itf == clazz.isInterface(), "%s expected to be interface: %s", owner, itf);
+
+    if (opcode == Opcodes.INVOKESTATIC) {
+      // Static interface invocation always goes to the given owner
+      checkState(itf); // we should've bailed out above.
+      return clazz;
+    }
+
+    // See if the invoked method is a default method, which will need rewriting.  For invokespecial
+    // we can only get here if its a default method, and invokestatic we handled above.
+    Method callee = findInterfaceMethod(clazz, name, desc);
+    if (callee != null && callee.isDefault()) {
+      if (isExcluded(callee)) {
+        return null;
+      }
+
+      if (!itf && opcode == Opcodes.INVOKESPECIAL) {
+        // See if the invoked implementation is moved; note we ignore all other overrides in classes
+        Class<?> impl = clazz; // we know clazz is not an interface because !itf
+        while (impl != null) {
+          String implName = impl.getName().replace('.', '/');
+          if (getMoveTarget(implName, name) != null) {
+            return impl;
+          }
+          impl = impl.getSuperclass();
+        }
+      }
+
+      Class<?> result = callee.getDeclaringClass();
+      if (isRenamedCoreLibrary(result.getName().replace('.', '/'))
+          || emulatedInterfaces.stream().anyMatch(emulated -> emulated.isAssignableFrom(result))) {
+        return result;
+      }
+      // We get here if the declaring class is a supertype of an emulated interface.  In that case
+      // use the emulated interface instead (since we don't desugar the supertype).  Fail in case
+      // there are multiple possibilities.
+      Iterator<Class<?>> roots =
+          emulatedInterfaces
+              .stream()
+              .filter(
+                  emulated -> emulated.isAssignableFrom(clazz) && result.isAssignableFrom(emulated))
+              .iterator();
+      checkState(roots.hasNext()); // must exist
+      Class<?> substitute = roots.next();
+      checkState(!roots.hasNext(), "Ambiguous emulation substitute: %s", callee);
+      return substitute;
+    } else {
+      checkArgument(!itf || opcode != Opcodes.INVOKESPECIAL,
+          "Couldn't resolve interface super call %s.super.%s : %s", owner, name, desc);
+    }
+    return null;
+  }
+
+  /**
+   * Returns the given class if it's a core library class or interface with emulated default
+   * methods.  This is equivalent to calling {@link #isEmulatedCoreClassOrInterface} and then
+   * just loading the class (using the target class loader).
+   */
+  public Class<?> getEmulatedCoreClassOrInterface(String internalName) {
+    if (looksGenerated(internalName)) {
+      // Regular desugaring handles generated classes, no emulation is needed
+      return null;
+    }
+    {
+      String unprefixedOwner = rewriter.unprefix(internalName);
+      if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) {
+        return null;
+      }
+    }
+
+    Class<?> clazz = loadFromInternal(internalName);
+    if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) {
+      return clazz;
+    }
+    return null;
+  }
+
+  public void makeDispatchHelpers(GeneratedClassStore store) {
+    HashMap<Class<?>, ClassVisitor> dispatchHelpers = new HashMap<>();
+    for (Collection<EmulatedMethod> group : emulatedDefaultMethods.asMap().values()) {
+      checkState(!group.isEmpty());
+      Class<?> root = group
+          .stream()
+          .map(EmulatedMethod::owner)
+          .max(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+          .get();
+      checkState(group.stream().map(m -> m.owner()).allMatch(o -> root.isAssignableFrom(o)),
+          "Not a single unique method: %s", group);
+      String methodName = group.stream().findAny().get().name();
+
+      ImmutableList<Class<?>> customOverrides = findCustomOverrides(root, methodName);
+
+      for (EmulatedMethod methodDefinition : group) {
+        Class<?> owner = methodDefinition.owner();
+        ClassVisitor dispatchHelper = dispatchHelpers.computeIfAbsent(owner, clazz -> {
+          String className = clazz.getName().replace('.', '/') + "$$Dispatch";
+          ClassVisitor result = store.add(className);
+          result.visit(
+              Opcodes.V1_7,
+              // Must be public so dispatch methods can be called from anywhere
+              Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC,
+              className,
+              /*signature=*/ null,
+              "java/lang/Object",
+              EMPTY_LIST);
+          return result;
+        });
+
+        // Types to check for before calling methodDefinition's companion, sub- before super-types
+        ImmutableList<Class<?>> typechecks =
+            concat(group.stream().map(EmulatedMethod::owner), customOverrides.stream())
+                .filter(o -> o != owner && owner.isAssignableFrom(o))
+                .distinct() // should already be but just in case
+                .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+                .collect(ImmutableList.toImmutableList());
+        makeDispatchHelperMethod(dispatchHelper, methodDefinition, typechecks);
+      }
+    }
+  }
+
+  private ImmutableList<Class<?>> findCustomOverrides(Class<?> root, String methodName) {
+    ImmutableList.Builder<Class<?>> customOverrides = ImmutableList.builder();
+    for (ImmutableMap.Entry<String, String> move : memberMoves.entrySet()) {
+      // move.getKey is a string <owner>#<name> which we validated in the constructor.
+      // We need to take the string apart here to compare owner and name separately.
+      if (!methodName.equals(move.getKey().substring(move.getKey().indexOf('#') + 1))) {
+        continue;
+      }
+      Class<?> target =
+          loadFromInternal(
+              rewriter.getPrefix() + move.getKey().substring(0, move.getKey().indexOf('#')));
+      if (!root.isAssignableFrom(target)) {
+        continue;
+      }
+      checkState(!target.isInterface(), "can't move emulated interface method: %s", move);
+      customOverrides.add(target);
+    }
+    return customOverrides.build();
+  }
+
+  private void makeDispatchHelperMethod(
+      ClassVisitor helper, EmulatedMethod method, ImmutableList<Class<?>> typechecks) {
+    checkArgument(method.owner().isInterface());
+    String owner = method.owner().getName().replace('.', '/');
+    Type methodType = Type.getMethodType(method.descriptor());
+    String companionDesc =
+        InterfaceDesugaring.companionDefaultMethodDescriptor(owner, method.descriptor());
+    MethodVisitor dispatchMethod =
+        helper.visitMethod(
+            method.access() | Opcodes.ACC_STATIC,
+            method.name(),
+            companionDesc,
+            /*signature=*/ null,  // signature is invalid due to extra "receiver" argument
+            method.exceptions().toArray(EMPTY_LIST));
+
+
+    dispatchMethod.visitCode();
+    {
+      // See if the receiver might come with its own implementation of the method, and call it.
+      // We do this by testing for the interface type created by EmulatedInterfaceRewriter
+      Label fallthrough = new Label();
+      String emulationInterface = renameCoreLibrary(owner);
+      dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0);  // load "receiver"
+      dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface);
+      dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough);
+      dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0);  // load "receiver"
+      dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface);
+
+      visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+      dispatchMethod.visitMethodInsn(
+          Opcodes.INVOKEINTERFACE,
+          emulationInterface,
+          method.name(),
+          method.descriptor(),
+          /*itf=*/ true);
+      dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+      dispatchMethod.visitLabel(fallthrough);
+      // Trivial frame for the branch target: same empty stack as before
+      dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME);
+    }
+
+    // Next, check for subtypes with specialized implementations and call them
+    for (Class<?> tested : typechecks) {
+      Label fallthrough = new Label();
+      String testedName = tested.getName().replace('.', '/');
+      // In case of a class this must be a member move; for interfaces use the companion.
+      String target =
+          tested.isInterface()
+              ? InterfaceDesugaring.getCompanionClassName(testedName)
+              : checkNotNull(memberMoves.get(rewriter.unprefix(testedName) + '#' + method.name()));
+      dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0);  // load "receiver"
+      dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, testedName);
+      dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough);
+      dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0);  // load "receiver"
+      dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, testedName);  // make verifier happy
+
+      visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+      dispatchMethod.visitMethodInsn(
+          Opcodes.INVOKESTATIC,
+          target,
+          method.name(),
+          InterfaceDesugaring.companionDefaultMethodDescriptor(testedName, method.descriptor()),
+          /*itf=*/ false);
+      dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+      dispatchMethod.visitLabel(fallthrough);
+      // Trivial frame for the branch target: same empty stack as before
+      dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME);
+    }
+
+    // Call static type's default implementation in companion class
+    dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0);  // load "receiver"
+    visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+    dispatchMethod.visitMethodInsn(
+        Opcodes.INVOKESTATIC,
+        InterfaceDesugaring.getCompanionClassName(owner),
+        method.name(),
+        companionDesc,
+        /*itf=*/ false);
+    dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+    dispatchMethod.visitMaxs(0, 0);
+    dispatchMethod.visitEnd();
+  }
+
+  private boolean isExcluded(Method method) {
+    String unprefixedOwner =
+        rewriter.unprefix(method.getDeclaringClass().getName().replace('.', '/'));
+    return excludeFromEmulation.contains(unprefixedOwner + "#" + method.getName());
+  }
+
+  private Class<?> loadFromInternal(String internalName) {
+    try {
+      return targetLoader.loadClass(internalName.replace('/', '.'));
+    } catch (ClassNotFoundException e) {
+      throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e);
+    }
+  }
+
+  private static Method findInterfaceMethod(Class<?> clazz, String name, String desc) {
+    return collectImplementedInterfaces(clazz, new LinkedHashSet<>())
+        .stream()
+        // search more subtypes before supertypes
+        .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+        .map(itf -> findMethod(itf, name, desc))
+        .filter(Objects::nonNull)
+        .findFirst()
+        .orElse((Method) null);
+  }
+
+  private static Method findMethod(Class<?> clazz, String name, String desc) {
+    for (Method m : clazz.getMethods()) {
+      if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) {
+        return m;
+      }
+    }
+    return null;
+  }
+
+  private static Set<Class<?>> collectImplementedInterfaces(Class<?> clazz, Set<Class<?>> dest) {
+    if (clazz.isInterface()) {
+      if (!dest.add(clazz)) {
+        return dest;
+      }
+    } else if (clazz.getSuperclass() != null) {
+      collectImplementedInterfaces(clazz.getSuperclass(), dest);
+    }
+
+    for (Class<?> itf : clazz.getInterfaces()) {
+      collectImplementedInterfaces(itf, dest);
+    }
+    return dest;
+  }
+
+  /**
+   * Emits instructions to load a method's parameters as arguments of a method call assumed to have
+   * compatible descriptor, starting at the given local variable slot.
+   */
+  private static void visitLoadArgs(MethodVisitor dispatchMethod, Type neededType, int slot) {
+    for (Type arg : neededType.getArgumentTypes()) {
+      dispatchMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+      slot += arg.getSize();
+    }
+  }
+
+  /** Checks whether the given class is (likely) generated by desugar itself. */
+  private static boolean looksGenerated(String owner) {
+    return owner.contains("$$Lambda$") || owner.endsWith("$$CC") || owner.endsWith("$$Dispatch");
+  }
+
+  @AutoValue
+  @Immutable
+  abstract static class EmulatedMethod {
+    public static EmulatedMethod create(
+        int access, Class<?> owner, String name, String desc, @Nullable String[] exceptions) {
+      return new AutoValue_CoreLibrarySupport_EmulatedMethod(access, owner, name, desc,
+          exceptions != null ? ImmutableList.copyOf(exceptions) : ImmutableList.of());
+    }
+
+    abstract int access();
+    abstract Class<?> owner();
+    abstract String name();
+    abstract String descriptor();
+    abstract ImmutableList<String> exceptions();
+  }
+}
diff --git a/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java
new file mode 100644
index 0000000..5f1dc2e
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java
@@ -0,0 +1,45 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
+
+/**
+ * A visitor that renames packages so configured using {@link CoreLibrarySupport}..
+ */
+class CorePackageRenamer extends ClassRemapper {
+
+  public CorePackageRenamer(ClassVisitor cv, CoreLibrarySupport support) {
+    super(cv, new CorePackageRemapper(support));
+  }
+
+  private static final class CorePackageRemapper extends Remapper {
+    private final CoreLibrarySupport support;
+
+    private CorePackageRemapper(CoreLibrarySupport support) {
+      this.support = support;
+    }
+
+    public boolean isRenamed(String owner) {
+      return support.isRenamedCoreLibrary(owner);
+    }
+
+    @Override
+    public String map(String typeName) {
+      return isRenamed(typeName) ? support.renameCoreLibrary(typeName) : typeName;
+    }
+  }
+}
diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
index 1aaf0b6..1de48bf 100644
--- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
@@ -18,11 +18,15 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
 import java.util.TreeSet;
+import javax.annotation.Nullable;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.MethodVisitor;
@@ -44,6 +48,7 @@
   private final ClassReaderFactory bootclasspath;
   private final ClassLoader targetLoader;
   private final DependencyCollector depsCollector;
+  @Nullable private final CoreLibrarySupport coreLibrarySupport;
   private final HashSet<String> instanceMethods = new HashSet<>();
 
   private boolean isInterface;
@@ -57,10 +62,12 @@
       ClassVisitor dest,
       ClassReaderFactory classpath,
       DependencyCollector depsCollector,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassReaderFactory bootclasspath,
       ClassLoader targetLoader) {
     super(Opcodes.ASM6, dest);
     this.classpath = classpath;
+    this.coreLibrarySupport = coreLibrarySupport;
     this.bootclasspath = bootclasspath;
     this.targetLoader = targetLoader;
     this.depsCollector = depsCollector;
@@ -88,7 +95,9 @@
 
   @Override
   public void visitEnd() {
-    if (!isInterface && defaultMethodsDefined(directInterfaces)) {
+    if (!isInterface
+        && (mayNeedInterfaceStubsForEmulatedSuperclass()
+            || defaultMethodsDefined(directInterfaces))) {
       // Inherited methods take precedence over default methods, so visit all superclasses and
       // figure out what methods they declare before stubbing in any missing default methods.
       recordInheritedMethods();
@@ -190,8 +199,14 @@
     return super.visitMethod(access, name, desc, signature, exceptions);
   }
 
+  private boolean mayNeedInterfaceStubsForEmulatedSuperclass() {
+    return coreLibrarySupport != null
+        && !coreLibrarySupport.isEmulatedCoreClassOrInterface(internalName)
+        && coreLibrarySupport.isEmulatedCoreClassOrInterface(superName);
+  }
+
   private void stubMissingDefaultAndBridgeMethods() {
-    TreeSet<Class<?>> allInterfaces = new TreeSet<>(InterfaceComparator.INSTANCE);
+    TreeSet<Class<?>> allInterfaces = new TreeSet<>(SubtypeComparator.INSTANCE);
     for (String direct : directInterfaces) {
       // Loading ensures all transitively implemented interfaces can be loaded, which is necessary
       // to produce correct default method stubs in all cases.  We could do without classloading but
@@ -203,19 +218,55 @@
     }
 
     Class<?> superclass = loadFromInternal(superName);
+    boolean mayNeedStubsForSuperclass = mayNeedInterfaceStubsForEmulatedSuperclass();
+    if (mayNeedStubsForSuperclass) {
+      // Collect interfaces inherited from emulated superclasses as well, to handle things like
+      // extending AbstractList without explicitly implementing List.
+      for (Class<?> clazz = superclass; clazz != null; clazz = clazz.getSuperclass()) {
+        for (Class<?> itf : superclass.getInterfaces()) {
+          collectInterfaces(itf, allInterfaces);
+        }
+      }
+    }
     for (Class<?> interfaceToVisit : allInterfaces) {
       // if J extends I, J is allowed to redefine I's default methods.  The comparator we used
       // above makes sure we visit J before I in that case so we can use J's definition.
-      if (superclass != null && interfaceToVisit.isAssignableFrom(superclass)) {
-        // superclass already implements this interface, so we must skip it.  The superclass will
-        // be similarly rewritten or comes from the bootclasspath; either way we don't need to and
-        // shouldn't stub default methods for this interface.
+      if (!mayNeedStubsForSuperclass && interfaceToVisit.isAssignableFrom(superclass)) {
+        // superclass is also rewritten and already implements this interface, so we _must_ skip it.
         continue;
       }
-      stubMissingDefaultAndBridgeMethods(interfaceToVisit.getName().replace('.', '/'));
+      stubMissingDefaultAndBridgeMethods(
+          interfaceToVisit.getName().replace('.', '/'), mayNeedStubsForSuperclass);
     }
   }
 
+  private void stubMissingDefaultAndBridgeMethods(
+      String implemented, boolean mayNeedStubsForSuperclass) {
+    ClassReader bytecode;
+    boolean isBootclasspath;
+    if (bootclasspath.isKnown(implemented)) {
+      if (coreLibrarySupport != null
+          && (coreLibrarySupport.isRenamedCoreLibrary(implemented)
+              || coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented))) {
+        bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented);
+        isBootclasspath = true;
+      } else {
+        // Default methods from interfaces on the bootclasspath that we're not renaming or emulating
+        // are assumed available at runtime, so just ignore them.
+        return;
+      }
+    } else {
+      bytecode =
+          checkNotNull(
+              classpath.readIfKnown(implemented),
+              "Couldn't find interface %s implemented by %s", implemented, internalName);
+      isBootclasspath = false;
+    }
+    bytecode.accept(
+        new DefaultMethodStubber(isBootclasspath, mayNeedStubsForSuperclass),
+        ClassReader.SKIP_DEBUG);
+  }
+
   private Class<?> loadFromInternal(String internalName) {
     try {
       return targetLoader.loadClass(internalName.replace('/', '.'));
@@ -236,7 +287,8 @@
   }
 
   private void recordInheritedMethods() {
-    InstanceMethodRecorder recorder = new InstanceMethodRecorder();
+    InstanceMethodRecorder recorder =
+        new InstanceMethodRecorder(mayNeedInterfaceStubsForEmulatedSuperclass());
     String internalName = superName;
     while (internalName != null) {
       ClassReader bytecode = bootclasspath.readIfKnown(internalName);
@@ -313,25 +365,37 @@
    */
   private boolean defaultMethodsDefined(ImmutableList<String> interfaces) {
     for (String implemented : interfaces) {
+      ClassReader bytecode;
       if (bootclasspath.isKnown(implemented)) {
-        continue;
-      }
-      ClassReader bytecode = classpath.readIfKnown(implemented);
-      if (bytecode == null) {
-        // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing
-        // dependency so we can check it later.  If we don't check then we may get runtime failures
-        // or wrong behavior from default methods that should've been stubbed in.
-        // TODO(kmb): Print a warning so people can start fixing their deps?
-        depsCollector.missingImplementedInterface(internalName, implemented);
-      } else {
-        // Class in classpath and bootclasspath is a bad idea but in any event, assume the
-        // bootclasspath will take precedence like in a classloader.
-        // We can skip code attributes as we just need to find default methods to stub.
-        DefaultMethodFinder finder = new DefaultMethodFinder();
-        bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
-        if (finder.foundDefaultMethods()) {
-          return true;
+        if (coreLibrarySupport != null
+            && coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented)) {
+          return true; // need to stub in emulated interface methods such as Collection.stream()
+        } else if (coreLibrarySupport != null
+            && coreLibrarySupport.isRenamedCoreLibrary(implemented)) {
+          // Check default methods of renamed interfaces
+          bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented);
+        } else {
+          continue;
         }
+      } else {
+        bytecode = classpath.readIfKnown(implemented);
+        if (bytecode == null) {
+          // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing
+          // dependency so we can check it later.  If we don't check then we may get runtime
+          // failures or wrong behavior from default methods that should've been stubbed in.
+          // TODO(kmb): Print a warning so people can start fixing their deps?
+          depsCollector.missingImplementedInterface(internalName, implemented);
+          continue;
+        }
+      }
+
+      // Class in classpath and bootclasspath is a bad idea but in any event, assume the
+      // bootclasspath will take precedence like in a classloader.
+      // We can skip code attributes as we just need to find default methods to stub.
+      DefaultMethodFinder finder = new DefaultMethodFinder();
+      bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
+      if (finder.foundDefaultMethods()) {
+        return true;
       }
     }
     return false;
@@ -365,30 +429,22 @@
         && !instanceMethods.contains(name + ":" + desc);
   }
 
-  private void stubMissingDefaultAndBridgeMethods(String implemented) {
-    if (bootclasspath.isKnown(implemented)) {
-      // Default methods on the bootclasspath will be available at runtime, so just ignore them.
-      return;
-    }
-    ClassReader bytecode =
-        checkNotNull(
-            classpath.readIfKnown(implemented),
-            "Couldn't find interface %s implemented by %s",
-            implemented,
-            internalName);
-    bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_DEBUG);
-  }
-
   /**
    * Visitor for interfaces that produces delegates in the class visited by the outer {@link
    * DefaultMethodClassFixer} for every default method encountered.
    */
   private class DefaultMethodStubber extends ClassVisitor {
 
+    private final boolean isBootclasspathInterface;
+    private final boolean mayNeedStubsForSuperclass;
+
     private String stubbedInterfaceName;
 
-    public DefaultMethodStubber() {
+    public DefaultMethodStubber(
+        boolean isBootclasspathInterface, boolean mayNeedStubsForSuperclass) {
       super(Opcodes.ASM6);
+      this.isBootclasspathInterface = isBootclasspathInterface;
+      this.mayNeedStubsForSuperclass = mayNeedStubsForSuperclass;
     }
 
     @Override
@@ -413,8 +469,11 @@
         // definitions conflict, but see stubMissingDefaultMethods() for how we deal with default
         // methods redefined in interfaces extending another.
         recordIfInstanceMethod(access, name, desc);
-        depsCollector.assumeCompanionClass(
-            internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName));
+        if (!isBootclasspathInterface) {
+          // Don't record these dependencies, as we can't check them
+          depsCollector.assumeCompanionClass(
+              internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName));
+        }
 
         // Add this method to the class we're desugaring and stub in a body to call the default
         // implementation in the interface's companion class. ijar omits these methods when setting
@@ -424,6 +483,21 @@
         MethodVisitor stubMethod =
             DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
 
+        String receiverName = stubbedInterfaceName;
+        String owner = InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName);
+        if (mayNeedStubsForSuperclass) {
+          // Reflect what CoreLibraryInvocationRewriter would do if it encountered a super-call to a
+          // moved implementation of an emulated method.  Equivalent to emitting the invokespecial
+          // super call here and relying on CoreLibraryInvocationRewriter for the rest
+          Class<?> emulatedImplementation =
+              coreLibrarySupport.getCoreInterfaceRewritingTarget(
+                  Opcodes.INVOKESPECIAL, superName, name, desc, /*itf=*/ false);
+          if (emulatedImplementation != null && !emulatedImplementation.isInterface()) {
+            receiverName = emulatedImplementation.getName().replace('.', '/');
+            owner = checkNotNull(coreLibrarySupport.getMoveTarget(receiverName, name));
+          }
+        }
+
         int slot = 0;
         stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
         Type neededType = Type.getMethodType(desc);
@@ -433,30 +507,92 @@
         }
         stubMethod.visitMethodInsn(
             Opcodes.INVOKESTATIC,
-            InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName),
+            owner,
             name,
-            InterfaceDesugaring.companionDefaultMethodDescriptor(stubbedInterfaceName, desc),
-            /*itf*/ false);
+            InterfaceDesugaring.companionDefaultMethodDescriptor(receiverName, desc),
+            /*itf=*/ false);
         stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
 
         stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
         stubMethod.visitEnd();
-        return null;
+        return null; // don't visit the visited interface's default method
       } else if (shouldStubAsBridgeDefaultMethod(access, name, desc)) {
         recordIfInstanceMethod(access, name, desc);
-        // For bridges we just copy their bodies instead of going through the companion class.
-        // Meanwhile, we also need to desugar the copied method bodies, so that any calls to
-        // interface methods are correctly handled.
-        return new InterfaceDesugaring.InterfaceInvocationRewriter(
-            DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions),
-            stubbedInterfaceName,
-            bootclasspath,
-            depsCollector,
-            internalName);
+        MethodVisitor stubMethod =
+            DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
+        // If we're visiting a bootclasspath interface then we most likely don't have the code.
+        // That means we can't just copy the method bodies as we're trying to do below.
+        if (isBootclasspathInterface) {
+          // Synthesize a "bridge" method that calls the true implementation
+          Method bridged = findBridgedMethod(name, desc);
+          checkState(bridged != null,
+              "TODO: Can't stub core interface bridge method %s.%s %s in %s",
+              stubbedInterfaceName, name, desc, internalName);
+
+          int slot = 0;
+          stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
+          Type neededType = Type.getType(bridged);
+          for (Type arg : neededType.getArgumentTypes()) {
+            // TODO(b/73586397): insert downcasts if necessary
+            stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+            slot += arg.getSize();
+          }
+          // Just call the bridged method directly on the visited class using invokevirtual
+          stubMethod.visitMethodInsn(
+              Opcodes.INVOKEVIRTUAL,
+              internalName,
+              name,
+              neededType.getDescriptor(),
+              /*itf=*/ false);
+          stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+          stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
+          stubMethod.visitEnd();
+          return null;  // don't visit the visited interface's bridge method
+        } else {
+          // For bridges we just copy their bodies instead of going through the companion class.
+          // Meanwhile, we also need to desugar the copied method bodies, so that any calls to
+          // interface methods are correctly handled.
+          return new InterfaceDesugaring.InterfaceInvocationRewriter(
+              stubMethod,
+              stubbedInterfaceName,
+              bootclasspath,
+              targetLoader,
+              depsCollector,
+              internalName);
+        }
       } else {
-        return null; // we don't care about the actual code in these methods
+        return null; // not a default or bridge method or the class already defines this method.
       }
     }
+
+    /**
+     * Returns a non-bridge interface method with given name that a method with the given descriptor
+     * can bridge to, if any such method can be found.
+     */
+    @Nullable
+    private Method findBridgedMethod(String name, String desc) {
+      Type[] paramTypes = Type.getArgumentTypes(desc);
+      Class<?> itf = loadFromInternal(stubbedInterfaceName);
+      checkArgument(itf.isInterface(), "Should be an interface: %s", stubbedInterfaceName);
+      Method result = null;
+      for (Method m : itf.getDeclaredMethods()) {
+        if (m.isBridge()) {
+          continue;
+        }
+        if (!m.getName().equals(name)) {
+          continue;
+        }
+        // For now, only support specialized return types (which don't require casts)
+        // TODO(b/73586397): Make this work for other kinds of bridges in core library interfaces
+        if (Arrays.equals(paramTypes, Type.getArgumentTypes(m))) {
+          checkState(result == null,
+              "Found multiple bridge target %s and %s for descriptor %s", result, m, desc);
+          return result = m;
+        }
+      }
+      return result;
+    }
   }
 
   /**
@@ -510,8 +646,13 @@
 
   private class InstanceMethodRecorder extends ClassVisitor {
 
-    public InstanceMethodRecorder() {
+    private final boolean ignoreEmulatedMethods;
+
+    private String className;
+
+    public InstanceMethodRecorder(boolean ignoreEmulatedMethods) {
       super(Opcodes.ASM6);
+      this.ignoreEmulatedMethods = ignoreEmulatedMethods;
     }
 
     @Override
@@ -523,12 +664,23 @@
         String superName,
         String[] interfaces) {
       checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE));
+      className = name;  // updated every time we start visiting another superclass
       super.visit(version, access, name, signature, superName, interfaces);
     }
 
     @Override
     public MethodVisitor visitMethod(
         int access, String name, String desc, String signature, String[] exceptions) {
+      if (ignoreEmulatedMethods
+          && BitFlags.noneSet(access, Opcodes.ACC_STATIC) // short-circuit
+          && coreLibrarySupport.getCoreInterfaceRewritingTarget(
+                  Opcodes.INVOKEVIRTUAL, className, name, desc, /*itf=*/ false)
+              != null) {
+        // *don't* record emulated core library method implementations in immediate subclasses of
+        // emulated core library clasess so that they can be stubbed (since the inherited
+        // implementation may be missing at runtime).
+        return null;
+      }
       recordIfInstanceMethod(access, name, desc);
       return null;
     }
@@ -594,17 +746,17 @@
     }
   }
 
-  /** Comparator for interfaces that compares by whether interfaces extend one another. */
-  enum InterfaceComparator implements Comparator<Class<?>> {
+  /** Comparator for classes and interfaces that compares by whether subtyping relationship. */
+  enum  SubtypeComparator implements Comparator<Class<?>> {
+    /** Orders subtypes before supertypes and breaks ties lexicographically. */
     INSTANCE;
 
     @Override
     public int compare(Class<?> o1, Class<?> o2) {
-      checkArgument(o1.isInterface());
-      checkArgument(o2.isInterface());
       if (o1 == o2) {
         return 0;
       }
+      // order subtypes before supertypes
       if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2
         return 1; // we want o1 to come after o2
       }
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 5d3df4a..c176f9c 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.devtools.build.android.desugar.LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
@@ -28,17 +27,24 @@
 import com.google.common.io.Closer;
 import com.google.devtools.build.android.Converters.ExistingPathConverter;
 import com.google.devtools.build.android.Converters.PathConverter;
-import com.google.devtools.build.android.desugar.CoreLibraryRewriter.UnprefixingClassWriter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter.UnprefixingClassWriter;
+import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
+import com.google.devtools.build.android.desugar.io.OutputFileProvider;
+import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionDocumentationCategory;
 import com.google.devtools.common.options.OptionEffectTag;
-import com.google.devtools.common.options.Options;
 import com.google.devtools.common.options.OptionsBase;
-import com.google.errorprone.annotations.MustBeClosed;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
 import java.io.IOError;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Field;
+import java.nio.file.FileSystems;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -195,6 +201,15 @@
     public boolean tolerateMissingDependencies;
 
     @Option(
+      name = "desugar_supported_core_libs",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Enable core library desugaring, which requires configuration with related flags."
+    )
+    public boolean desugarCoreLibs;
+
+    @Option(
       name = "desugar_interface_method_bodies_if_needed",
       defaultValue = "true",
       category = "misc",
@@ -248,6 +263,51 @@
     )
     public boolean coreLibrary;
 
+    /** Type prefixes that we'll move to a custom package. */
+    @Option(
+      name = "rewrite_core_library_prefix",
+      defaultValue = "", // ignored
+      allowMultiple = true,
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Assume the given java.* prefixes are desugared."
+    )
+    public List<String> rewriteCoreLibraryPrefixes;
+
+    /** Interfaces whose default and static interface methods we'll emulate. */
+    @Option(
+      name = "emulate_core_library_interface",
+      defaultValue = "", // ignored
+      allowMultiple = true,
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Assume the given java.* interfaces are emulated."
+    )
+    public List<String> emulateCoreLibraryInterfaces;
+
+    /** Members that we will retarget to the given new owner. */
+    @Option(
+      name = "retarget_core_library_member",
+      defaultValue = "", // ignored
+      allowMultiple = true,
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Method invocations to retarget, given as \"class/Name#member->new/class/Name\".  "
+          + "The new owner is blindly assumed to exist."
+    )
+    public List<String> retargetCoreLibraryMembers;
+
+    /** Members not to rewrite. */
+    @Option(
+      name = "dont_rewrite_core_library_invocation",
+      defaultValue = "", // ignored
+      allowMultiple = true,
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Method invocations not to rewrite, given as \"class/Name#method\"."
+    )
+    public List<String> dontTouchCoreLibraryMembers;
+
     /** Set to work around b/62623509 with JaCoCo versions prior to 0.7.9. */
     // TODO(kmb): Remove when Android Studio doesn't need it anymore (see b/37116789)
     @Option(
@@ -265,7 +325,7 @@
   private final DesugarOptions options;
   private final CoreLibraryRewriter rewriter;
   private final LambdaClassMaker lambdas;
-  private final GeneratedClassStore store;
+  private final GeneratedClassStore store = new GeneratedClassStore();
   private final Set<String> visitedExceptionTypes = new HashSet<>();
   /** The counter to record the times of try-with-resources desugaring is invoked. */
   private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger();
@@ -282,7 +342,6 @@
     this.options = options;
     this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : "");
     this.lambdas = new LambdaClassMaker(dumpDirectory);
-    this.store = new GeneratedClassStore();
     this.outputJava7 = options.minSdkVersion < 24;
     this.allowDefaultMethods =
         options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24;
@@ -332,8 +391,8 @@
         Files.isDirectory(inputPath) || !Files.isDirectory(outputPath),
         "Input jar file requires an output jar file");
 
-    try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath);
-        InputFileProvider inputFiles = toInputFileProvider(inputPath)) {
+    try (OutputFileProvider outputFileProvider = OutputFileProvider.create(outputPath);
+        InputFileProvider inputFiles = InputFileProvider.open(inputPath)) {
       DependencyCollector depsCollector = createDepsCollector();
       IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles));
       // Prepend classpath with input file itself so LambdaDesugaring can load classes with
@@ -358,6 +417,17 @@
 
       ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
       ClassVsInterface interfaceCache = new ClassVsInterface(classpathReader);
+      CoreLibrarySupport coreLibrarySupport =
+          options.desugarCoreLibs
+              ? new CoreLibrarySupport(
+                  rewriter,
+                  loader,
+                  options.rewriteCoreLibraryPrefixes,
+                  options.emulateCoreLibraryInterfaces,
+                  options.retargetCoreLibraryMembers,
+                  options.dontTouchCoreLibraryMembers)
+              : null;
+
       desugarClassesInInput(
           inputFiles,
           outputFileProvider,
@@ -365,6 +435,7 @@
           classpathReader,
           depsCollector,
           bootclasspathReader,
+          coreLibrarySupport,
           interfaceCache,
           interfaceLambdaMethodCollector);
 
@@ -374,11 +445,14 @@
           classpathReader,
           depsCollector,
           bootclasspathReader,
+          coreLibrarySupport,
           interfaceCache,
           interfaceLambdaMethodCollector.build(),
           bridgeMethodReader);
 
-      desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader);
+      desugarAndWriteGeneratedClasses(
+          outputFileProvider, loader, bootclasspathReader, coreLibrarySupport);
+
       copyThrowableExtensionClass(outputFileProvider);
 
       byte[] depsInfo = depsCollector.toByteArray();
@@ -394,7 +468,7 @@
   }
 
   /**
-   * Returns a dependency collector for use with a single input Jar.  If
+   * Returns a dependency collector for use with a single input Jar. If
    * {@link DesugarOptions#emitDependencyMetadata} is set, this method instantiates the collector
    * reflectively to allow compiling and using the desugar tool without this mechanism.
    */
@@ -445,19 +519,20 @@
       @Nullable ClassReaderFactory classpathReader,
       DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassVsInterface interfaceCache,
       ImmutableSet.Builder<String> interfaceLambdaMethodCollector)
       throws IOException {
-    for (String filename : inputFiles) {
-      if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(filename)) {
+    for (String inputFilename : inputFiles) {
+      if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(inputFilename)) {
         // TODO(kmb): rule out that this happens or merge input file with what's in depsCollector
         continue;  // skip as we're writing a new file like this at the end or don't want it
       }
-      try (InputStream content = inputFiles.getInputStream(filename)) {
+      try (InputStream content = inputFiles.getInputStream(inputFilename)) {
         // We can write classes uncompressed since they need to be converted to .dex format
         // for Android anyways. Resources are written as they were in the input jar to avoid
         // any danger of accidentally uncompressed resources ending up in an .apk.
-        if (filename.endsWith(".class")) {
+        if (inputFilename.endsWith(".class")) {
           ClassReader reader = rewriter.reader(content);
           UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
           ClassVisitor visitor =
@@ -466,19 +541,24 @@
                   classpathReader,
                   depsCollector,
                   bootclasspathReader,
+                  coreLibrarySupport,
                   interfaceCache,
                   interfaceLambdaMethodCollector,
                   writer,
                   reader);
           if (writer == visitor) {
             // Just copy the input if there are no rewritings
-            outputFileProvider.write(filename, reader.b);
+            outputFileProvider.write(inputFilename, reader.b);
           } else {
             reader.accept(visitor, 0);
+            String filename = writer.getClassName() + ".class";
+            checkState(
+                (options.coreLibrary && coreLibrarySupport != null)
+                    || filename.equals(inputFilename));
             outputFileProvider.write(filename, writer.toByteArray());
           }
         } else {
-          outputFileProvider.copyFrom(filename, inputFiles);
+          outputFileProvider.copyFrom(inputFilename, inputFiles);
         }
       }
     }
@@ -494,6 +574,7 @@
       @Nullable ClassReaderFactory classpathReader,
       DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassVsInterface interfaceCache,
       ImmutableSet<String> interfaceLambdaMethods,
       @Nullable ClassReaderFactory bridgeMethodReader)
@@ -525,6 +606,7 @@
                 classpathReader,
                 depsCollector,
                 bootclasspathReader,
+                coreLibrarySupport,
                 interfaceCache,
                 interfaceLambdaMethods,
                 bridgeMethodReader,
@@ -532,17 +614,26 @@
                 writer,
                 reader);
         reader.accept(visitor, 0);
-        String filename =
-            rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class";
-        outputFileProvider.write(filename, writer.toByteArray());
+        checkState(
+            (options.coreLibrary && coreLibrarySupport != null)
+                || rewriter
+                    .unprefix(lambdaClass.getValue().desiredInternalName())
+                    .equals(writer.getClassName()));
+        outputFileProvider.write(writer.getClassName() + ".class", writer.toByteArray());
       }
     }
   }
 
   private void desugarAndWriteGeneratedClasses(
-      OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader)
+      OutputFileProvider outputFileProvider,
+      ClassLoader loader,
+      ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport)
       throws IOException {
     // Write out any classes we generated along the way
+    if (coreLibrarySupport != null) {
+      coreLibrarySupport.makeDispatchHelpers(store);
+    }
     ImmutableMap<String, ClassNode> generatedClasses = store.drain();
     checkState(
         generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7),
@@ -552,11 +643,39 @@
       UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
       // checkState above implies that we want Java 7 .class files, so send through that visitor.
       // Don't need a ClassReaderFactory b/c static interface methods should've been moved.
-      ClassVisitor visitor =
-          new Java7Compatibility(writer, (ClassReaderFactory) null, bootclasspathReader);
+      ClassVisitor visitor = writer;
+      if (coreLibrarySupport != null) {
+        visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+        visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+        visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+      }
+
+      if (!allowTryWithResources) {
+        CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
+        generated.getValue().accept(closeResourceMethodScanner);
+        visitor =
+            new TryWithResourcesRewriter(
+                visitor,
+                loader,
+                visitedExceptionTypes,
+                numOfTryWithResourcesInvoked,
+                closeResourceMethodScanner.hasCloseResourceMethod());
+      }
+      if (!allowCallsToObjectsNonNull) {
+        // Not sure whether there will be implicit null check emitted by javac, so we rerun
+        // the inliner again
+        visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
+      }
+      if (!allowCallsToLongCompare) {
+        visitor = new LongCompareMethodRewriter(visitor, rewriter);
+      }
+
+      visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader);
       generated.getValue().accept(visitor);
-      String filename = rewriter.unprefix(generated.getKey()) + ".class";
-      outputFileProvider.write(filename, writer.toByteArray());
+      checkState(
+          (options.coreLibrary && coreLibrarySupport != null)
+              || rewriter.unprefix(generated.getKey()).equals(writer.getClassName()));
+      outputFileProvider.write(writer.getClassName() + ".class", writer.toByteArray());
     }
   }
 
@@ -569,6 +688,7 @@
       @Nullable ClassReaderFactory classpathReader,
       DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassVsInterface interfaceCache,
       ImmutableSet<String> interfaceLambdaMethods,
       @Nullable ClassReaderFactory bridgeMethodReader,
@@ -576,6 +696,13 @@
       UnprefixingClassWriter writer,
       ClassReader input) {
     ClassVisitor visitor = checkNotNull(writer);
+
+    if (coreLibrarySupport != null) {
+      visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+      visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+      visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+    }
+
     if (!allowTryWithResources) {
       CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
       input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG);
@@ -590,10 +717,10 @@
     if (!allowCallsToObjectsNonNull) {
       // Not sure whether there will be implicit null check emitted by javac, so we rerun
       // the inliner again
-      visitor = new ObjectsRequireNonNullMethodRewriter(visitor);
+      visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
     }
     if (!allowCallsToLongCompare) {
-      visitor = new LongCompareMethodRewriter(visitor);
+      visitor = new LongCompareMethodRewriter(visitor, rewriter);
     }
     if (outputJava7) {
       // null ClassReaderFactory b/c we don't expect to need it for lambda classes
@@ -601,17 +728,25 @@
       if (options.desugarInterfaceMethodBodiesIfNeeded) {
         visitor =
             new DefaultMethodClassFixer(
-                visitor, classpathReader, depsCollector, bootclasspathReader, loader);
+                visitor,
+                classpathReader,
+                depsCollector,
+                coreLibrarySupport,
+                bootclasspathReader,
+                loader);
         visitor =
             new InterfaceDesugaring(
                 visitor,
                 interfaceCache,
                 depsCollector,
+                coreLibrarySupport,
                 bootclasspathReader,
+                loader,
                 store,
                 options.legacyJacocoFix);
       }
     }
+
     visitor =
         new LambdaClassFixer(
             visitor,
@@ -639,11 +774,19 @@
       @Nullable ClassReaderFactory classpathReader,
       DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassVsInterface interfaceCache,
       ImmutableSet.Builder<String> interfaceLambdaMethodCollector,
       UnprefixingClassWriter writer,
       ClassReader input) {
     ClassVisitor visitor = checkNotNull(writer);
+
+    if (coreLibrarySupport != null) {
+      visitor = new EmulatedInterfaceRewriter(visitor, coreLibrarySupport);
+      visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+      visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+    }
+
     if (!allowTryWithResources) {
       CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
       input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG);
@@ -656,10 +799,10 @@
               closeResourceMethodScanner.hasCloseResourceMethod());
     }
     if (!allowCallsToObjectsNonNull) {
-      visitor = new ObjectsRequireNonNullMethodRewriter(visitor);
+      visitor = new ObjectsRequireNonNullMethodRewriter(visitor, rewriter);
     }
     if (!allowCallsToLongCompare) {
-      visitor = new LongCompareMethodRewriter(visitor);
+      visitor = new LongCompareMethodRewriter(visitor, rewriter);
     }
     if (!options.onlyDesugarJavac9ForLint) {
       if (outputJava7) {
@@ -667,17 +810,25 @@
         if (options.desugarInterfaceMethodBodiesIfNeeded) {
           visitor =
               new DefaultMethodClassFixer(
-                  visitor, classpathReader, depsCollector, bootclasspathReader, loader);
+                  visitor,
+                  classpathReader,
+                  depsCollector,
+                  coreLibrarySupport,
+                  bootclasspathReader,
+                  loader);
           visitor =
               new InterfaceDesugaring(
                   visitor,
                   interfaceCache,
                   depsCollector,
+                  coreLibrarySupport,
                   bootclasspathReader,
+                  loader,
                   store,
                   options.legacyJacocoFix);
         }
       }
+
       // LambdaDesugaring is relatively expensive, so check first whether we need it.  Additionally,
       // we need to collect lambda methods referenced by invokedynamic instructions up-front anyway.
       // TODO(kmb): Scan constant pool instead of visiting the class to find bootstrap methods etc.
@@ -730,7 +881,7 @@
     } catch (ReflectiveOperationException e) {
       // We do not want to crash Desugar, if we cannot load or access these classes or fields.
       // We aim to provide better diagnostics. If we cannot, just let it go.
-      e.printStackTrace();
+      e.printStackTrace(System.err); // To silence error-prone's complaint.
     }
   }
 
@@ -760,13 +911,12 @@
     return dumpDirectory;
   }
 
-  private static DesugarOptions parseCommandLineOptions(String[] args) throws IOException {
-    if (args.length == 1 && args[0].startsWith("@")) {
-      args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]);
-    }
-    DesugarOptions options =
-        Options.parseAndExitUponError(DesugarOptions.class, /*allowResidue=*/ false, args)
-            .getOptions();
+  private static DesugarOptions parseCommandLineOptions(String[] args) {
+    OptionsParser parser = OptionsParser.newOptionsParser(DesugarOptions.class);
+    parser.setAllowResidue(false);
+    parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
+    parser.parseAndExitUponError(args);
+    DesugarOptions options = parser.getOptions(DesugarOptions.class);
 
     checkArgument(!options.inputJars.isEmpty(), "--input is required");
     checkArgument(
@@ -780,6 +930,10 @@
     for (Path path : options.bootclasspath) {
       checkArgument(!Files.isDirectory(path), "Bootclasspath entry must be a jar file: %s", path);
     }
+    checkArgument(!options.desugarCoreLibs
+        || !options.rewriteCoreLibraryPrefixes.isEmpty()
+        || !options.emulateCoreLibraryInterfaces.isEmpty(),
+        "--desugar_supported_core_libs requires specifying renamed and/or emulated core libraries");
     return options;
   }
 
@@ -793,19 +947,6 @@
     return ioPairListbuilder.build();
   }
 
-  @VisibleForTesting
-  static class ThrowingClassLoader extends ClassLoader {
-    @Override
-    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
-      if (name.startsWith("java.")) {
-        // Use system class loader for java. classes, since ClassLoader.defineClass gets
-        // grumpy when those don't come from the standard place.
-        return super.loadClass(name, resolve);
-      }
-      throw new ClassNotFoundException();
-    }
-  }
-
   private static void deleteTreeOnExit(final Path directory) {
     Thread shutdownHook =
         new Thread() {
@@ -844,26 +985,6 @@
     }
   }
 
-  /** Transform a Path to an {@link OutputFileProvider} */
-  @MustBeClosed
-  private static OutputFileProvider toOutputFileProvider(Path path) throws IOException {
-    if (Files.isDirectory(path)) {
-      return new DirectoryOutputFileProvider(path);
-    } else {
-      return new ZipOutputFileProvider(path);
-    }
-  }
-
-  /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
-  @MustBeClosed
-  private static InputFileProvider toInputFileProvider(Path path) throws IOException {
-    if (Files.isDirectory(path)) {
-      return new DirectoryInputFileProvider(path);
-    } else {
-      return new ZipInputFileProvider(path);
-    }
-  }
-
   /**
    * Transform a list of Path to a list of InputFileProvider and register them with the given
    * closer.
@@ -874,7 +995,7 @@
       Closer closer, List<Path> paths) throws IOException {
     ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
     for (Path path : paths) {
-      builder.add(closer.register(toInputFileProvider(path)));
+      builder.add(closer.register(InputFileProvider.open(path)));
     }
     return builder.build();
   }
diff --git a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
new file mode 100644
index 0000000..355dd97
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
@@ -0,0 +1,92 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Visitor that renames emulated interfaces and marks classes that extend emulated interfaces to
+ * also implement the renamed interfaces.  {@link DefaultMethodClassFixer} makes sure the requisite
+ * methods are present in all classes implementing the renamed interface.  Doing this helps with
+ * dynamic dispatch on emulated interfaces.
+ */
+public class EmulatedInterfaceRewriter extends ClassVisitor {
+
+  private static final String[] EMPTY_ARRAY = new String[0];
+
+  private final CoreLibrarySupport support;
+
+  public EmulatedInterfaceRewriter(ClassVisitor dest, CoreLibrarySupport support) {
+    super(Opcodes.ASM6, dest);
+    this.support = support;
+  }
+
+  @Override
+  public void visit(
+      int version,
+      int access,
+      String name,
+      String signature,
+      String superName,
+      String[] interfaces) {
+    boolean emulated = support.isEmulatedCoreClassOrInterface(name);
+    {
+      // 1. see if we should implement any additional interfaces.
+      // Use LinkedHashSet to dedupe but maintain deterministic order
+      LinkedHashSet<String> newInterfaces = new LinkedHashSet<>();
+      if (interfaces != null && interfaces.length > 0) {
+        // Make classes implementing emulated interfaces also implement the renamed interfaces we
+        // create below.  This includes making the renamed interfaces extends each other as needed.
+        Collections.addAll(newInterfaces, interfaces);
+        for (String itf : interfaces) {
+          if (support.isEmulatedCoreClassOrInterface(itf)) {
+            newInterfaces.add(support.renameCoreLibrary(itf));
+          }
+        }
+      }
+      if (!emulated) {
+        // For an immediate subclass of an emulated class, also fill in any interfaces implemented
+        // by superclasses, similar to the additional default method stubbing performed in
+        // DefaultMethodClassFixer in this situation.
+        Class<?> superclass = support.getEmulatedCoreClassOrInterface(superName);
+        while (superclass != null) {
+          for (Class<?> implemented : superclass.getInterfaces()) {
+            String itf = implemented.getName().replace('.', '/');
+            if (support.isEmulatedCoreClassOrInterface(itf)) {
+              newInterfaces.add(support.renameCoreLibrary(itf));
+            }
+          }
+          superclass = superclass.getSuperclass();
+        }
+      }
+      // Update implemented interfaces and signature if we did anything above
+      if (interfaces == null
+          ? !newInterfaces.isEmpty()
+          : interfaces.length != newInterfaces.size()) {
+        interfaces = newInterfaces.toArray(EMPTY_ARRAY);
+        signature = null; // additional interfaces invalidate any signature
+      }
+    }
+
+    // 2. see if we need to rename this interface itself
+    if (BitFlags.isInterface(access) && emulated) {
+      name = support.renameCoreLibrary(name);
+    }
+    super.visit(version, access, name, signature, superName, interfaces);
+  }
+}
diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
index b8b3ead..e9e3199 100644
--- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -17,6 +17,9 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import com.google.devtools.build.android.desugar.io.FieldInfo;
+import java.lang.reflect.Method;
 import javax.annotation.Nullable;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassVisitor;
@@ -46,7 +49,9 @@
 
   private final ClassVsInterface interfaceCache;
   private final DependencyCollector depsCollector;
+  private final CoreLibrarySupport coreLibrarySupport;
   private final ClassReaderFactory bootclasspath;
+  private final ClassLoader targetLoader;
   private final GeneratedClassStore store;
   private final boolean legacyJaCoCo;
 
@@ -61,13 +66,17 @@
       ClassVisitor dest,
       ClassVsInterface interfaceCache,
       DependencyCollector depsCollector,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassReaderFactory bootclasspath,
+      ClassLoader targetLoader,
       GeneratedClassStore store,
       boolean legacyJaCoCo) {
     super(Opcodes.ASM6, dest);
     this.interfaceCache = interfaceCache;
     this.depsCollector = depsCollector;
+    this.coreLibrarySupport = coreLibrarySupport;
     this.bootclasspath = bootclasspath;
+    this.targetLoader = targetLoader;
     this.store = store;
     this.legacyJaCoCo = legacyJaCoCo;
   }
@@ -210,6 +219,10 @@
               internalName,
               desc);
           ++numberOfDefaultMethods;
+          if (coreLibrarySupport != null) {
+            coreLibrarySupport.registerIfEmulatedCoreInterface(
+                access, internalName, name, desc, exceptions);
+          }
           abstractDest =
               super.visitMethod(access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions);
         }
@@ -234,7 +247,12 @@
     }
     return result != null
         ? new InterfaceInvocationRewriter(
-            result, isInterface() ? internalName : null, bootclasspath, depsCollector, codeOwner)
+            result,
+            isInterface() ? internalName : null,
+            bootclasspath,
+            targetLoader,
+            depsCollector,
+            codeOwner)
         : null;
   }
 
@@ -265,8 +283,7 @@
     return "<clinit>".equals(methodName);
   }
 
-  private static String normalizeInterfaceMethodName(
-      String name, boolean isLambda, boolean isStatic) {
+  static String normalizeInterfaceMethodName(String name, boolean isLambda, boolean isStatic) {
     if (isLambda) {
       // Rename lambda method to reflect the new owner.  Not doing so confuses LambdaDesugaring
       // if it's run over this class again. LambdaDesugaring has already renamed the method from
@@ -355,6 +372,7 @@
     @Nullable private final String interfaceName;
 
     private final ClassReaderFactory bootclasspath;
+    private final ClassLoader targetLoader;
     private final DependencyCollector depsCollector;
     /** Internal name that'll be used to record any dependencies on interface methods. */
     private final String declaringClass;
@@ -363,11 +381,13 @@
         MethodVisitor dest,
         @Nullable String knownInterfaceName,
         ClassReaderFactory bootclasspath,
+        ClassLoader targetLoader,
         DependencyCollector depsCollector,
         String declaringClass) {
       super(Opcodes.ASM6, dest);
       this.interfaceName = knownInterfaceName;
       this.bootclasspath = bootclasspath;
+      this.targetLoader = targetLoader;
       this.depsCollector = depsCollector;
       this.declaringClass = declaringClass;
     }
@@ -410,7 +430,16 @@
           checkArgument(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX),
               "shouldn't consider %s an interface", owner);
           if (opcode == Opcodes.INVOKESPECIAL) {
-            // Turn Interface.super.m() into Interface$$CC.m(receiver)
+            // Turn Interface.super.m() into DefiningInterface$$CC.m(receiver). Note that owner
+            // always refers to the current type's immediate super-interface, but the default method
+            // may be inherited by that interface, so we have to figure out where the method is
+            // defined and invoke it in the corresponding companion class (b/73355452).  Note that
+            // we're always dealing with interfaces here, and all interface methods are public,
+            // so using Class.getMethods should suffice to find inherited methods.  Also note this
+            // can only be a default method invocation, no abstract method invocation.
+            owner =
+                findDefaultMethod(owner, name, desc)
+                    .getDeclaringClass().getName().replace('.', '/');
             opcode = Opcodes.INVOKESTATIC;
             desc = companionDefaultMethodDescriptor(owner, desc);
           }
@@ -422,6 +451,23 @@
       }
       super.visitMethodInsn(opcode, owner, name, desc, itf);
     }
+
+    private Method findDefaultMethod(String owner, String name, String desc) {
+      try {
+        Class<?> clazz = targetLoader.loadClass(owner.replace('/', '.'));
+        // otherwise getting public methods with getMethods() below isn't enough
+        checkArgument(clazz.isInterface(), "Not an interface: %s", owner);
+        for (Method m : clazz.getMethods()) {
+          if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) {
+            checkState(m.isDefault(), "Found non-default method: %s", m);
+            return m;
+          }
+        }
+      } catch (ClassNotFoundException e) {
+        throw new IllegalStateException("Couldn't load " + owner, e);
+      }
+      throw new IllegalArgumentException("Method not found: " + owner + "." + name + desc);
+    }
   }
 
   /**
diff --git a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
index 37a45dd..2090d5c 100644
--- a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
+++ b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.devtools.build.android.desugar.io.BitFlags;
 import javax.annotation.Nullable;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.Attribute;
diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
index fb05bcb..6b0a921 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.android.desugar.io.BitFlags;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import org.objectweb.asm.AnnotationVisitor;
diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
index 72f039a..94c6bbc 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java
@@ -81,12 +81,12 @@
     if (!Files.exists(rootPathPrefix.getParent())) {
       return ImmutableList.of();
     }
-    try (Stream<Path> paths =
-        Files.list(rootPathPrefix.getParent())
-            .filter(
-                path -> path.toString().startsWith(rootPathPrefixStr)
-                    && !existingPaths.contains(path))) {
-      return paths.collect(ImmutableList.toImmutableList());
+    try (Stream<Path> paths = Files.list(rootPathPrefix.getParent())) {
+      return paths
+          .filter(
+              path ->
+                  path.toString().startsWith(rootPathPrefixStr) && !existingPaths.contains(path))
+          .collect(ImmutableList.toImmutableList());
     }
   }
 }
diff --git a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
index 5f41347..f9b5316 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java
@@ -21,6 +21,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.android.desugar.io.BitFlags;
 import java.io.IOException;
 import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles.Lookup;
diff --git a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
index f66d862..7f2f355 100644
--- a/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/LongCompareMethodRewriter.java
@@ -17,6 +17,7 @@
 import static org.objectweb.asm.Opcodes.INVOKESTATIC;
 import static org.objectweb.asm.Opcodes.LCMP;
 
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.MethodVisitor;
 
@@ -26,8 +27,11 @@
  */
 public class LongCompareMethodRewriter extends ClassVisitor {
 
-  public LongCompareMethodRewriter(ClassVisitor cv) {
+  private final CoreLibraryRewriter rewriter;
+
+  public LongCompareMethodRewriter(ClassVisitor cv, CoreLibraryRewriter rewriter) {
     super(ASM6, cv);
+    this.rewriter = rewriter;
   }
 
   @Override
@@ -37,7 +41,7 @@
     return visitor == null ? visitor : new LongCompareMethodVisitor(visitor);
   }
 
-  private static class LongCompareMethodVisitor extends MethodVisitor {
+  private class LongCompareMethodVisitor extends MethodVisitor {
 
     public LongCompareMethodVisitor(MethodVisitor visitor) {
       super(ASM6, visitor);
@@ -45,14 +49,14 @@
 
     @Override
     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
-      if (opcode != INVOKESTATIC
-          || !owner.equals("java/lang/Long")
-          || !name.equals("compare")
-          || !desc.equals("(JJ)I")) {
+      if (opcode == INVOKESTATIC
+          && rewriter.unprefix(owner).equals("java/lang/Long")
+          && name.equals("compare")
+          && desc.equals("(JJ)I")) {
+        super.visitInsn(LCMP);
+      } else {
         super.visitMethodInsn(opcode, owner, name, desc, itf);
-        return;
       }
-      super.visitInsn(LCMP);
     }
   }
 }
diff --git a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
index 86465d6..931459a 100644
--- a/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/ObjectsRequireNonNullMethodRewriter.java
@@ -19,6 +19,7 @@
 import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
 import static org.objectweb.asm.Opcodes.POP;
 
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.MethodVisitor;
 
@@ -29,8 +30,11 @@
  */
 public class ObjectsRequireNonNullMethodRewriter extends ClassVisitor {
 
-  public ObjectsRequireNonNullMethodRewriter(ClassVisitor cv) {
+  private final CoreLibraryRewriter rewriter;
+
+  public ObjectsRequireNonNullMethodRewriter(ClassVisitor cv, CoreLibraryRewriter rewriter) {
     super(ASM6, cv);
+    this.rewriter = rewriter;
   }
 
   @Override
@@ -40,7 +44,7 @@
     return visitor == null ? visitor : new ObjectsMethodInlinerMethodVisitor(visitor);
   }
 
-  private static class ObjectsMethodInlinerMethodVisitor extends MethodVisitor {
+  private class ObjectsMethodInlinerMethodVisitor extends MethodVisitor {
 
     public ObjectsMethodInlinerMethodVisitor(MethodVisitor mv) {
       super(ASM6, mv);
@@ -48,20 +52,19 @@
 
     @Override
     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
-      if (opcode != INVOKESTATIC
-          || !owner.equals("java/util/Objects")
-          || !name.equals("requireNonNull")
-          || !desc.equals("(Ljava/lang/Object;)Ljava/lang/Object;")) {
+      if (opcode == INVOKESTATIC
+          && rewriter.unprefix(owner).equals("java/util/Objects")
+          && name.equals("requireNonNull")
+          && desc.equals("(Ljava/lang/Object;)Ljava/lang/Object;")) {
+        // a call to Objects.requireNonNull(Object o)
+        // duplicate the first argument 'o', as this method returns 'o'.
+        super.visitInsn(DUP);
+        super.visitMethodInsn(
+            INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
+        super.visitInsn(POP);
+      } else {
         super.visitMethodInsn(opcode, owner, name, desc, itf);
-        return;
       }
-
-      // a call to Objects.requireNonNull(Object o)
-      // duplicate the first argument 'o', as this method returns 'o'.
-      super.visitInsn(DUP);
-      super.visitMethodInsn(
-          INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
-      super.visitInsn(POP);
     }
   }
 }
diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
index 8e6d6d5..98eef45 100644
--- a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
@@ -29,6 +29,7 @@
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.android.desugar.BytecodeTypeInference.InferredType;
+import com.google.devtools.build.android.desugar.io.BitFlags;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.Optional;
@@ -165,14 +166,12 @@
             new CloseResourceMethodSpecializer(cv, resourceInternalName, isInterface));
       }
     } else {
+      // It is possible that all calls to $closeResources(...) are in dead code regions, and the
+      // calls are eliminated, which leaving the method $closeResources() unused. (b/78030676).
+      // In this case, we just discard the method body.
       checkState(
-          closeResourceMethod == null,
-          "The field resourceTypeInternalNames is empty. "
-              + "But the class has the $closeResource method.");
-      checkState(
-          !hasCloseResourceMethod,
-          "The class %s has close resource method, but resourceTypeInternalNames is empty.",
-          internalName);
+          !hasCloseResourceMethod || closeResourceMethod != null,
+          "There should be $closeResources(...) in the class file.");
     }
     super.visitEnd();
   }
@@ -311,18 +310,20 @@
 
         InferredType resourceType = typeInference.getTypeOfOperandFromTop(0);
         Optional<String> resourceClassInternalName = resourceType.getInternalName();
-        checkState(
-            resourceClassInternalName.isPresent(),
-            "The resource class %s is not a reference type in %s.%s",
-            resourceType,
-            internalName,
-            methodSignature);
-        checkState(
-            isAssignableFrom(
-                "java.lang.AutoCloseable", resourceClassInternalName.get().replace('/', '.')),
-            "The resource type should be a subclass of java.lang.AutoCloseable: %s",
-            resourceClassInternalName);
-
+        {
+          // Check the resource type.
+          checkState(
+              resourceClassInternalName.isPresent(),
+              "The resource class %s is not a reference type in %s.%s",
+              resourceType,
+              internalName,
+              methodSignature);
+          String resourceClassName = resourceClassInternalName.get().replace('/', '.');
+          checkState(
+              hasCloseMethod(resourceClassName),
+              "The resource class %s should have a close() method.",
+              resourceClassName);
+        }
         resourceTypeInternalNames.add(resourceClassInternalName.get());
         super.visitMethodInsn(
             opcode,
@@ -356,6 +357,26 @@
       return isAssignableFrom("java.lang.Throwable", owner.replace('/', '.'));
     }
 
+    private boolean hasCloseMethod(String resourceClassName) {
+      try {
+        Class<?> klass = classLoader.loadClass(resourceClassName);
+        klass.getMethod("close");
+        return true;
+      } catch (ClassNotFoundException e) {
+        throw new AssertionError(
+            "Failed to load class "
+                + resourceClassName
+                + " when desugaring method "
+                + internalName
+                + "."
+                + methodSignature,
+            e);
+      } catch (NoSuchMethodException e) {
+        // There is no close() method in the class, so return false.
+        return false;
+      }
+    }
+
     private boolean isAssignableFrom(String baseClassName, String subClassName) {
       try {
         Class<?> baseClass = classLoader.loadClass(baseClassName);
diff --git a/java/com/google/devtools/build/android/desugar/BitFlags.java b/java/com/google/devtools/build/android/desugar/io/BitFlags.java
similarity index 95%
rename from java/com/google/devtools/build/android/desugar/BitFlags.java
rename to java/com/google/devtools/build/android/desugar/io/BitFlags.java
index 8be2288..af6f481 100644
--- a/java/com/google/devtools/build/android/desugar/BitFlags.java
+++ b/java/com/google/devtools/build/android/desugar/io/BitFlags.java
@@ -11,12 +11,12 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import org.objectweb.asm.Opcodes;
 
 /** Convenience method for working with {@code int} bitwise flags. */
-class BitFlags {
+public class BitFlags {
 
   /**
    * Returns {@code true} iff <b>all</b> bits in {@code bitmask} are set in {@code flags}. Trivially
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
similarity index 87%
rename from java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java
rename to java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
index 7f1591b..f3c546c 100644
--- a/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/io/CoreLibraryRewriter.java
@@ -11,10 +11,11 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import java.io.IOException;
 import java.io.InputStream;
+import javax.annotation.Nullable;
 import org.objectweb.asm.Attribute;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
@@ -24,7 +25,7 @@
 import org.objectweb.asm.commons.Remapper;
 
 /** Utility class to prefix or unprefix class names of core library classes */
-class CoreLibraryRewriter {
+public class CoreLibraryRewriter {
   private final String prefix;
 
   public CoreLibraryRewriter(String prefix) {
@@ -85,6 +86,10 @@
     return false;
   }
 
+  public String getPrefix() {
+    return prefix;
+  }
+
   /** Removes prefix from class names */
   public String unprefix(String typeName) {
     if (prefix.isEmpty() || !typeName.startsWith(prefix)) {
@@ -152,6 +157,8 @@
   public class UnprefixingClassWriter extends ClassVisitor {
     private final ClassWriter writer;
 
+    private String finalClassName;
+
     UnprefixingClassWriter(int flags) {
       super(Opcodes.ASM6);
       this.writer = new ClassWriter(flags);
@@ -159,7 +166,7 @@
       if (!prefix.isEmpty()) {
         this.cv =
             new ClassRemapper(
-                this.cv,
+                this.writer,
                 new Remapper() {
                   @Override
                   public String map(String typeName) {
@@ -169,8 +176,26 @@
       }
     }
 
-    byte[] toByteArray() {
+    /** Returns the (unprefixed) name of the class once written. */
+    @Nullable
+    public String getClassName() {
+      return finalClassName;
+    }
+
+    public byte[] toByteArray() {
       return writer.toByteArray();
     }
+
+    @Override
+    public void visit(
+        int version,
+        int access,
+        String name,
+        String signature,
+        String superName,
+        String[] interfaces) {
+      finalClassName = unprefix(name);
+      super.visit(version, access, name, signature, superName, interfaces);
+    }
   }
 }
diff --git a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
similarity index 97%
rename from java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java
rename to java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
index 1c5abc9..c607b42 100644
--- a/java/com/google/devtools/build/android/desugar/DirectoryInputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/DirectoryInputFileProvider.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import java.io.File;
 import java.io.FileInputStream;
diff --git a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
similarity index 93%
rename from java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java
rename to java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
index 782a81e..f8e87cb 100644
--- a/java/com/google/devtools/build/android/desugar/DirectoryOutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/DirectoryOutputFileProvider.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
@@ -21,7 +21,7 @@
 import java.nio.file.Path;
 
 /** Output provider is a directory. */
-public class DirectoryOutputFileProvider implements OutputFileProvider {
+class DirectoryOutputFileProvider implements OutputFileProvider {
 
   private final Path root;
 
diff --git a/java/com/google/devtools/build/android/desugar/FieldInfo.java b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java
similarity index 87%
rename from java/com/google/devtools/build/android/desugar/FieldInfo.java
rename to java/com/google/devtools/build/android/desugar/io/FieldInfo.java
index c281039..0b4f634 100644
--- a/java/com/google/devtools/build/android/desugar/FieldInfo.java
+++ b/java/com/google/devtools/build/android/desugar/io/FieldInfo.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import com.google.auto.value.AutoValue;
 
@@ -19,7 +19,7 @@
 @AutoValue
 public abstract class FieldInfo {
 
-  static FieldInfo create(String owner, String name, String desc) {
+  public static FieldInfo create(String owner, String name, String desc) {
     return new AutoValue_FieldInfo(owner, name, desc);
   }
 
diff --git a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
similarity index 97%
rename from java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
rename to java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
index 0a757bf..f70dc0e 100644
--- a/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java
+++ b/java/com/google/devtools/build/android/desugar/io/HeaderClassLoader.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import com.google.common.collect.ImmutableList;
 import java.io.IOError;
@@ -33,7 +33,7 @@
  *
  * @see java.net.URLClassLoader
  */
-class HeaderClassLoader extends ClassLoader {
+public class HeaderClassLoader extends ClassLoader {
 
   private final IndexedInputs indexedInputs;
   private final CoreLibraryRewriter rewriter;
@@ -58,7 +58,8 @@
       // Have ASM compute maxs so we don't need to figure out how many formal parameters there are
       ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
       ImmutableList<FieldInfo> interfaceFieldNames = getFieldsIfReaderIsInterface(reader);
-      reader.accept(new CodeStubber(writer, interfaceFieldNames), 0);
+      // TODO(kmb): Consider SKIP_CODE and stubbing everything so class loader doesn't verify code
+      reader.accept(new CodeStubber(writer, interfaceFieldNames), ClassReader.SKIP_DEBUG);
       bytecode = writer.toByteArray();
     } catch (IOException e) {
       throw new IOError(e);
diff --git a/java/com/google/devtools/build/android/desugar/IndexedInputs.java b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
similarity index 97%
rename from java/com/google/devtools/build/android/desugar/IndexedInputs.java
rename to java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
index 33c6132..8ce4b62 100644
--- a/java/com/google/devtools/build/android/desugar/IndexedInputs.java
+++ b/java/com/google/devtools/build/android/desugar/io/IndexedInputs.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
@@ -28,7 +28,7 @@
  * scanning all inputs over and over for each class to load. An indexed inputs can have a parent
  * that is firstly used when a file name is searched.
  */
-class IndexedInputs {
+public class IndexedInputs {
 
   private final ImmutableMap<String, InputFileProvider> inputFiles;
 
diff --git a/java/com/google/devtools/build/android/desugar/InputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
similarity index 70%
rename from java/com/google/devtools/build/android/desugar/InputFileProvider.java
rename to java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
index c2b6353..c41d018 100644
--- a/java/com/google/devtools/build/android/desugar/InputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/InputFileProvider.java
@@ -11,15 +11,18 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
+import com.google.errorprone.annotations.MustBeClosed;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.zip.ZipEntry;
 
 /** Input file provider allows to iterate on relative path filename of a directory or a jar file. */
-interface InputFileProvider extends Closeable, Iterable<String> {
+public interface InputFileProvider extends Closeable, Iterable<String> {
 
   /**
    * Return a ZipEntry for {@code filename}. If the provider is a {@link ZipInputFileProvider}, the
@@ -33,4 +36,14 @@
    * responsibility of the caller to close this stream.
    */
   InputStream getInputStream(String filename) throws IOException;
+
+  /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
+  @MustBeClosed
+  public static InputFileProvider open(Path path) throws IOException {
+    if (Files.isDirectory(path)) {
+      return new DirectoryInputFileProvider(path);
+    } else {
+      return new ZipInputFileProvider(path);
+    }
+  }
 }
diff --git a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
similarity index 71%
rename from java/com/google/devtools/build/android/desugar/OutputFileProvider.java
rename to java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
index 7a590ef..e693786 100644
--- a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/OutputFileProvider.java
@@ -11,12 +11,15 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
+import com.google.errorprone.annotations.MustBeClosed;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
 /** Output file provider allows to write files in directory or jar files. */
-interface OutputFileProvider extends AutoCloseable {
+public interface OutputFileProvider extends AutoCloseable {
 
   /** Filename to use to write out dependency metadata for later consistency checking. */
   public static final String DESUGAR_DEPS_FILENAME = "META-INF/desugar_deps";
@@ -29,4 +32,14 @@
 
   /** Write {@code content} in {@code filename} to this output */
   void write(String filename, byte[] content) throws IOException;
+
+  /** Transform a Path to an {@link OutputFileProvider} */
+  @MustBeClosed
+  public static OutputFileProvider create(Path path) throws IOException {
+    if (Files.isDirectory(path)) {
+      return new DirectoryOutputFileProvider(path);
+    } else {
+      return new ZipOutputFileProvider(path);
+    }
+  }
 }
diff --git a/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java
new file mode 100644
index 0000000..16f83f2
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/io/ThrowingClassLoader.java
@@ -0,0 +1,27 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.io;
+
+/** Class loader that throws whenever it can, for use the parent of a class loader hierarchy. */
+public class ThrowingClassLoader extends ClassLoader {
+  @Override
+  protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+    if (name.startsWith("java.")) {
+      // Use system class loader for java. classes, since ClassLoader.defineClass gets
+      // grumpy when those don't come from the standard place.
+      return super.loadClass(name, resolve);
+    }
+    throw new ClassNotFoundException();
+  }
+}
\ No newline at end of file
diff --git a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
similarity index 96%
rename from java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java
rename to java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
index 307c8b8..9bd7758 100644
--- a/java/com/google/devtools/build/android/desugar/ZipInputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/ZipInputFileProvider.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import com.google.common.base.Functions;
 import com.google.common.collect.Iterators;
diff --git a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
similarity index 95%
rename from java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
rename to java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
index 8d6501d..36cb26d 100644
--- a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/io/ZipOutputFileProvider.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
@@ -26,7 +26,7 @@
 import java.util.zip.ZipOutputStream;
 
 /** Output provider is a zip file. */
-public class ZipOutputFileProvider implements OutputFileProvider {
+class ZipOutputFileProvider implements OutputFileProvider {
 
   private final ZipOutputStream out;
 
diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepReference.java b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java
new file mode 100644
index 0000000..bae3f38
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/KeepReference.java
@@ -0,0 +1,51 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.Immutable;
+
+@AutoValue
+@Immutable
+abstract class KeepReference {
+  public static KeepReference classReference(String internalName) {
+    checkArgument(!internalName.isEmpty());
+    return new AutoValue_KeepReference(internalName, "", "");
+  }
+
+  public static KeepReference memberReference(String internalName, String name, String desc) {
+    checkArgument(!internalName.isEmpty());
+    checkArgument(!name.isEmpty());
+    checkArgument(!desc.isEmpty());
+    return new AutoValue_KeepReference(internalName, name, desc);
+  }
+
+  public final boolean isMemberReference() {
+    return !name().isEmpty();
+  }
+
+  public final boolean isMethodReference() {
+    return desc().startsWith("(");
+  }
+
+  public final boolean isFieldReference() {
+    return isMemberReference() && !isMethodReference();
+  }
+
+  public abstract String internalName();
+  public abstract String name();
+  public abstract String desc();
+}
diff --git a/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java
new file mode 100644
index 0000000..4924f7c
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/KeepScanner.java
@@ -0,0 +1,303 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Closer;
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.InputFileProvider;
+import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Method;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Type;
+
+class KeepScanner {
+
+  public static class KeepScannerOptions extends OptionsBase {
+    @Option(
+      name = "input",
+      defaultValue = "null",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = OptionEffectTag.UNKNOWN,
+      converter = ExistingPathConverter.class,
+      abbrev = 'i',
+      help = "Input Jar with classes to scan."
+    )
+    public Path inputJars;
+
+    @Option(
+      name = "classpath_entry",
+      allowMultiple = true,
+      defaultValue = "",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      converter = ExistingPathConverter.class,
+      help =
+          "Ordered classpath (Jar or directory) to resolve symbols in the --input Jar, like "
+              + "javac's -cp flag."
+    )
+    public List<Path> classpath;
+
+    @Option(
+      name = "bootclasspath_entry",
+      allowMultiple = true,
+      defaultValue = "",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      converter = ExistingPathConverter.class,
+      help =
+          "Bootclasspath that was used to compile the --input Jar with, like javac's "
+              + "-bootclasspath flag (required)."
+    )
+    public List<Path> bootclasspath;
+
+    @Option(
+      name = "keep_file",
+      defaultValue = "null",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = OptionEffectTag.UNKNOWN,
+      converter = PathConverter.class,
+      help = "Where to write keep rules to."
+    )
+    public Path keepDest;
+
+    @Option(
+      name = "prefix",
+      defaultValue = "j$/",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = OptionEffectTag.UNKNOWN,
+      help = "type to scan for."
+    )
+    public String prefix;
+  }
+
+  public static void main(String... args) throws Exception {
+    OptionsParser parser = OptionsParser.newOptionsParser(KeepScannerOptions.class);
+    parser.setAllowResidue(false);
+    parser.enableParamsFileSupport(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
+    parser.parseAndExitUponError(args);
+    KeepScannerOptions options = parser.getOptions(KeepScannerOptions.class);
+
+    Map<String, ImmutableSet<KeepReference>> seeds;
+    try (Closer closer = Closer.create()) {
+      // TODO(kmb): Try to share more of this code with Desugar binary
+      IndexedInputs classpath =
+          new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
+      IndexedInputs bootclasspath =
+          new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath));
+
+      // Construct classloader from classpath.  Since we're assuming the prefix we're looking for
+      // isn't part of the input itself we shouldn't need to include the input in the classloader.
+      CoreLibraryRewriter noopRewriter = new CoreLibraryRewriter("");
+      ClassLoader classloader =
+          new HeaderClassLoader(classpath, noopRewriter,
+              new HeaderClassLoader(bootclasspath, noopRewriter,
+                  new ThrowingClassLoader()));
+      seeds = scan(checkNotNull(options.inputJars), options.prefix, classloader);
+    }
+
+    try (PrintStream out =
+        new PrintStream(
+            Files.newOutputStream(options.keepDest, CREATE), /*autoFlush=*/ false, "UTF-8")) {
+      writeKeepDirectives(out, seeds);
+    }
+  }
+
+  /**
+   * Writes a -keep rule for each class listing any members to keep.  We sort classes and members
+   * so the output is deterministic.
+   */
+  private static void writeKeepDirectives(
+      PrintStream out, Map<String, ImmutableSet<KeepReference>> seeds) {
+    seeds
+        .entrySet()
+        .stream()
+        .sorted(comparing(Map.Entry::getKey))
+        .forEachOrdered(
+            type -> {
+              out.printf("-keep class %s {%n", type.getKey().replace('/', '.'));
+              type.getValue()
+                  .stream()
+                  .filter(KeepReference::isMemberReference)
+                  .sorted(comparing(KeepReference::name).thenComparing(KeepReference::desc))
+                  .map(ref -> toKeepDescriptor(ref))
+                  .distinct() // drop duplicates due to method descriptors with different returns
+                  .forEachOrdered(line -> out.append("  ").append(line).append(";").println());
+              out.printf("}%n");
+            });
+  }
+
+  /** Scans for and returns references with owners matching the given prefix grouped by owner. */
+  private static Map<String, ImmutableSet<KeepReference>> scan(
+      Path jarFile, String prefix, ClassLoader classpath) throws IOException {
+    // We read the Jar sequentially since ZipFile uses locks anyway but then allow scanning each
+    // class in parallel.
+    try (ZipFile zip = new ZipFile(jarFile.toFile())) {
+      return zip.stream()
+          .filter(entry -> entry.getName().endsWith(".class"))
+          .map(entry -> readFully(zip, entry))
+          .parallel()
+          .flatMap(
+              content -> PrefixReferenceScanner.scan(new ClassReader(content), prefix).stream())
+          .distinct() // so we don't process the same reference multiple times next
+          .map(ref -> nearestDeclaration(ref, classpath))
+          .collect(
+              Collectors.groupingByConcurrent(
+                  KeepReference::internalName, ImmutableSet.toImmutableSet()));
+    }
+  }
+
+  private static byte[] readFully(ZipFile zip, ZipEntry entry) {
+    byte[] result = new byte[(int) entry.getSize()];
+    try (InputStream content = zip.getInputStream(entry)) {
+      ByteStreams.readFully(content, result);
+      return result;
+    } catch (IOException e) {
+      throw new IOError(e);
+    }
+  }
+
+  /**
+   * Find the nearest definition of the given reference in the class hierarchy and return the
+   * modified reference.  This is needed b/c bytecode sometimes refers to a method or field using
+   * an owner type that inherits the method or field instead of defining the member itself.
+   * In that case we need to find and keep the inherited definition.
+   */
+  private static KeepReference nearestDeclaration(KeepReference ref, ClassLoader classpath) {
+    if (!ref.isMemberReference() || "<init>".equals(ref.name())) {
+      return ref; // class and constructor references don't need any further work
+    }
+
+    Class<?> clazz;
+    try {
+      clazz = classpath.loadClass(ref.internalName().replace('/', '.'));
+    } catch (ClassNotFoundException e) {
+      throw (NoClassDefFoundError) new NoClassDefFoundError("Couldn't load " + ref).initCause(e);
+    }
+
+    Class<?> owner = findDeclaringClass(clazz, ref);
+    if (owner == clazz) {
+      return ref;
+    }
+    String parent = checkNotNull(owner, "Can't resolve: %s", ref).getName().replace('.', '/');
+    return KeepReference.memberReference(parent, ref.name(), ref.desc());
+  }
+
+  private static Class<?> findDeclaringClass(Class<?> clazz, KeepReference ref) {
+    if (ref.isFieldReference()) {
+      try {
+        return clazz.getField(ref.name()).getDeclaringClass();
+      } catch (NoSuchFieldException e) {
+        // field must be non-public, so search class hierarchy
+        do {
+          try {
+            return clazz.getDeclaredField(ref.name()).getDeclaringClass();
+          } catch (NoSuchFieldException ignored) {
+            // fall through for clarity
+          }
+          clazz = clazz.getSuperclass();
+        } while (clazz != null);
+      }
+    } else {
+      checkState(ref.isMethodReference());
+      Type descriptor = Type.getMethodType(ref.desc());
+      for (Method m : clazz.getMethods()) {
+        if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) {
+          return m.getDeclaringClass();
+        }
+      }
+      do {
+        // Method must be non-public, so search class hierarchy
+        for (Method m : clazz.getDeclaredMethods()) {
+          if (m.getName().equals(ref.name()) && Type.getType(m).equals(descriptor)) {
+            return m.getDeclaringClass();
+          }
+        }
+        clazz = clazz.getSuperclass();
+      } while (clazz != null);
+    }
+    return null;
+  }
+
+  private static CharSequence toKeepDescriptor(KeepReference member) {
+    StringBuilder result = new StringBuilder();
+    if (member.isMethodReference()) {
+      if (!"<init>".equals(member.name())) {
+        result.append("*** ");
+      }
+      result.append(member.name()).append("(");
+      // Ignore return type as it's unique in the source language
+      boolean first = true;
+      for (Type param : Type.getMethodType(member.desc()).getArgumentTypes()) {
+        if (first) {
+          first = false;
+        } else {
+          result.append(", ");
+        }
+        result.append(param.getClassName());
+      }
+      result.append(")");
+    } else {
+      checkArgument(member.isFieldReference());
+      result.append("*** ").append(member.name()); // field names are unique so ignore descriptor
+    }
+    return result;
+  }
+
+  /**
+   * Transform a list of Path to a list of InputFileProvider and register them with the given
+   * closer.
+   */
+  @SuppressWarnings("MustBeClosedChecker")
+  private static ImmutableList<InputFileProvider> toRegisteredInputFileProvider(
+      Closer closer, List<Path> paths) throws IOException {
+    ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>();
+    for (Path path : paths) {
+      builder.add(closer.register(InputFileProvider.open(path)));
+    }
+    return builder.build();
+  }
+
+  private KeepScanner() {}
+}
diff --git a/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java
new file mode 100644
index 0000000..b899ccc
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/scan/PrefixReferenceScanner.java
@@ -0,0 +1,405 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableSet;
+import javax.annotation.Nullable;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.TypePath;
+
+/** {@link ClassVisitor} that records references to classes starting with a given prefix. */
+class PrefixReferenceScanner extends ClassVisitor {
+
+  /**
+   * Returns references with the given prefix in the given class.
+   *
+   * @param prefix an internal name prefix, typically a package such as {@code com/google/}
+   */
+  public static ImmutableSet<KeepReference> scan(ClassReader reader, String prefix) {
+    PrefixReferenceScanner scanner = new PrefixReferenceScanner(prefix);
+    // Frames irrelevant for Android so skip them.  Don't skip debug info in case the class we're
+    // visiting has local variable tables (typically it doesn't anyways).
+    reader.accept(scanner, ClassReader.SKIP_FRAMES);
+    return scanner.roots.build();
+  }
+
+  private final ImmutableSet.Builder<KeepReference> roots = ImmutableSet.builder();
+  private final PrefixReferenceMethodVisitor mv = new PrefixReferenceMethodVisitor();
+  private final PrefixReferenceFieldVisitor fv = new PrefixReferenceFieldVisitor();
+  private final PrefixReferenceAnnotationVisitor av = new PrefixReferenceAnnotationVisitor();
+
+  private final String prefix;
+
+  public PrefixReferenceScanner(String prefix) {
+    super(Opcodes.ASM6);
+    this.prefix = prefix;
+  }
+
+  @Override
+  public void visit(
+      int version,
+      int access,
+      String name,
+      String signature,
+      String superName,
+      String[] interfaces) {
+    checkArgument(!name.startsWith(prefix));
+    if (superName != null) {
+      classReference(superName);
+    }
+    classReferences(interfaces);
+    super.visit(version, access, name, signature, superName, interfaces);
+  }
+
+  @Override
+  public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+    typeReference(desc);
+    return av;
+  }
+
+  @Override
+  public void visitOuterClass(String owner, String name, String desc) {
+    classReference(owner);
+    if (desc != null) {
+      typeReference(Type.getMethodType(desc));
+    }
+  }
+
+  @Override
+  public AnnotationVisitor visitTypeAnnotation(
+      int typeRef, TypePath typePath, String desc, boolean visible) {
+    typeReference(desc);
+    return av;
+  }
+
+  @Override
+  public void visitInnerClass(String name, String outerName, String innerName, int access) {
+    classReference(name);
+    if (outerName != null) {
+      classReference(outerName);
+    }
+  }
+
+  @Override
+  public FieldVisitor visitField(
+      int access, String name, String desc, String signature, Object value) {
+    typeReference(desc);
+    return fv;
+  }
+
+  @Override
+  public MethodVisitor visitMethod(
+      int access, String name, String desc, String signature, String[] exceptions) {
+    typeReference(Type.getMethodType(desc));
+    classReferences(exceptions);
+    return mv;
+  }
+
+  private void classReferences(@Nullable String[] internalNames) {
+    if (internalNames != null) {
+      for (String itf : internalNames) {
+        classReference(itf);
+      }
+    }
+  }
+
+  // The following methods are package-private so they don't incur bridge methods when called from
+  // inner classes below.
+
+  void classReference(String internalName) {
+    checkArgument(internalName.charAt(0) != '[' && internalName.charAt(0) != '(', internalName);
+    checkArgument(!internalName.endsWith(";"), internalName);
+    if (internalName.startsWith(prefix)) {
+      roots.add(KeepReference.classReference(internalName));
+    }
+  }
+
+  void objectReference(String internalName) {
+    // don't call this for method types, convert to Type instead
+    checkArgument(internalName.charAt(0) != '(', internalName);
+    if (internalName.charAt(0) == '[') {
+      typeReference(internalName);
+    } else {
+      classReference(internalName);
+    }
+  }
+
+  void typeReference(String typeDesc) {
+    // don't call this for method types, convert to Type instead
+    checkArgument(typeDesc.charAt(0) != '(', typeDesc);
+
+    int lpos = typeDesc.lastIndexOf('[') + 1;
+    if (typeDesc.charAt(lpos) == 'L') {
+      checkArgument(typeDesc.endsWith(";"), typeDesc);
+      classReference(typeDesc.substring(lpos, typeDesc.length() - 1));
+    } else {
+      // else primitive or primitive array
+      checkArgument(typeDesc.length() == lpos + 1, typeDesc);
+      switch (typeDesc.charAt(lpos)) {
+        case 'B':
+        case 'C':
+        case 'S':
+        case 'I':
+        case 'J':
+        case 'D':
+        case 'F':
+        case 'Z':
+          break;
+        default:
+          throw new AssertionError("Unexpected type descriptor: " + typeDesc);
+      }
+    }
+  }
+
+  void typeReference(Type type) {
+    switch (type.getSort()) {
+      case Type.ARRAY:
+        typeReference(type.getElementType());
+        break;
+      case Type.OBJECT:
+        classReference(type.getInternalName());
+        break;
+
+      case Type.METHOD:
+        for (Type param : type.getArgumentTypes()) {
+          typeReference(param);
+        }
+        typeReference(type.getReturnType());
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  void fieldReference(String owner, String name, String desc) {
+    objectReference(owner);
+    typeReference(desc);
+    if (owner.startsWith(prefix)) {
+      roots.add(KeepReference.memberReference(owner, name, desc));
+    }
+  }
+
+  void methodReference(String owner, String name, String desc) {
+    checkArgument(desc.charAt(0) == '(', desc);
+    objectReference(owner);
+    typeReference(Type.getMethodType(desc));
+    if (owner.startsWith(prefix)) {
+      roots.add(KeepReference.memberReference(owner, name, desc));
+    }
+  }
+
+  void handleReference(Handle handle) {
+    switch (handle.getTag()) {
+      case Opcodes.H_GETFIELD:
+      case Opcodes.H_GETSTATIC:
+      case Opcodes.H_PUTFIELD:
+      case Opcodes.H_PUTSTATIC:
+        fieldReference(handle.getOwner(), handle.getName(), handle.getDesc());
+        break;
+
+      default:
+        methodReference(handle.getOwner(), handle.getName(), handle.getDesc());
+        break;
+    }
+  }
+
+  private class PrefixReferenceMethodVisitor extends MethodVisitor {
+
+    public PrefixReferenceMethodVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotationDefault() {
+      return av;
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      typeReference(desc);
+      return av;
+    }
+
+    @Override
+    public AnnotationVisitor visitTypeAnnotation(
+        int typeRef, TypePath typePath, String desc, boolean visible) {
+      typeReference(desc);
+      return av;
+    }
+
+    @Override
+    public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+      typeReference(desc);
+      return av;
+    }
+
+    @Override
+    public void visitTypeInsn(int opcode, String type) {
+      objectReference(type);
+    }
+
+    @Override
+    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+      fieldReference(owner, name, desc);
+    }
+
+    @Override
+    @SuppressWarnings("deprecation") // Implementing deprecated method to be sure
+    public void visitMethodInsn(int opcode, String owner, String name, String desc) {
+      visitMethodInsn(opcode, owner, name, desc, opcode == Opcodes.INVOKEINTERFACE);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      methodReference(owner, name, desc);
+    }
+
+    @Override
+    public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
+      typeReference(Type.getMethodType(desc));
+      handleReference(bsm);
+      for (Object bsmArg : bsmArgs) {
+        visitConstant(bsmArg);
+      }
+    }
+
+    @Override
+    public void visitLdcInsn(Object cst) {
+      visitConstant(cst);
+    }
+
+    private void visitConstant(Object cst) {
+      if (cst instanceof Type) {
+        typeReference((Type) cst);
+      } else if (cst instanceof Handle) {
+        handleReference((Handle) cst);
+      } else {
+        // Check for other expected types as javadoc recommends
+        checkArgument(
+            cst instanceof String
+                || cst instanceof Integer
+                || cst instanceof Long
+                || cst instanceof Float
+                || cst instanceof Double,
+            "Unexpected constant: ", cst);
+      }
+    }
+
+    @Override
+    public void visitMultiANewArrayInsn(String desc, int dims) {
+      typeReference(desc);
+    }
+
+    @Override
+    public AnnotationVisitor visitInsnAnnotation(
+        int typeRef, TypePath typePath, String desc, boolean visible) {
+      typeReference(desc);
+      return av;
+    }
+
+    @Override
+    public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+      if (type != null) {
+        classReference(type);
+      }
+    }
+
+    @Override
+    public AnnotationVisitor visitTryCatchAnnotation(
+        int typeRef, TypePath typePath, String desc, boolean visible) {
+      typeReference(desc);
+      return av;
+    }
+
+    @Override
+    public void visitLocalVariable(
+        String name, String desc, String signature, Label start, Label end, int index) {
+      typeReference(desc);
+    }
+
+    @Override
+    public AnnotationVisitor visitLocalVariableAnnotation(
+        int typeRef,
+        TypePath typePath,
+        Label[] start,
+        Label[] end,
+        int[] index,
+        String desc,
+        boolean visible) {
+      typeReference(desc);
+      return av;
+    }
+  }
+
+  private class PrefixReferenceFieldVisitor extends FieldVisitor {
+
+    public PrefixReferenceFieldVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      typeReference(desc);
+      return av;
+    }
+
+    @Override
+    public AnnotationVisitor visitTypeAnnotation(
+        int typeRef, TypePath typePath, String desc, boolean visible) {
+      typeReference(desc);
+      return av;
+    }
+  }
+
+  private class PrefixReferenceAnnotationVisitor extends AnnotationVisitor {
+
+    public PrefixReferenceAnnotationVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (value instanceof Type) {
+        typeReference((Type) value);
+      }
+    }
+
+    @Override
+    public void visitEnum(String name, String desc, String value) {
+      fieldReference(desc.substring(1, desc.length() - 1), value, desc);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String name, String desc) {
+      typeReference(desc);
+      return av;
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      return av;
+    }
+  }
+}
diff --git a/java/com/google/devtools/common/options/Converters.java b/java/com/google/devtools/common/options/Converters.java
index 35f2da4..fb3bbfa 100644
--- a/java/com/google/devtools/common/options/Converters.java
+++ b/java/com/google/devtools/common/options/Converters.java
@@ -26,10 +26,7 @@
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
-/**
- * Some convenient converters used by blaze. Note: These are specific to
- * blaze.
- */
+/** Some convenient converters used by blaze. Note: These are specific to blaze. */
 public final class Converters {
 
   /** Standard converter for booleans. Accepts common shorthands/synonyms. */
@@ -180,9 +177,7 @@
     }
   }
 
-  /**
-   * Standard converter for the {@link java.time.Duration} type.
-   */
+  /** Standard converter for the {@link java.time.Duration} type. */
   public static class DurationConverter implements Converter<Duration> {
     private final Pattern durationRegex = Pattern.compile("^([0-9]+)(d|h|m|s|ms)$");
 
@@ -198,7 +193,7 @@
       }
       long duration = Long.parseLong(m.group(1));
       String unit = m.group(2);
-      switch(unit) {
+      switch (unit) {
         case "d":
           return Duration.ofDays(duration);
         case "h":
@@ -210,8 +205,8 @@
         case "ms":
           return Duration.ofMillis(duration);
         default:
-          throw new IllegalStateException("This must not happen. Did you update the regex without "
-              + "the switch case?");
+          throw new IllegalStateException(
+              "This must not happen. Did you update the regex without the switch case?");
       }
     }
 
@@ -240,14 +235,8 @@
           .build();
 
   /**
-   * Join a list of words as in English.  Examples:
-   * "nothing"
-   * "one"
-   * "one or two"
-   * "one and two"
-   * "one, two or three".
-   * "one, two and three".
-   * The toString method of each element is used.
+   * Join a list of words as in English. Examples: "nothing" "one" "one or two" "one and two" "one,
+   * two or three". "one, two and three". The toString method of each element is used.
    */
   static String joinEnglishList(Iterable<?> choices) {
     StringBuilder buf = new StringBuilder();
@@ -261,14 +250,12 @@
     return buf.length() == 0 ? "nothing" : buf.toString();
   }
 
-  public static class SeparatedOptionListConverter
-      implements Converter<List<String>> {
+  public static class SeparatedOptionListConverter implements Converter<List<String>> {
 
     private final String separatorDescription;
     private final Splitter splitter;
 
-    protected SeparatedOptionListConverter(char separator,
-                                           String separatorDescription) {
+    protected SeparatedOptionListConverter(char separator, String separatorDescription) {
       this.separatorDescription = separatorDescription;
       this.splitter = Splitter.on(separator);
     }
@@ -284,8 +271,7 @@
     }
   }
 
-  public static class CommaSeparatedOptionListConverter
-      extends SeparatedOptionListConverter {
+  public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter {
     public CommaSeparatedOptionListConverter() {
       super(',', "comma");
     }
@@ -299,10 +285,10 @@
 
   public static class LogLevelConverter implements Converter<Level> {
 
-    public static final Level[] LEVELS = new Level[] {
-      Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE,
-      Level.FINER, Level.FINEST
-    };
+    public static final Level[] LEVELS =
+        new Level[] {
+          Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, Level.FINE, Level.FINER, Level.FINEST
+        };
 
     @Override
     public Level convert(String input) throws OptionsParsingException {
@@ -318,12 +304,9 @@
     public String getTypeDescription() {
       return "0 <= an integer <= " + (LEVELS.length - 1);
     }
-
   }
 
-  /**
-   * Checks whether a string is part of a set of strings.
-   */
+  /** Checks whether a string is part of a set of strings. */
   public static class StringSetConverter implements Converter<String> {
 
     // TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/
@@ -349,9 +332,7 @@
     }
   }
 
-  /**
-   * Checks whether a string is a valid regex pattern and compiles it.
-   */
+  /** Checks whether a string is a valid regex pattern and compiles it. */
   public static class RegexPatternConverter implements Converter<Pattern> {
 
     @Override
@@ -369,9 +350,7 @@
     }
   }
 
-  /**
-   * Limits the length of a string argument.
-   */
+  /** Limits the length of a string argument. */
   public static class LengthLimitingConverter implements Converter<String> {
     private final int maxSize;
 
@@ -393,9 +372,7 @@
     }
   }
 
-  /**
-   * Checks whether an integer is in the given range.
-   */
+  /** Checks whether an integer is in the given range. */
   public static class RangeConverter implements Converter<Integer> {
     final int minValue;
     final int maxValue;
@@ -432,25 +409,27 @@
         return "an integer, >= " + minValue;
       } else {
         return "an integer in "
-            + (minValue < 0 ? "(" + minValue + ")" : minValue) + "-" + maxValue + " range";
+            + (minValue < 0 ? "(" + minValue + ")" : minValue)
+            + "-"
+            + maxValue
+            + " range";
       }
     }
   }
 
   /**
-   * A converter for variable assignments from the parameter list of a blaze
-   * command invocation. Assignments are expected to have the form "name=value",
-   * where names and values are defined to be as permissive as possible.
+   * A converter for variable assignments from the parameter list of a blaze command invocation.
+   * Assignments are expected to have the form "name=value", where names and values are defined to
+   * be as permissive as possible.
    */
   public static class AssignmentConverter implements Converter<Map.Entry<String, String>> {
 
     @Override
-    public Map.Entry<String, String> convert(String input)
-        throws OptionsParsingException {
+    public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
       int pos = input.indexOf("=");
       if (pos <= 0) {
-        throw new OptionsParsingException("Variable definitions must be in the form of a "
-            + "'name=value' assignment");
+        throw new OptionsParsingException(
+            "Variable definitions must be in the form of a 'name=value' assignment");
       }
       String name = input.substring(0, pos);
       String value = input.substring(pos + 1);
@@ -461,24 +440,22 @@
     public String getTypeDescription() {
       return "a 'name=value' assignment";
     }
-
   }
 
   /**
-   * A converter for variable assignments from the parameter list of a blaze
-   * command invocation. Assignments are expected to have the form "name[=value]",
-   * where names and values are defined to be as permissive as possible and value
-   * part can be optional (in which case it is considered to be null).
+   * A converter for variable assignments from the parameter list of a blaze command invocation.
+   * Assignments are expected to have the form "name[=value]", where names and values are defined to
+   * be as permissive as possible and value part can be optional (in which case it is considered to
+   * be null).
    */
   public static class OptionalAssignmentConverter implements Converter<Map.Entry<String, String>> {
 
     @Override
-    public Map.Entry<String, String> convert(String input)
-        throws OptionsParsingException {
-      int pos = input.indexOf("=");
+    public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
+      int pos = input.indexOf('=');
       if (pos == 0 || input.length() == 0) {
-        throw new OptionsParsingException("Variable definitions must be in the form of a "
-            + "'name=value' or 'name' assignment");
+        throw new OptionsParsingException(
+            "Variable definitions must be in the form of a 'name=value' or 'name' assignment");
       } else if (pos < 0) {
         return Maps.immutableEntry(input, null);
       }
@@ -491,7 +468,40 @@
     public String getTypeDescription() {
       return "a 'name=value' assignment with an optional value part";
     }
+  }
 
+  /**
+   * A converter for named integers of the form "[name=]value". When no name is specified, an empty
+   * string is used for the key.
+   */
+  public static class NamedIntegersConverter implements Converter<Map.Entry<String, Integer>> {
+
+    @Override
+    public Map.Entry<String, Integer> convert(String input) throws OptionsParsingException {
+      int pos = input.indexOf('=');
+      if (pos == 0 || input.length() == 0) {
+        throw new OptionsParsingException(
+            "Specify either 'value' or 'name=value', where 'value' is an integer");
+      } else if (pos < 0) {
+        try {
+          return Maps.immutableEntry("", Integer.parseInt(input));
+        } catch (NumberFormatException e) {
+          throw new OptionsParsingException("'" + input + "' is not an int");
+        }
+      }
+      String name = input.substring(0, pos);
+      String value = input.substring(pos + 1);
+      try {
+        return Maps.immutableEntry(name, Integer.parseInt(value));
+      } catch (NumberFormatException e) {
+        throw new OptionsParsingException("'" + value + "' is not an int");
+      }
+    }
+
+    @Override
+    public String getTypeDescription() {
+      return "an integer or a named integer, 'name=value'";
+    }
   }
 
   public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> {
@@ -508,5 +518,4 @@
       super(0, 100);
     }
   }
-
 }
diff --git a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
index a53ff5b..3b42a29 100644
--- a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
+++ b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
@@ -248,6 +248,15 @@
     OptionPriority nextPriority =
         OptionPriority.lowestOptionPriorityAtCategory(PriorityCategory.INVOCATION_POLICY);
     for (FlagPolicy policy : invocationPolicy.getFlagPoliciesList()) {
+      // Explicitly disallow --config in invocation policy.
+      if (policy.getFlagName().equals("config")) {
+        throw new OptionsParsingException(
+            "Invocation policy is applied after --config expansion, changing config values now "
+                + "would have no effect and is disallowed to prevent confusion. Please remove the "
+                + "following policy : "
+                + policy);
+      }
+
       // These policies are high-level, before expansion, and so are not the implicitDependents or
       // expansions of any other flag, other than in an obtuse sense from --invocation_policy.
       OptionPriority currentPriority = nextPriority;
@@ -306,9 +315,7 @@
    * <p>None of the flagPolicies returned should be on expansion flags.
    */
   private static List<FlagPolicyWithContext> expandPolicy(
-      FlagPolicyWithContext originalPolicy,
-      OptionsParser parser,
-      Level loglevel)
+      FlagPolicyWithContext originalPolicy, OptionsParser parser, Level loglevel)
       throws OptionsParsingException {
     List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
 
@@ -701,11 +708,15 @@
       }
 
       // Check that if the default value of the flag is disallowed by the policy, that the policy
-      // does not also set use_default. Otherwise the default value would will still be set if the
+      // does not also set use_default. Otherwise the default value would still be set if the
       // user uses a disallowed value. This doesn't apply to repeatable flags since the default
-      // value for repeatable flags is always the empty list.
-      if (!optionDescription.getOptionDefinition().allowsMultiple()) {
-
+      // value for repeatable flags is always the empty list. It also doesn't apply to flags that
+      // are null by default, since these flags' default value is not parsed by the converter, so
+      // there is no guarantee that there exists an accepted user-input value that would also set
+      // the value to NULL. In these cases, we assume that "unset" is a distinct value that is
+      // always allowed.
+      if (!optionDescription.getOptionDefinition().allowsMultiple()
+          && !optionDescription.getOptionDefinition().isSpecialNullDefault()) {
         boolean defaultValueAllowed =
             isFlagValueAllowed(
                 convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue());
@@ -749,8 +760,12 @@
         throws OptionsParsingException {
 
       OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
-      if (!isFlagValueAllowed(
-          convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue())) {
+      if (optionDefinition.isSpecialNullDefault()) {
+        // Do nothing, the unset value by definition cannot be set. In option filtering operations,
+        // the value is being filtered, but the value that is `no value` passes any filter.
+        // Otherwise, there is no way to "usedefault" on one of these options that has no value by
+        // default.
+      } else if (!isFlagValueAllowed(convertedPolicyValues, optionDefinition.getDefaultValue())) {
         if (newValue != null) {
           // Use the default value from the policy, since the original default is not allowed
           logger.log(
diff --git a/java/com/google/devtools/common/options/Option.java b/java/com/google/devtools/common/options/Option.java
index 45f320a..f26a136 100644
--- a/java/com/google/devtools/common/options/Option.java
+++ b/java/com/google/devtools/common/options/Option.java
@@ -186,22 +186,4 @@
    * that the old name is deprecated and the new name should be used.
    */
   String oldName() default "";
-
-  /**
-   * Indicates that this option is a wrapper for other options, and will be unwrapped when parsed.
-   * For example, if foo is a wrapper option, then "--foo=--bar=baz" will be parsed as the flag
-   * "--bar=baz" (rather than --foo taking the value "--bar=baz"). A wrapper option should have the
-   * type {@link Void} (if it is something other than Void, the parser will not assign a value to
-   * it). The {@link Option#implicitRequirements()}, {@link Option#expansion()}, {@link
-   * Option#converter()} attributes will not be processed. Wrapper options are implicitly repeatable
-   * (i.e., as though {@link Option#allowMultiple()} is true regardless of its value in the
-   * annotation).
-   *
-   * <p>Wrapper options are provided only for transitioning flags which appear as values to other
-   * flags, to top-level flags. Wrapper options should not be used in Invocation Policy, as
-   * expansion flags to other flags, or as implicit requirements to other flags. Use the inner flags
-   * instead.
-   */
-  @Deprecated
-  boolean wrapperOption() default false;
 }
diff --git a/java/com/google/devtools/common/options/OptionDefinition.java b/java/com/google/devtools/common/options/OptionDefinition.java
index 84a9d2d..e89234b 100644
--- a/java/com/google/devtools/common/options/OptionDefinition.java
+++ b/java/com/google/devtools/common/options/OptionDefinition.java
@@ -137,7 +137,7 @@
   }
 
   /** {@link Option#expansionFunction()} ()} */
-  Class<? extends ExpansionFunction> getExpansionFunction() {
+  public Class<? extends ExpansionFunction> getExpansionFunction() {
     return optionAnnotation.expansionFunction();
   }
 
@@ -156,11 +156,6 @@
     return optionAnnotation.oldName();
   }
 
-  /** {@link Option#wrapperOption()} ()} ()} */
-  public boolean isWrapperOption() {
-    return optionAnnotation.wrapperOption();
-  }
-
   /** Returns whether an option --foo has a negative equivalent --nofoo. */
   public boolean hasNegativeOption() {
     return getType().equals(boolean.class) || getType().equals(TriState.class);
diff --git a/java/com/google/devtools/common/options/OptionFilterDescriptions.java b/java/com/google/devtools/common/options/OptionFilterDescriptions.java
index 2a7999d..4b4373a 100644
--- a/java/com/google/devtools/common/options/OptionFilterDescriptions.java
+++ b/java/com/google/devtools/common/options/OptionFilterDescriptions.java
@@ -170,6 +170,9 @@
             "This option is deprecated. It might be that the feature it affects is deprecated, "
                 + "or that another method of supplying the information is preferred.")
         .put(
+            OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES,
+            "This option is triggered by the expansion option --all_incompatible_changes.")
+        .put(
             OptionMetadataTag.HIDDEN, // Here for completeness, these options are UNDOCUMENTED.
             "This option should not be used by a user, and should not be logged.")
         .put(
diff --git a/java/com/google/devtools/common/options/OptionInstanceOrigin.java b/java/com/google/devtools/common/options/OptionInstanceOrigin.java
index 584e75b..b0782f8 100644
--- a/java/com/google/devtools/common/options/OptionInstanceOrigin.java
+++ b/java/com/google/devtools/common/options/OptionInstanceOrigin.java
@@ -22,14 +22,14 @@
 public class OptionInstanceOrigin {
   private final OptionPriority priority;
   @Nullable private final String source;
-  @Nullable private final OptionDefinition implicitDependent;
-  @Nullable private final OptionDefinition expandedFrom;
+  @Nullable private final ParsedOptionDescription implicitDependent;
+  @Nullable private final ParsedOptionDescription expandedFrom;
 
   public OptionInstanceOrigin(
       OptionPriority priority,
       String source,
-      OptionDefinition implicitDependent,
-      OptionDefinition expandedFrom) {
+      ParsedOptionDescription implicitDependent,
+      ParsedOptionDescription expandedFrom) {
     this.priority = priority;
     this.source = source;
     this.implicitDependent = implicitDependent;
@@ -46,12 +46,12 @@
   }
 
   @Nullable
-  public OptionDefinition getImplicitDependent() {
+  public ParsedOptionDescription getImplicitDependent() {
     return implicitDependent;
   }
 
   @Nullable
-  public OptionDefinition getExpandedFrom() {
+  public ParsedOptionDescription getExpandedFrom() {
     return expandedFrom;
   }
 }
diff --git a/java/com/google/devtools/common/options/OptionMetadataTag.java b/java/com/google/devtools/common/options/OptionMetadataTag.java
index c511fa6..563aa3e 100644
--- a/java/com/google/devtools/common/options/OptionMetadataTag.java
+++ b/java/com/google/devtools/common/options/OptionMetadataTag.java
@@ -55,7 +55,17 @@
    *
    * <p>These should be in category {@code OptionDocumentationCategory.UNDOCUMENTED}.
    */
-  INTERNAL(4);
+  INTERNAL(4),
+
+  /**
+   * Options that are triggered by --all_incompatible_changes.
+   *
+   * <p>These must also be labelled {@link OptionMetadataTag#INCOMPATIBLE_CHANGE} and have the
+   * prefix --incompatible_. Note that the option name prefix is also a triggering case for the
+   * --all_incompatible_changes expansion, and so all options that start with the "incompatible_"
+   * prefix must have this tag.
+   */
+  TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES(5);
 
   private final int value;
 
diff --git a/java/com/google/devtools/common/options/OptionPriority.java b/java/com/google/devtools/common/options/OptionPriority.java
index ec5d0d8..53f0d75 100644
--- a/java/com/google/devtools/common/options/OptionPriority.java
+++ b/java/com/google/devtools/common/options/OptionPriority.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.common.options;
 
+import com.google.common.collect.ImmutableList;
 import java.util.Objects;
 
 /**
@@ -25,43 +26,63 @@
  */
 public class OptionPriority implements Comparable<OptionPriority> {
   private final PriorityCategory priorityCategory;
-  private final int index;
-  private final boolean locked;
+  /**
+   * Each option that is passed explicitly has 0 ancestors, so it only has its command line index
+   * (or rc index, etc., depending on the category), but expanded options have the command line
+   * index of its parent and then its position within the options that were expanded at that point.
+   * Since options can expand to expanding options, and --config can expand to expansion options as
+   * well, this can technically go arbitrarily deep, but in practice this is very short, of length <
+   * 5, most commonly of length 1.
+   */
+  private final ImmutableList<Integer> priorityIndices;
 
-  private OptionPriority(PriorityCategory priorityCategory, int index, boolean locked) {
+  private boolean alreadyExpanded = false;
+
+  private OptionPriority(
+      PriorityCategory priorityCategory, ImmutableList<Integer> priorityIndices) {
     this.priorityCategory = priorityCategory;
-    this.index = index;
-    this.locked = locked;
+    this.priorityIndices = priorityIndices;
   }
 
   /** Get the first OptionPriority for that category. */
   static OptionPriority lowestOptionPriorityAtCategory(PriorityCategory category) {
-    return new OptionPriority(category, 0, false);
+    return new OptionPriority(category, ImmutableList.of(0));
   }
 
   /**
    * Get the priority for the option following this one. In normal, incremental option parsing, the
-   * returned priority would compareTo as after the current one. Does not increment locked
+   * returned priority would compareTo as after the current one. Does not increment ancestor
    * priorities.
    */
   static OptionPriority nextOptionPriority(OptionPriority priority) {
-    if (priority.locked) {
-      return priority;
-    }
-    return new OptionPriority(priority.priorityCategory, priority.index + 1, false);
+    int lastElementPosition = priority.priorityIndices.size() - 1;
+    return new OptionPriority(
+        priority.priorityCategory,
+        ImmutableList.<Integer>builder()
+            .addAll(priority.priorityIndices.subList(0, lastElementPosition))
+            .add(priority.priorityIndices.get(lastElementPosition) + 1)
+            .build());
   }
 
   /**
-   * Return a priority for this option that will avoid priority increases by calls to
-   * nextOptionPriority.
+   * Some options are expanded to other options, and the children options need to have their order
+   * preserved while maintaining their position between the options that flank the parent option.
    *
-   * <p>Some options are expanded in-place, and need to be all parsed at the priority of the
-   * original option. In this case, parsing one of these after another should not cause the option
-   * to be considered as higher priority than the ones before it (this would cause overlap between
-   * the expansion of --expansion_flag and a option following it in the same list of options).
+   * @return the priority for the first child of the passed priority. This child's ordering can be
+   *     tracked the same way that the parent's was.
    */
-  public static OptionPriority getLockedPriority(OptionPriority priority) {
-    return new OptionPriority(priority.priorityCategory, priority.index, true);
+  public static OptionPriority getChildPriority(OptionPriority parentPriority)
+      throws OptionsParsingException {
+    if (parentPriority.alreadyExpanded) {
+      throw new OptionsParsingException("Tried to expand option too many times");
+    }
+    // Prevent this option from being re-expanded.
+    parentPriority.alreadyExpanded = true;
+
+    // The child priority has 1 more level of nesting than its parent.
+    return new OptionPriority(
+        parentPriority.priorityCategory,
+        ImmutableList.<Integer>builder().addAll(parentPriority.priorityIndices).add(0).build());
   }
 
   public PriorityCategory getPriorityCategory() {
@@ -71,28 +92,36 @@
   @Override
   public int compareTo(OptionPriority o) {
     if (priorityCategory.equals(o.priorityCategory)) {
-      return index - o.index;
+      for (int i = 0; i < priorityIndices.size() && i < o.priorityIndices.size(); ++i) {
+        if (!priorityIndices.get(i).equals(o.priorityIndices.get(i))) {
+          return priorityIndices.get(i).compareTo(o.priorityIndices.get(i));
+        }
+      }
+      // The values are up to the shorter one's length are the same, so the shorter one is a direct
+      // ancestor and comes first.
+      return Integer.compare(priorityIndices.size(), o.priorityIndices.size());
     }
-    return priorityCategory.ordinal() - o.priorityCategory.ordinal();
+    return Integer.compare(priorityCategory.ordinal(), o.priorityCategory.ordinal());
   }
 
   @Override
   public boolean equals(Object o) {
     if (o instanceof OptionPriority) {
       OptionPriority other = (OptionPriority) o;
-      return other.priorityCategory.equals(priorityCategory) && other.index == index;
+      return priorityCategory.equals(other.priorityCategory)
+          && priorityIndices.equals(other.priorityIndices);
     }
     return false;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(priorityCategory, index);
+    return Objects.hash(priorityCategory, priorityIndices);
   }
 
   @Override
   public String toString() {
-    return String.format("OptionPriority(%s,%s)", priorityCategory, index);
+    return String.format("OptionPriority(%s,%s)", priorityCategory, priorityIndices);
   }
 
   /**
diff --git a/java/com/google/devtools/common/options/OptionValueDescription.java b/java/com/google/devtools/common/options/OptionValueDescription.java
index 616b3b5..11ad7fe 100644
--- a/java/com/google/devtools/common/options/OptionValueDescription.java
+++ b/java/com/google/devtools/common/options/OptionValueDescription.java
@@ -22,7 +22,7 @@
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Map.Entry;
+import java.util.Map;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
@@ -98,8 +98,6 @@
       return new RepeatableOptionValueDescription(option);
     } else if (option.hasImplicitRequirements()) {
       return new OptionWithImplicitRequirementsValueDescription(option);
-    } else if (option.isWrapperOption()) {
-      return new WrapperOptionValueDescription(option);
     } else {
       return new SingleOptionValueDescription(option);
     }
@@ -187,11 +185,11 @@
       // log warnings describing the change.
       if (parsedOption.getPriority().compareTo(effectiveOptionInstance.getPriority()) >= 0) {
         // Identify the option that might have led to the current and new value of this option.
-        OptionDefinition implicitDependent = parsedOption.getImplicitDependent();
-        OptionDefinition expandedFrom = parsedOption.getExpandedFrom();
-        OptionDefinition optionThatDependsOnEffectiveValue =
+        ParsedOptionDescription implicitDependent = parsedOption.getImplicitDependent();
+        ParsedOptionDescription expandedFrom = parsedOption.getExpandedFrom();
+        ParsedOptionDescription optionThatDependsOnEffectiveValue =
             effectiveOptionInstance.getImplicitDependent();
-        OptionDefinition optionThatExpandedToEffectiveValue =
+        ParsedOptionDescription optionThatExpandedToEffectiveValue =
             effectiveOptionInstance.getExpandedFrom();
 
         Object newValue = parsedOption.getConvertedValue();
@@ -227,7 +225,7 @@
             // Create a warning if an expansion option overrides an explicit option:
             warnings.add(
                 String.format(
-                    "%s was expanded and now overrides a previous explicitly specified %s with %s",
+                    "%s was expanded and now overrides the explicit option %s with %s",
                     expandedFrom,
                     effectiveOptionInstance.getCommandLineForm(),
                     parsedOption.getCommandLineForm()));
@@ -278,8 +276,8 @@
           .asMap()
           .entrySet()
           .stream()
-          .sorted(Comparator.comparing(Entry::getKey))
-          .map(Entry::getValue)
+          .sorted(Comparator.comparing(Map.Entry::getKey))
+          .map(Map.Entry::getValue)
           .flatMap(Collection::stream)
           .map(ParsedOptionDescription::getSource)
           .distinct()
@@ -294,8 +292,8 @@
           .asMap()
           .entrySet()
           .stream()
-          .sorted(Comparator.comparing(Entry::getKey))
-          .map(Entry::getValue)
+          .sorted(Comparator.comparing(Map.Entry::getKey))
+          .map(Map.Entry::getValue)
           .flatMap(Collection::stream)
           .collect(Collectors.toList());
     }
@@ -322,8 +320,8 @@
           .asMap()
           .entrySet()
           .stream()
-          .sorted(Comparator.comparing(Entry::getKey))
-          .map(Entry::getValue)
+          .sorted(Comparator.comparing(Map.Entry::getKey))
+          .map(Map.Entry::getValue)
           .flatMap(Collection::stream)
           // Only provide the options that aren't implied elsewhere.
           .filter(optionDesc -> optionDesc.getImplicitDependent() == null)
@@ -430,50 +428,6 @@
                   optionDefinition, parsedOption.getSource()));
     }
   }
-
-  /** Form for options that contain other options in the value text to which they expand. */
-  private static final class WrapperOptionValueDescription extends OptionValueDescription {
-
-    WrapperOptionValueDescription(OptionDefinition optionDefinition) {
-      super(optionDefinition);
-    }
-
-    @Override
-    public Object getValue() {
-      return null;
-    }
-
-    @Override
-    public String getSourceString() {
-      return null;
-    }
-
-    @Override
-    ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
-        throws OptionsParsingException {
-      if (!parsedOption.getUnconvertedValue().startsWith("-")) {
-        throw new OptionsParsingException(
-            String.format(
-                "Invalid value format for %s. You may have meant --%s=--%s",
-                optionDefinition,
-                optionDefinition.getOptionName(),
-                parsedOption.getUnconvertedValue()));
-      }
-      return new ExpansionBundle(
-          ImmutableList.of(parsedOption.getUnconvertedValue()),
-          (parsedOption.getSource() == null)
-              ? String.format("unwrapped from %s", optionDefinition)
-              : String.format(
-                  "unwrapped from %s (source %s)", optionDefinition, parsedOption.getSource()));
-    }
-
-    @Override
-    public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
-      // No wrapper options get listed in the canonical form - the options they are wrapping will
-      // be in the right place.
-      return ImmutableList.of();
-    }
-  }
 }
 
 
diff --git a/java/com/google/devtools/common/options/OptionsBase.java b/java/com/google/devtools/common/options/OptionsBase.java
index 6b9f2f1..9496c65 100644
--- a/java/com/google/devtools/common/options/OptionsBase.java
+++ b/java/com/google/devtools/common/options/OptionsBase.java
@@ -20,7 +20,6 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * Base class for all options classes. Extend this class, adding public instance fields annotated
@@ -93,7 +92,7 @@
   public final String cacheKey() {
     StringBuilder result = new StringBuilder(getClass().getName()).append("{");
 
-    for (Entry<String, Object> entry : asMap().entrySet()) {
+    for (Map.Entry<String, Object> entry : asMap().entrySet()) {
       result.append(entry.getKey()).append("=");
 
       Object value = entry.getValue();
diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java
index fb7161c..6f1d7b6 100644
--- a/java/com/google/devtools/common/options/OptionsParser.java
+++ b/java/com/google/devtools/common/options/OptionsParser.java
@@ -33,7 +33,6 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -299,7 +298,7 @@
         getOptionsSortedByCategory();
     ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions =
         OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName);
-    for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
+    for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
         optionsByCategory.entrySet()) {
       String categoryDescription = optionCategoryDescriptions.get(e.getKey());
       List<OptionDefinition> categorizedOptionList = e.getValue();
@@ -463,7 +462,7 @@
     ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions =
         OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName);
 
-    for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
+    for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
         optionsByCategory.entrySet()) {
       desc.append("<dl>");
       String categoryDescription = optionCategoryDescriptions.get(e.getKey());
@@ -619,13 +618,26 @@
     }
   }
 
-  public void parseOptionsFixedAtSpecificPriority(
-      OptionPriority priority, String source, List<String> args) throws OptionsParsingException {
-    Preconditions.checkNotNull(priority, "Priority not specified for arglist " + args);
+  /**
+   * Parses the args at the priority of the provided option. This is useful for after-the-fact
+   * expansion.
+   *
+   * @param optionToExpand the option that is being "expanded" after the fact. The provided args
+   *     will have the same priority as this option.
+   * @param source a description of where the expansion arguments came from.
+   * @param args the arguments to parse as the expansion. Order matters, as the value of a flag may
+   *     be in the following argument.
+   */
+  public void parseArgsAsExpansionOfOption(
+      ParsedOptionDescription optionToExpand, String source, List<String> args)
+      throws OptionsParsingException {
+    Preconditions.checkNotNull(
+        optionToExpand, "Option for expansion not specified for arglist " + args);
     Preconditions.checkArgument(
-        priority.getPriorityCategory() != OptionPriority.PriorityCategory.DEFAULT,
+        optionToExpand.getPriority().getPriorityCategory()
+            != OptionPriority.PriorityCategory.DEFAULT,
         "Priority cannot be default, which was specified for arglist " + args);
-    residue.addAll(impl.parseOptionsFixedAtSpecificPriority(priority, o -> source, args));
+    residue.addAll(impl.parseArgsAsExpansionOfOption(optionToExpand, o -> source, args));
     if (!allowResidue && !residue.isEmpty()) {
       String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue);
       throw new OptionsParsingException(errorMsg);
diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java
index 496927b..2c15430 100644
--- a/java/com/google/devtools/common/options/OptionsParserImpl.java
+++ b/java/com/google/devtools/common/options/OptionsParserImpl.java
@@ -142,9 +142,10 @@
     return optionValues
         .keySet()
         .stream()
-        .sorted()
         .map(optionDefinition -> optionValues.get(optionDefinition).getCanonicalInstances())
         .flatMap(Collection::stream)
+        // Return the effective (canonical) options in the order they were applied.
+        .sorted(comparing(ParsedOptionDescription::getPriority))
         .collect(ImmutableList.toImmutableList());
   }
 
@@ -204,30 +205,33 @@
    * OptionInstanceOrigin)}
    */
   ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions(
-      OptionDefinition expansionFlag, OptionInstanceOrigin originOfExpansionFlag)
+      OptionDefinition expansionFlagDef, OptionInstanceOrigin originOfExpansionFlag)
       throws OptionsParsingException {
     ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder();
-    OptionInstanceOrigin originOfSubflags;
+
+    // Values needed to correctly track the origin of the expanded options.
+    OptionPriority nextOptionPriority =
+        OptionPriority.getChildPriority(originOfExpansionFlag.getPriority());
+    String source;
+    ParsedOptionDescription implicitDependent = null;
+    ParsedOptionDescription expandedFrom = null;
+
     ImmutableList<String> options;
-    if (expansionFlag.hasImplicitRequirements()) {
-      options = ImmutableList.copyOf(expansionFlag.getImplicitRequirements());
-      originOfSubflags =
-          new OptionInstanceOrigin(
-              originOfExpansionFlag.getPriority(),
-              String.format(
-                  "implicitly required by %s (source: %s)",
-                  expansionFlag, originOfExpansionFlag.getSource()),
-              expansionFlag,
-              null);
-    } else if (expansionFlag.isExpansionOption()) {
-      options = optionsData.getEvaluatedExpansion(expansionFlag);
-      originOfSubflags =
-          new OptionInstanceOrigin(
-              originOfExpansionFlag.getPriority(),
-              String.format(
-                  "expanded by %s (source: %s)", expansionFlag, originOfExpansionFlag.getSource()),
-              null,
-              expansionFlag);
+    ParsedOptionDescription expansionFlagParsedDummy =
+        ParsedOptionDescription.newDummyInstance(expansionFlagDef, originOfExpansionFlag);
+    if (expansionFlagDef.hasImplicitRequirements()) {
+      options = ImmutableList.copyOf(expansionFlagDef.getImplicitRequirements());
+      source =
+          String.format(
+              "implicitly required by %s (source: %s)",
+              expansionFlagDef, originOfExpansionFlag.getSource());
+      implicitDependent = expansionFlagParsedDummy;
+    } else if (expansionFlagDef.isExpansionOption()) {
+      options = optionsData.getEvaluatedExpansion(expansionFlagDef);
+      source =
+          String.format(
+              "expanded by %s (source: %s)", expansionFlagDef, originOfExpansionFlag.getSource());
+      expandedFrom = expansionFlagParsedDummy;
     } else {
       return ImmutableList.of();
     }
@@ -239,11 +243,12 @@
           identifyOptionAndPossibleArgument(
               unparsedFlagExpression,
               optionsIterator,
-              originOfSubflags.getPriority(),
-              o -> originOfSubflags.getSource(),
-              originOfSubflags.getImplicitDependent(),
-              originOfSubflags.getExpandedFrom());
+              nextOptionPriority,
+              o -> source,
+              implicitDependent,
+              expandedFrom);
       builder.add(parsedOption);
+      nextOptionPriority = OptionPriority.nextOptionPriority(nextOptionPriority);
     }
     return builder.build();
   }
@@ -284,12 +289,19 @@
     }
   }
 
-  /** Parses the args at the fixed priority. */
-  List<String> parseOptionsFixedAtSpecificPriority(
-      OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args)
+  /** Implements {@link OptionsParser#parseArgsAsExpansionOfOption} */
+  List<String> parseArgsAsExpansionOfOption(
+      ParsedOptionDescription optionToExpand,
+      Function<OptionDefinition, String> sourceFunction,
+      List<String> args)
       throws OptionsParsingException {
     ResidueAndPriority residueAndPriority =
-        parse(OptionPriority.getLockedPriority(priority), sourceFunction, null, null, args);
+        parse(
+            OptionPriority.getChildPriority(optionToExpand.getPriority()),
+            sourceFunction,
+            null,
+            optionToExpand,
+            args);
     return residueAndPriority.residue;
   }
 
@@ -304,8 +316,8 @@
   private ResidueAndPriority parse(
       OptionPriority priority,
       Function<OptionDefinition, String> sourceFunction,
-      OptionDefinition implicitDependent,
-      OptionDefinition expandedFrom,
+      ParsedOptionDescription implicitDependent,
+      ParsedOptionDescription expandedFrom,
       List<String> args)
       throws OptionsParsingException {
     List<String> unparsedArgs = new ArrayList<>();
@@ -369,7 +381,7 @@
         priorityCategory);
 
     handleNewParsedOption(
-        new ParsedOptionDescription(
+        ParsedOptionDescription.newParsedOptionDescription(
             option,
             String.format("--%s=%s", option.getOptionName(), unconvertedValue),
             unconvertedValue,
@@ -392,16 +404,15 @@
     @Nullable String unconvertedValue = parsedOption.getUnconvertedValue();
 
     // There are 3 types of flags that expand to other flag values. Expansion flags are the
-    // accepted way to do this, but two legacy features remain: implicit requirements and wrapper
-    // options. We rely on the OptionProcessor compile-time check's guarantee that no option sets
-    // multiple of these behaviors. (In Bazel, --config is another such flag, but that expansion
+    // accepted way to do this, but implicit requirements also do this. We rely on the
+    // OptionProcessor compile-time check's guarantee that no option sets
+    // both expansion behaviors. (In Bazel, --config is another such flag, but that expansion
     // is not controlled within the options parser, so we ignore it here)
 
     // As much as possible, we want the behaviors of these different types of flags to be
     // identical, as this minimizes the number of edge cases, but we do not yet track these values
-    // in the same way. Wrapper options are replaced by their value and implicit requirements are
-    // hidden from the reported lists of parsed options.
-    if (parsedOption.getImplicitDependent() == null && !optionDefinition.isWrapperOption()) {
+    // in the same way.
+    if (parsedOption.getImplicitDependent() == null) {
       // Log explicit options and expanded options in the order they are parsed (can be sorted
       // later). This information is needed to correctly canonicalize flags.
       parsedOptions.add(parsedOption);
@@ -410,19 +421,13 @@
     if (expansionBundle != null) {
       ResidueAndPriority residueAndPriority =
           parse(
-              OptionPriority.getLockedPriority(parsedOption.getPriority()),
+              OptionPriority.getChildPriority(parsedOption.getPriority()),
               o -> expansionBundle.sourceOfExpansionArgs,
-              optionDefinition.hasImplicitRequirements() ? optionDefinition : null,
-              optionDefinition.isExpansionOption() ? optionDefinition : null,
+              optionDefinition.hasImplicitRequirements() ? parsedOption : null,
+              optionDefinition.isExpansionOption() ? parsedOption : null,
               expansionBundle.expansionArgs);
       if (!residueAndPriority.residue.isEmpty()) {
-        if (optionDefinition.isWrapperOption()) {
-          throw new OptionsParsingException(
-              "Unparsed options remain after unwrapping "
-                  + unconvertedValue
-                  + ": "
-                  + Joiner.on(' ').join(residueAndPriority.residue));
-        } else {
+
           // Throw an assertion here, because this indicates an error in the definition of this
           // option's expansion or requirements, not with the input as provided by the user.
           throw new AssertionError(
@@ -430,7 +435,7 @@
                   + unconvertedValue
                   + ": "
                   + Joiner.on(' ').join(residueAndPriority.residue));
-        }
+
       }
     }
   }
@@ -440,8 +445,8 @@
       Iterator<String> nextArgs,
       OptionPriority priority,
       Function<OptionDefinition, String> sourceFunction,
-      OptionDefinition implicitDependent,
-      OptionDefinition expandedFrom)
+      ParsedOptionDescription implicitDependent,
+      ParsedOptionDescription expandedFrom)
       throws OptionsParsingException {
 
     // Store the way this option was parsed on the command line.
@@ -506,9 +511,8 @@
       // Special-case boolean to supply value based on presence of "no" prefix.
       if (optionDefinition.usesBooleanValueSyntax()) {
         unconvertedValue = booleanValue ? "1" : "0";
-      } else if (optionDefinition.getType().equals(Void.class)
-          && !optionDefinition.isWrapperOption()) {
-        // This is expected, Void type options have no args (unless they're wrapper options).
+      } else if (optionDefinition.getType().equals(Void.class)) {
+        // This is expected, Void type options have no args.
       } else if (nextArgs.hasNext()) {
         // "--flag value" form
         unconvertedValue = nextArgs.next();
@@ -518,7 +522,7 @@
       }
     }
 
-    return new ParsedOptionDescription(
+    return ParsedOptionDescription.newParsedOptionDescription(
         optionDefinition,
         commandLineForm.toString(),
         unconvertedValue,
diff --git a/java/com/google/devtools/common/options/OptionsProvider.java b/java/com/google/devtools/common/options/OptionsProvider.java
index d467fe5..ece5d5d 100644
--- a/java/com/google/devtools/common/options/OptionsProvider.java
+++ b/java/com/google/devtools/common/options/OptionsProvider.java
@@ -73,11 +73,13 @@
   List<OptionValueDescription> asListOfOptionValues();
 
   /**
-   * Canonicalizes the list of options that this OptionsParser has parsed. The
-   * contract is that if the returned set of options is passed to an options
-   * parser with the same options classes, then that will have the same effect
-   * as using the original args (which are passed in here), except for cosmetic
-   * differences.
+   * Canonicalizes the list of options that this OptionsParser has parsed.
+   *
+   * <p>The contract is that if the returned set of options is passed to an options parser with the
+   * same options classes, then that will have the same effect as using the original args (which are
+   * passed in here), except for cosmetic differences. We do not guarantee that the 'canonical' list
+   * is unique, since some flags may have effects unknown to the parser (--config, for Bazel), so we
+   * do not reorder flags to further simplify the list.
    */
   List<String> canonicalize();
 }
diff --git a/java/com/google/devtools/common/options/ParsedOptionDescription.java b/java/com/google/devtools/common/options/ParsedOptionDescription.java
index f55f8ad..5088153 100644
--- a/java/com/google/devtools/common/options/ParsedOptionDescription.java
+++ b/java/com/google/devtools/common/options/ParsedOptionDescription.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.common.options;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import java.util.function.Function;
 import javax.annotation.Nullable;
@@ -27,25 +28,49 @@
 public final class ParsedOptionDescription {
 
   private final OptionDefinition optionDefinition;
-  private final String commandLineForm;
+  @Nullable private final String commandLineForm;
   @Nullable private final String unconvertedValue;
   private final OptionInstanceOrigin origin;
 
-  public ParsedOptionDescription(
+  private ParsedOptionDescription(
+      OptionDefinition optionDefinition,
+      @Nullable String commandLineForm,
+      @Nullable String unconvertedValue,
+      OptionInstanceOrigin origin) {
+    this.optionDefinition = Preconditions.checkNotNull(optionDefinition);
+    this.commandLineForm = commandLineForm;
+    this.unconvertedValue = unconvertedValue;
+    this.origin = Preconditions.checkNotNull(origin);
+  }
+
+  static ParsedOptionDescription newParsedOptionDescription(
       OptionDefinition optionDefinition,
       String commandLineForm,
       @Nullable String unconvertedValue,
       OptionInstanceOrigin origin) {
-    this.optionDefinition = optionDefinition;
-    this.commandLineForm = commandLineForm;
-    this.unconvertedValue = unconvertedValue;
-    this.origin = origin;
+    // An actual ParsedOptionDescription should always have a form in which it was parsed, but some
+    // options, such as expansion options, legitimately have no value.
+    return new ParsedOptionDescription(
+        optionDefinition,
+        Preconditions.checkNotNull(commandLineForm),
+        unconvertedValue,
+        origin);
+  }
+
+  /**
+   * This factory should be used when there is no actual parsed option, since in those cases we do
+   * not have an original value or form that the option took.
+   */
+  static ParsedOptionDescription newDummyInstance(
+      OptionDefinition optionDefinition, OptionInstanceOrigin origin) {
+    return new ParsedOptionDescription(optionDefinition, null, null, origin);
   }
 
   public OptionDefinition getOptionDefinition() {
     return optionDefinition;
   }
 
+  @Nullable
   public String getCommandLineForm() {
     return commandLineForm;
   }
@@ -127,11 +152,11 @@
     return origin.getSource();
   }
 
-  OptionDefinition getImplicitDependent() {
+  ParsedOptionDescription getImplicitDependent() {
     return origin.getImplicitDependent();
   }
 
-  OptionDefinition getExpandedFrom() {
+  ParsedOptionDescription getExpandedFrom() {
     return origin.getExpandedFrom();
   }
 
@@ -152,14 +177,14 @@
 
   @Override
   public String toString() {
-    StringBuilder result = new StringBuilder();
-    result.append(optionDefinition);
-    result.append("set to '").append(unconvertedValue).append("' ");
-    result.append("with priority ").append(origin.getPriority());
-    if (origin.getSource() != null) {
-      result.append(" and source '").append(origin.getSource()).append("'");
+    // Check that a dummy value-less option instance does not output all the default information.
+    if (commandLineForm == null) {
+      return optionDefinition.toString();
     }
-    return result.toString();
+    String source = origin.getSource();
+    return String.format(
+        "option '%s'%s",
+        commandLineForm, source == null ? "" : String.format(" (source %s)", source));
   }
 
 }
diff --git a/java/com/google/devtools/common/options/processor/OptionProcessor.java b/java/com/google/devtools/common/options/processor/OptionProcessor.java
index fd7c023..485efcd 100644
--- a/java/com/google/devtools/common/options/processor/OptionProcessor.java
+++ b/java/com/google/devtools/common/options/processor/OptionProcessor.java
@@ -15,7 +15,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMap.Builder;
 import com.google.devtools.common.options.Converter;
 import com.google.devtools.common.options.Converters;
 import com.google.devtools.common.options.ExpansionFunction;
@@ -27,7 +26,7 @@
 import com.google.devtools.common.options.OptionsParser;
 import com.google.devtools.common.options.OptionsParsingException;
 import java.util.List;
-import java.util.Map.Entry;
+import java.util.Map;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -36,7 +35,6 @@
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.annotation.processing.RoundEnvironment;
 import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedSourceVersion;
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.AnnotationMirror;
 import javax.lang.model.element.Element;
@@ -74,7 +72,6 @@
  * <p>These properties can be relied upon at runtime without additional checks.
  */
 @SupportedAnnotationTypes({"com.google.devtools.common.options.Option"})
-@SupportedSourceVersion(SourceVersion.RELEASE_8)
 public final class OptionProcessor extends AbstractProcessor {
 
   private Types typeUtils;
@@ -84,6 +81,11 @@
   private ImmutableMap<Class<?>, PrimitiveType> primitiveTypeMap;
 
   @Override
+  public SourceVersion getSupportedSourceVersion() {
+    return SourceVersion.latestSupported();
+  }
+
+  @Override
   public synchronized void init(ProcessingEnvironment processingEnv) {
     super.init(processingEnv);
     typeUtils = processingEnv.getTypeUtils();
@@ -93,18 +95,19 @@
     // Because of the discrepancies between the java.lang and javax.lang type models, we can't
     // directly use the get() method for the default converter map. Instead, we'll convert it once,
     // to be more usable, and with the boxed type return values of convert() as the keys.
-    ImmutableMap.Builder<TypeMirror, Converter<?>> converterMapBuilder = new Builder<>();
+    ImmutableMap.Builder<TypeMirror, Converter<?>> converterMapBuilder =
+        new ImmutableMap.Builder<>();
 
     // Create a link from the primitive Classes to their primitive types. This intentionally
     // only contains the types in the DEFAULT_CONVERTERS map.
-    ImmutableMap.Builder<Class<?>, PrimitiveType> builder = new Builder<>();
+    ImmutableMap.Builder<Class<?>, PrimitiveType> builder = new ImmutableMap.Builder<>();
     builder.put(int.class, typeUtils.getPrimitiveType(TypeKind.INT));
     builder.put(double.class, typeUtils.getPrimitiveType(TypeKind.DOUBLE));
     builder.put(boolean.class, typeUtils.getPrimitiveType(TypeKind.BOOLEAN));
     builder.put(long.class, typeUtils.getPrimitiveType(TypeKind.LONG));
     primitiveTypeMap = builder.build();
 
-    for (Entry<Class<?>, Converter<?>> entry : Converters.DEFAULT_CONVERTERS.entrySet()) {
+    for (Map.Entry<Class<?>, Converter<?>> entry : Converters.DEFAULT_CONVERTERS.entrySet()) {
       Class<?> converterClass = entry.getKey();
       String typeName = converterClass.getCanonicalName();
       TypeElement typeElement = elementUtils.getTypeElement(typeName);
@@ -476,10 +479,6 @@
     }
 
     if (isExpansion || hasImplicitRequirements) {
-      if (annotation.wrapperOption()) {
-        throw new OptionProcessorException(
-            optionField, "Wrapper options cannot have expansions or implicit requirements.");
-      }
       if (annotation.allowMultiple()) {
         throw new OptionProcessorException(
             optionField,
@@ -488,30 +487,6 @@
     }
   }
 
-  /**
-   * Some flags wrap other flags. They are objectively useless, as there is no difference between
-   * passing --wrapper=--foo and --foo other than the "source" information tracked. This
-   * functionality comes from requiring compatibility at some past point in time, but is actively
-   * being deprecated. No non-deprecated flag can use this feature.
-   */
-  private void checkWrapperOptions(VariableElement optionField) throws OptionProcessorException {
-    Option annotation = optionField.getAnnotation(Option.class);
-    if (annotation.wrapperOption()) {
-      if (annotation.deprecationWarning().isEmpty()) {
-        throw new OptionProcessorException(
-            optionField,
-            "Can't have non deprecated wrapper options, this feature is deprecated. "
-                + "Please add a deprecationWarning.");
-      }
-      if (!ImmutableList.copyOf(annotation.metadataTags()).contains(OptionMetadataTag.DEPRECATED)) {
-        throw new OptionProcessorException(
-            optionField,
-            "Can't have non deprecated wrapper options, this feature is deprecated. "
-                + "Please add the metadata tag DEPRECATED.");
-      }
-    }
-  }
-
   @Override
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
     for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Option.class)) {
@@ -528,7 +503,6 @@
         checkConverter(optionField);
         checkEffectTagRationality(optionField);
         checkMetadataTagAndCategoryRationality(optionField);
-        checkWrapperOptions(optionField);
       } catch (OptionProcessorException e) {
         error(e.getElementInError(), e.getMessage());
       }
diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
new file mode 100644
index 0000000..9b43207
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
@@ -0,0 +1,455 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(JUnit4.class)
+public class CoreLibrarySupportTest {
+
+  @Test
+  public void testIsRenamedCoreLibrary() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            null,
+            ImmutableList.of("java/time/"),
+            ImmutableList.of(),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("java/io/X")).isFalse();
+    assertThat(support.isRenamedCoreLibrary("java/io/X$$CC")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("java/io/X$$Lambda$17")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse();
+  }
+
+  @Test
+  public void testIsRenamedCoreLibrary_prefixedLoader() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter("__/"),
+            null,
+            ImmutableList.of("java/time/"),
+            ImmutableList.of(),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("__/java/io/X")).isFalse();
+    assertThat(support.isRenamedCoreLibrary("__/java/io/X$$CC")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("__/java/io/X$$Lambda$17")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse();
+  }
+
+  @Test
+  public void testRenameCoreLibrary() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            null,
+            ImmutableList.of(),
+            ImmutableList.of(),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(support.renameCoreLibrary("java/time/X")).isEqualTo("j$/time/X");
+    assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X");
+  }
+
+  @Test
+  public void testRenameCoreLibrary_prefixedLoader() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter("__/"),
+            null,
+            ImmutableList.of(),
+            ImmutableList.of(),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(support.renameCoreLibrary("__/java/time/X")).isEqualTo("j$/time/X");
+    assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X");
+  }
+
+  @Test
+  public void testMoveTarget() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter("__/"),
+            null,
+            ImmutableList.of("java/util/Helper"),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Existing#match -> java/util/Helper"),
+            ImmutableList.of());
+    assertThat(support.getMoveTarget("__/java/util/Existing", "match")).isEqualTo("j$/util/Helper");
+    assertThat(support.getMoveTarget("java/util/Existing", "match")).isEqualTo("j$/util/Helper");
+    assertThat(support.getMoveTarget("__/java/util/Existing", "matchesnot")).isNull();
+    assertThat(support.getMoveTarget("__/java/util/ExistingOther", "match")).isNull();
+  }
+
+  @Test
+  public void testIsEmulatedCoreClassOrInterface() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of("java/util/concurrent/"),
+            ImmutableList.of("java/util/Map"),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map")).isTrue();
+    assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$Lambda$17")).isFalse();
+    assertThat(support.isEmulatedCoreClassOrInterface("java/util/Map$$CC")).isFalse();
+    assertThat(support.isEmulatedCoreClassOrInterface("java/util/HashMap")).isTrue();
+    assertThat(support.isEmulatedCoreClassOrInterface("java/util/concurrent/ConcurrentMap"))
+        .isFalse(); // false for renamed prefixes
+    assertThat(support.isEmulatedCoreClassOrInterface("com/google/Map")).isFalse();
+  }
+
+  @Test
+  public void testGetCoreInterfaceRewritingTarget_emulatedDefaultMethod() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Collection"),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Collection",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                true))
+        .isEqualTo(Collection.class);
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEVIRTUAL,
+                "java/util/ArrayList",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                false))
+        .isEqualTo(Collection.class);
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "com/google/HypotheticalListInterface",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                true))
+        .isNull();
+  }
+
+  @Test
+  public void testGetCoreInterfaceRewritingTarget_emulatedImplementationMoved() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of("java/util/Moved"),
+            ImmutableList.of("java/util/Map"),
+            ImmutableList.of("java/util/LinkedHashMap#forEach->java/util/Moved"),
+            ImmutableList.of());
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Map",
+                "forEach",
+                "(Ljava/util/function/BiConsumer;)V",
+                true))
+        .isEqualTo(Map.class);
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKESPECIAL,
+                "java/util/Map",
+                "forEach",
+                "(Ljava/util/function/BiConsumer;)V",
+                true))
+        .isEqualTo(Map.class);
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEVIRTUAL,
+                "java/util/LinkedHashMap",
+                "forEach",
+                "(Ljava/util/function/BiConsumer;)V",
+                false))
+        .isEqualTo(Map.class);
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKESPECIAL,
+                "java/util/LinkedHashMap",
+                "forEach",
+                "(Ljava/util/function/BiConsumer;)V",
+                false))
+        .isEqualTo(LinkedHashMap.class);
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKESPECIAL,
+                "java/util/HashMap",
+                "forEach",
+                "(Ljava/util/function/BiConsumer;)V",
+                false))
+        .isEqualTo(Map.class);
+  }
+
+  @Test
+  public void testGetCoreInterfaceRewritingTarget_abstractMethod() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Collection"),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Collection",
+                "size",
+                "()I",
+                true))
+        .isNull();
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEVIRTUAL,
+                "java/util/ArrayList",
+                "size",
+                "()I",
+                false))
+        .isNull();
+  }
+
+  @Test
+  public void testGetCoreInterfaceRewritingTarget_emulatedDefaultOverride() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Map"),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Map",
+                "putIfAbsent",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isEqualTo(Map.class);
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/concurrent/ConcurrentMap",
+                "putIfAbsent",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isNull(); // putIfAbsent is default in Map but abstract in ConcurrentMap
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/concurrent/ConcurrentMap",
+                "getOrDefault",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isEqualTo(ConcurrentMap.class); // conversely, getOrDefault is overridden as default method
+  }
+
+  @Test
+  public void testGetCoreInterfaceRewritingTarget_staticInterfaceMethod() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Comparator"),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKESTATIC,
+                "java/util/Comparator",
+                "reverseOrder",
+                "()Ljava/util/Comparator;",
+                true))
+        .isEqualTo(Comparator.class);
+  }
+
+  /**
+   * Tests that call sites of renamed core libraries are treated like call sites in regular
+   * {@link InterfaceDesugaring}.
+   */
+  @Test
+  public void testGetCoreInterfaceRewritingTarget_renamed() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of("java/util/"),
+            ImmutableList.of(),
+            ImmutableList.of(),
+            ImmutableList.of());
+
+    // regular invocations of default methods: ignored
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Collection",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                true))
+        .isNull();
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEVIRTUAL,
+                "java/util/ArrayList",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                false))
+        .isNull();
+
+    // abstract methods: ignored
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Collection",
+                "size",
+                "()I",
+                true))
+        .isNull();
+
+    // static interface method
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKESTATIC,
+                "java/util/Comparator",
+                "reverseOrder",
+                "()Ljava/util/Comparator;",
+                true))
+        .isEqualTo(Comparator.class);
+
+    // invokespecial for default methods: find nearest definition
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKESPECIAL,
+                "java/util/List",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                true))
+        .isEqualTo(Collection.class);
+    // invokespecial on a class: ignore (even if there's an inherited default method)
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKESPECIAL,
+                "java/util/ArrayList",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                false))
+        .isNull();
+  }
+
+  @Test
+  public void testGetCoreInterfaceRewritingTarget_ignoreRenamedInvokeInterface() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of("java/util/concurrent/"),  // should return null for these
+            ImmutableList.of("java/util/Map"),
+            ImmutableList.of(),
+            ImmutableList.of());
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Map",
+                "getOrDefault",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isEqualTo(Map.class);
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/concurrent/ConcurrentMap",
+                "getOrDefault",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isNull();
+  }
+
+  @Test
+  public void testGetCoreInterfaceRewritingTarget_excludedMethodIgnored() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Collection"),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Collection#removeIf"));
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/List",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                true))
+        .isNull();
+    assertThat(
+            support.getCoreInterfaceRewritingTarget(
+                Opcodes.INVOKEVIRTUAL,
+                "java/util/ArrayList",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                false))
+        .isNull();
+  }
+
+  @Test
+  public void testEmulatedMethod_nullExceptions() throws Exception {
+    CoreLibrarySupport.EmulatedMethod m =
+        CoreLibrarySupport.EmulatedMethod.create(1, Number.class, "a", "()V", null);
+    assertThat(m.access()).isEqualTo(1);
+    assertThat(m.owner()).isEqualTo(Number.class);
+    assertThat(m.name()).isEqualTo("a");
+    assertThat(m.descriptor()).isEqualTo("()V");
+    assertThat(m.exceptions()).isEmpty();
+  }
+
+  @Test
+  public void testEmulatedMethod_givenExceptions() throws Exception {
+    CoreLibrarySupport.EmulatedMethod m =
+        CoreLibrarySupport.EmulatedMethod.create(
+            1, Number.class, "a", "()V", new String[] {"b", "c"});
+    assertThat(m.access()).isEqualTo(1);
+    assertThat(m.owner()).isEqualTo(Number.class);
+    assertThat(m.name()).isEqualTo("a");
+    assertThat(m.descriptor()).isEqualTo("()V");
+    assertThat(m.exceptions()).containsExactly("b", "c").inOrder();
+  }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
new file mode 100644
index 0000000..5220ed6
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
@@ -0,0 +1,104 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(JUnit4.class)
+public class CorePackageRenamerTest {
+
+  @Test
+  public void testSymbolRewrite() throws Exception {
+    MockClassVisitor out = new MockClassVisitor();
+    CorePackageRenamer renamer =
+        new CorePackageRenamer(
+            out,
+            new CoreLibrarySupport(
+                new CoreLibraryRewriter(""),
+                null,
+                ImmutableList.of("java/time/"),
+                ImmutableList.of(),
+                ImmutableList.of("java/util/A#m->java/time/B"),
+                ImmutableList.of()));
+    MethodVisitor mv = renamer.visitMethod(0, "test", "()V", null, null);
+
+    mv.visitMethodInsn(
+        Opcodes.INVOKESTATIC, "java/time/Instant", "now", "()Ljava/time/Instant;", false);
+    assertThat(out.mv.owner).isEqualTo("j$/time/Instant");
+    assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;");
+
+    // Ignore moved methods but not their descriptors
+    mv.visitMethodInsn(
+        Opcodes.INVOKESTATIC, "java/util/A", "m", "()Ljava/time/Instant;", false);
+    assertThat(out.mv.owner).isEqualTo("java/util/A");
+    assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;");
+
+    // Ignore arbitrary other methods but not their descriptors
+    mv.visitMethodInsn(
+        Opcodes.INVOKESTATIC, "other/time/Instant", "now", "()Ljava/time/Instant;", false);
+    assertThat(out.mv.owner).isEqualTo("other/time/Instant");
+    assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;");
+
+    mv.visitFieldInsn(
+        Opcodes.GETFIELD, "other/time/Instant", "now", "Ljava/time/Instant;");
+    assertThat(out.mv.owner).isEqualTo("other/time/Instant");
+    assertThat(out.mv.desc).isEqualTo("Lj$/time/Instant;");
+  }
+
+  private static class MockClassVisitor extends ClassVisitor {
+
+    final MockMethodVisitor mv = new MockMethodVisitor();
+
+    public MockClassVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public MethodVisitor visitMethod(
+        int access, String name, String desc, String signature, String[] exceptions) {
+      return mv;
+    }
+  }
+
+  private static class MockMethodVisitor extends MethodVisitor {
+
+    String owner;
+    String desc;
+
+    public MockMethodVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      this.owner = owner;
+      this.desc = desc;
+    }
+
+    @Override
+    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+      this.owner = owner;
+      this.desc = desc;
+    }
+  }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
index c74febb..406a36f 100644
--- a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
@@ -15,11 +15,14 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer.InterfaceComparator.INSTANCE;
+import static com.google.devtools.build.android.desugar.DefaultMethodClassFixer.SubtypeComparator.INSTANCE;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.Closer;
-import com.google.devtools.build.android.desugar.Desugar.ThrowingClassLoader;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.devtools.build.android.desugar.io.HeaderClassLoader;
+import com.google.devtools.build.android.desugar.io.IndexedInputs;
+import com.google.devtools.build.android.desugar.io.ThrowingClassLoader;
 import java.io.File;
 import java.io.IOException;
 import java.io.Serializable;
@@ -102,6 +105,7 @@
             writer,
             classpathReader,
             DependencyCollector.NoWriteCollectors.FAIL_ON_MISSING,
+            /*coreLibrarySupport=*/ null,
             bootclassPath,
             classLoader);
     reader.accept(fixer, 0);
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
index 20e6028..75d4f43 100644
--- a/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
@@ -32,6 +32,7 @@
 import com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod;
 import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithDefaultMethod;
 import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithDuplicateMethods.ClassWithDuplicateMethods;
+import com.google.devtools.build.android.desugar.testdata.java8.InterfaceWithInheritedMethods;
 import com.google.devtools.build.android.desugar.testdata.java8.Java7InterfaceWithBridges;
 import com.google.devtools.build.android.desugar.testdata.java8.Named;
 import com.google.devtools.build.android.desugar.testdata.java8.TwoInheritedDefaultMethods;
@@ -415,4 +416,11 @@
     assertThat(new DefaultMethodTransitivelyFromSeparateJava8Target().dflt()).isEqualTo("dflt");
     assertThat(new DefaultMethodFromSeparateJava8TargetOverridden().dflt()).isEqualTo("override");
   }
+
+  /** Regression test for b/73355452 */
+  @Test
+  public void testSuperCallToInheritedDefaultMethod() {
+    assertThat(new InterfaceWithInheritedMethods.Impl().name()).isEqualTo("Base");
+    assertThat(new InterfaceWithInheritedMethods.Impl().suffix()).isEqualTo("!");
+  }
 }
diff --git a/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java b/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java
index 38df9e3..2d567d3 100644
--- a/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarTryWithResourcesFunctionalTest.java
@@ -96,4 +96,34 @@
       }
     }
   }
+
+  @Test
+  public void testInheritanceTryWithResources() {
+
+    try {
+      ClassUsingTryWithResources.inheritanceTryWithResources();
+      fail("Expected RuntimeException");
+    } catch (Exception expected) {
+      assertThat(expected.getClass()).isEqualTo(RuntimeException.class);
+
+      String expectedStrategyName = getTwrStrategyClassNameSpecifiedInSystemProperty();
+      assertThat(getStrategyClassName()).isEqualTo(expectedStrategyName);
+      if (isMimicStrategy()) {
+        assertThat(expected.getSuppressed()).isEmpty();
+        assertThat(ThrowableExtension.getSuppressed(expected)).hasLength(1);
+        assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass())
+            .isEqualTo(IOException.class);
+      } else if (isReuseStrategy()) {
+        assertThat(expected.getSuppressed()).hasLength(1);
+        assertThat(expected.getSuppressed()[0].getClass()).isEqualTo(IOException.class);
+        assertThat(ThrowableExtension.getSuppressed(expected)[0].getClass())
+            .isEqualTo(IOException.class);
+      } else if (isNullStrategy()) {
+        assertThat(expected.getSuppressed()).isEmpty();
+        assertThat(ThrowableExtension.getSuppressed(expected)).isEmpty();
+      } else {
+        fail("unexpected desugaring strategy " + getStrategyClassName());
+      }
+    }
+  }
 }
diff --git a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
index 2eab943..99e51c1 100644
--- a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
@@ -16,6 +16,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.fail;
 
+import com.google.devtools.build.android.desugar.io.BitFlags;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
diff --git a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java
index 37afae7..dc0da22 100644
--- a/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriterTest.java
@@ -25,6 +25,7 @@
 import static org.objectweb.asm.Opcodes.INVOKESTATIC;
 import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
 
+import com.google.devtools.build.android.desugar.io.BitFlags;
 import com.google.devtools.build.android.desugar.runtime.ThrowableExtension;
 import com.google.devtools.build.android.desugar.testdata.ClassUsingTryWithResources;
 import java.io.IOException;
diff --git a/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar b/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar
new file mode 100644
index 0000000..6cca3a0
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/b72690624_testdata.jar
Binary files differ
diff --git a/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java b/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java
index e99abc4..64dd871 100644
--- a/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java
@@ -16,11 +16,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.android.desugar.proto.DesugarDeps;
 import com.google.devtools.build.android.desugar.proto.DesugarDeps.Dependency;
 import com.google.devtools.build.android.desugar.proto.DesugarDeps.DesugarDepsInfo;
 import com.google.devtools.build.android.desugar.proto.DesugarDeps.InterfaceDetails;
 import com.google.devtools.build.android.desugar.proto.DesugarDeps.InterfaceWithCompanion;
-import com.google.devtools.build.android.desugar.proto.DesugarDeps.Type;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -103,8 +103,8 @@
                 .build());
   }
 
-  private static Type wrapType(String name) {
-    return Type.newBuilder().setBinaryName(name).build();
+  private static DesugarDeps.Type wrapType(String name) {
+    return DesugarDeps.Type.newBuilder().setBinaryName(name).build();
   }
 
   private DesugarDepsInfo extractProto(MetadataCollector collector) throws Exception {
diff --git a/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt b/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt
new file mode 100644
index 0000000..4be60b6
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/desugar_unused_synthetic_close_resource_method_golden.txt
@@ -0,0 +1,8 @@
+Compiled from "UnusedSyntheticCloseResourceMethod.java"
+public class com.google.devtools.build.android.desugar.testdata.UnusedSyntheticCloseResourceMethod {
+  public com.google.devtools.build.android.desugar.testdata.UnusedSyntheticCloseResourceMethod();
+    Code:
+       0: aload_0
+       1: invokespecial #9                  // Method java/lang/Object."<init>":()V
+       4: return
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java b/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java
similarity index 95%
rename from test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java
rename to test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java
index afb2bea..0579822 100644
--- a/test/java/com/google/devtools/build/android/desugar/FieldInfoTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/io/FieldInfoTest.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java b/test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java
similarity index 98%
rename from test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java
rename to test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java
index bac3fc9..81a4b31 100644
--- a/test/java/com/google/devtools/build/android/desugar/IndexedInputsTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/io/IndexedInputsTest.java
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package com.google.devtools.build.android.desugar;
+package com.google.devtools.build.android.desugar.io;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java
index b65b8bd..489bd7a 100644
--- a/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java
+++ b/test/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtensionTestUtility.java
@@ -27,7 +27,7 @@
   private static final String SYSTEM_PROPERTY_EXPECTED_STRATEGY = "expected.strategy";
 
   public static String getTwrStrategyClassNameSpecifiedInSystemProperty() {
-    String className = System.getProperty(SYSTEM_PROPERTY_EXPECTED_STRATEGY);
+    String className = unquote(System.getProperty(SYSTEM_PROPERTY_EXPECTED_STRATEGY));
     assertThat(className).isNotEmpty();
     return className;
   }
@@ -61,4 +61,13 @@
   public static boolean isReuseStrategy() {
     return isStrategyOfClass(THROWABLE_EXTENSION_CLASS_NAME + "$ReuseDesugaringStrategy");
   }
+
+  private static String unquote(String s) {
+    if (s.startsWith("'") || s.startsWith("\"")) {
+      assertThat(s).endsWith(s.substring(0, 1));
+      return s.substring(1, s.length() - 1);
+    } else {
+      return s;
+    }
+  }
 }
diff --git a/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh b/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh
new file mode 100755
index 0000000..d42859f
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/scan/test_keep_scanner.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+set -eux
+
+out=$(mktemp)
+"$1" --input "$2" --keep_file "${out}" --prefix java/
+
+if ! diff "$3" "${out}"; then
+  echo "Unexpected output"
+  cat "${out}"
+  rm "${out}"
+  exit 1
+fi
+rm "${out}"
diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java
new file mode 100644
index 0000000..830364c
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata/CollectionReferences.java
@@ -0,0 +1,64 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan.testdata;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+/** Test data for {@code KeepScanner} with references to java.* */
+public class CollectionReferences {
+
+  private final List<Date> dates;
+
+  public CollectionReferences() {
+    dates = new ArrayList<>(7);
+    assert !(dates instanceof LinkedList);
+  }
+
+  @SuppressWarnings("unchecked")
+  public void add(Date date) {
+    List<Date> l = (AbstractList<Date>) Collection.class.cast(dates);
+    l.add(date);
+  }
+
+  public Date first() {
+    try {
+      return dates.get(0);
+    } catch (IndexOutOfBoundsException e) {
+      return null;
+    }
+  }
+
+  public long min() {
+    long result = Long.MAX_VALUE; // compile-time constant, no ref
+    for (Date d : dates) {
+      if (d.getTime() < result) {
+        result = d.getTime();
+      }
+    }
+    return result;
+  }
+
+  public void expire(long before) {
+    dates.removeIf(d -> d.getTime() < before);
+  }
+
+  static {
+    System.out.println("Hello!");
+  }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java b/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java
new file mode 100644
index 0000000..e743a0d
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata/OverlappingCollectionReferences.java
@@ -0,0 +1,49 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.scan.testdata;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+/** Supplements {@link CollectionReferences} with additional and overlapping references to java.* */
+public class OverlappingCollectionReferences {
+
+  private final ArrayList<Date> dates;
+
+  public OverlappingCollectionReferences() {
+    dates = new ArrayList<>();
+  }
+
+  public void add(Date date) {
+    dates.add(date);
+  }
+
+  public Date first() {
+    try {
+      return dates.get(0);
+    } catch (IndexOutOfBoundsException e) {
+      return null;
+    }
+  }
+
+  public Date max() {
+    long result = Long.MIN_VALUE; // compile-time constant, no ref
+    for (Date d : dates) {
+      if (d.getTime() > result) {
+        result = d.getTime();
+      }
+    }
+    return new Date(result);
+  }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt
new file mode 100644
index 0000000..6082576
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/scan/testdata_golden.txt
@@ -0,0 +1,62 @@
+-keep class java.io.PrintStream {
+  *** println(java.lang.String);
+}
+-keep class java.lang.AssertionError {
+  <init>();
+}
+-keep class java.lang.Class {
+  *** cast(java.lang.Object);
+  *** desiredAssertionStatus();
+}
+-keep class java.lang.IndexOutOfBoundsException {
+}
+-keep class java.lang.Object {
+  <init>();
+}
+-keep class java.lang.String {
+}
+-keep class java.lang.System {
+  *** out;
+}
+-keep class java.lang.invoke.CallSite {
+}
+-keep class java.lang.invoke.LambdaMetafactory {
+  *** metafactory(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType, java.lang.invoke.MethodType, java.lang.invoke.MethodHandle, java.lang.invoke.MethodType);
+}
+-keep class java.lang.invoke.MethodHandle {
+}
+-keep class java.lang.invoke.MethodHandles {
+}
+-keep class java.lang.invoke.MethodHandles$Lookup {
+}
+-keep class java.lang.invoke.MethodType {
+}
+-keep class java.util.AbstractList {
+}
+-keep class java.util.ArrayList {
+  <init>();
+  <init>(int);
+  *** add(java.lang.Object);
+  *** get(int);
+  *** iterator();
+}
+-keep class java.util.Collection {
+  *** removeIf(java.util.function.Predicate);
+}
+-keep class java.util.Date {
+  <init>(long);
+  *** getTime();
+}
+-keep class java.util.Iterator {
+  *** hasNext();
+  *** next();
+}
+-keep class java.util.LinkedList {
+}
+-keep class java.util.List {
+  *** add(java.lang.Object);
+  *** get(int);
+  *** iterator();
+}
+-keep class java.util.function.Predicate {
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java b/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java
index c340c84..e4f7f18 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.java
@@ -49,6 +49,9 @@
     }
   }
 
+  /** A resource inheriting the close() method from its parent. */
+  public static class InheritanceResource extends SimpleResource {}
+
   /** This method will always throw {@link java.lang.Exception}. */
   public static void simpleTryWithResources() throws Exception {
     // Throwable.addSuppressed(Throwable) should be called in the following block.
@@ -57,6 +60,17 @@
     }
   }
 
+  /**
+   * This method useds {@link InheritanceResource}, which inherits all methods from {@link
+   * SimpleResource}.
+   */
+  public static void inheritanceTryWithResources() throws Exception {
+    // Throwable.addSuppressed(Throwable) should be called in the following block.
+    try (InheritanceResource resource = new InheritanceResource()) {
+      resource.call(true);
+    }
+  }
+
   public static Throwable[] checkSuppressedExceptions(boolean throwException) {
     // Throwable.addSuppressed(Throwable) should be called in the following block.
     try (SimpleResource resource = new SimpleResource()) {
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java
new file mode 100644
index 0000000..8656e26
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.java
@@ -0,0 +1,44 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar.testdata.java8;
+
+/** Regression test data for b/73355452 that also includes calling static methods. */
+public interface InterfaceWithInheritedMethods {
+  default String name() {
+    return "Base";
+  }
+
+  static String staticSuffix() {
+    return "!";
+  }
+  
+  static interface Passthrough extends InterfaceWithInheritedMethods {
+    // inherits name().  Note that desugar doesn't produce a companion class for this interface
+    // since it doesn't define any default or static interface methods itself.
+  }
+
+  static class Impl implements Passthrough {
+    @Override
+    public String name() {
+      // Even though Passthrough itself doesn't define name(), bytecode refers to Passthrough.name.
+      return Passthrough.super.name();
+    }
+
+    public String suffix() {
+      // Note that Passthrough.defaultSuffix doesn't compile and bytecode refers to
+      // InterfaceWithInheritedMethods.staticSuffix, so this shouldn't cause issues like b/73355452
+      return staticSuffix();
+    }
+  }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt
index b7c3c25..8e396f5 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_disabling_twr_with_large_minsdkversion_jar_toc_golden.txt
@@ -12,6 +12,7 @@
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
 com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt
index d03b121..41e8d1c 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_for_try_with_resources_jar_toc_golden.txt
@@ -12,6 +12,7 @@
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
 com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt
index 91fc415..b79961e 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_jar_toc_golden.txt
@@ -12,6 +12,7 @@
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
 com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt
index 907edd0..16439ae 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_java8_jar_toc_golden.txt
@@ -12,6 +12,7 @@
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
 com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
@@ -81,6 +82,9 @@
 com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDefaultMethod.class
 com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods$ClassWithDuplicateMethods.class
 com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithDuplicateMethods.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods$Impl.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods$Passthrough.class
+com/google/devtools/build/android/desugar/testdata/java8/InterfaceWithInheritedMethods.class
 com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$AbstractClassOne.class
 com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$ClassAddOne.class
 com/google/devtools/build/android/desugar/testdata/java8/Java7InterfaceWithBridges$ClassAddTwo.class
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt
index 256760b..4fdc023 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/testdata_desugared_without_lambda_desugared_jar_toc_golden.txt
@@ -11,6 +11,7 @@
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare$LongCmpFunc.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingLongCompare.class
 com/google/devtools/build/android/desugar/testdata/ClassCallingRequireNonNull.class
+com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$InheritanceResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources$SimpleResource.class
 com/google/devtools/build/android/desugar/testdata/ClassUsingTryWithResources.class
 com/google/devtools/build/android/desugar/testdata/ConcreteFunction$Parser.class
diff --git a/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar b/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar
new file mode 100644
index 0000000..6a451eb
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/unused_closed_resource.jar
Binary files differ