Merge remote-tracking branch upstream-master into master

* origin/upstream-master: (43 commits)
  Allow --worker_max_instances to take MnemonicName=value to specify max for each named worker.
  Clean up code that directly imports nested classes like Builder, Entry, etc.
  Clean up code that directly imports nested classes like Builder, Entry, etc.
  Clean up code that directly imports nested classes like Builder, Entry, etc.
  Remove use of bare Immutable{List,Map,Set} Builder classes.
  Relax the assertion in Desugar for checking the calls to $closeResource(...). It is possible that $closeResource(...) is not used as the calls to it might be eliminated by some optimization tools, such as Proguard.
  Make attempting to change --config in invocation policy an error.
  Remove alphabetical sorting of options in the canonical list.
  Remove some deprecated resources flags.
  Remove category checking from incompatible changes.
  Support source versions newer than 8 in Bazel's annotation processors
  stub simple core library bridge methods that only differ in return type RELNOTES: None.
  Reflect core library moves in super calls, even in default method stubs.  Always generate default method stubs for emulated methods. RELNOTES: None.
  Make KeepScanner tool search classpath for nearest definition of each member reference, instead of potentially referring to a subtype. Refactor desugar's class loading machinery and related code into a separate package for easier reuse in this tool. RELNOTES: None.
  Minor fixes to KeepScanner tool: - use Guava to read zip entries - Fix keep rules emitted for constructors RELNOTES: None.
  Support custom implementations of emulated core interface methods RELNOTES: None.
  tests,windows: enable android.desugar.runtime
  emulate dynamic dispatch of emulated default interface methods RELNOTES: None.
  Android desugar config options to exclude methods from interface emulation RELNOTES: None.
  send invocations to emulated interfaces through dispatch helper. fix logic for implementing emulated interfaces. RELNOTES: None.
  ...

BUG: none
Test: existing
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