Merge remote-tracking branch 'aosp/upstream-master' into master am: bc20f321cd am: 2a4a26af86
am: fb6ba24633

Change-Id: I8c89cf7bd3e9745da06326d71ef68c26cff44bc2
diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
index 9b20c9d..1aaf0b6 100644
--- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
@@ -43,6 +43,7 @@
   private final ClassReaderFactory classpath;
   private final ClassReaderFactory bootclasspath;
   private final ClassLoader targetLoader;
+  private final DependencyCollector depsCollector;
   private final HashSet<String> instanceMethods = new HashSet<>();
 
   private boolean isInterface;
@@ -55,12 +56,14 @@
   public DefaultMethodClassFixer(
       ClassVisitor dest,
       ClassReaderFactory classpath,
+      DependencyCollector depsCollector,
       ClassReaderFactory bootclasspath,
       ClassLoader targetLoader) {
     super(Opcodes.ASM6, dest);
     this.classpath = classpath;
     this.bootclasspath = bootclasspath;
     this.targetLoader = targetLoader;
+    this.depsCollector = depsCollector;
   }
 
   @Override
@@ -310,8 +313,17 @@
    */
   private boolean defaultMethodsDefined(ImmutableList<String> interfaces) {
     for (String implemented : interfaces) {
+      if (bootclasspath.isKnown(implemented)) {
+        continue;
+      }
       ClassReader bytecode = classpath.readIfKnown(implemented);
-      if (bytecode != null && !bootclasspath.isKnown(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.
@@ -321,10 +333,6 @@
           return true;
         }
       }
-      // Else interface isn't on the classpath, which indicates incomplete classpaths. For now
-      // we'll just assume the missing interfaces don't declare default methods but if they do
-      // we'll end up with concrete classes that don't implement an abstract method, which can
-      // cause runtime failures.  The classpath needs to be fixed in this case.
     }
     return false;
   }
@@ -377,7 +385,7 @@
    */
   private class DefaultMethodStubber extends ClassVisitor {
 
-    private String interfaceName;
+    private String stubbedInterfaceName;
 
     public DefaultMethodStubber() {
       super(Opcodes.ASM6);
@@ -392,8 +400,8 @@
         String superName,
         String[] interfaces) {
       checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
-      checkState(interfaceName == null);
-      interfaceName = name;
+      checkState(stubbedInterfaceName == null);
+      stubbedInterfaceName = name;
     }
 
     @Override
@@ -405,6 +413,8 @@
         // 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));
 
         // 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
@@ -423,9 +433,9 @@
         }
         stubMethod.visitMethodInsn(
             Opcodes.INVOKESTATIC,
-            interfaceName + InterfaceDesugaring.COMPANION_SUFFIX,
+            InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName),
             name,
-            InterfaceDesugaring.companionDefaultMethodDescriptor(interfaceName, desc),
+            InterfaceDesugaring.companionDefaultMethodDescriptor(stubbedInterfaceName, desc),
             /*itf*/ false);
         stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
 
@@ -439,8 +449,10 @@
         // interface methods are correctly handled.
         return new InterfaceDesugaring.InterfaceInvocationRewriter(
             DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions),
-            interfaceName,
-            bootclasspath);
+            stubbedInterfaceName,
+            bootclasspath,
+            depsCollector,
+            internalName);
       } else {
         return null; // we don't care about the actual code in these methods
       }
diff --git a/java/com/google/devtools/build/android/desugar/DependencyCollector.java b/java/com/google/devtools/build/android/desugar/DependencyCollector.java
new file mode 100644
index 0000000..272a273
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/DependencyCollector.java
@@ -0,0 +1,98 @@
+// Copyright 2017 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 javax.annotation.Nullable;
+
+/**
+ * Interface for collecting desugaring metadata that we can use to double-check correct desugaring
+ * at the binary level by looking at the metadata written for all Jars on the runtime classpath
+ * (b/65645388). Use {@link NoWriteCollectors} for "no-op" collectors and {@link
+ * com.google.devtools.build.android.desugar.dependencies.MetadataCollector} for writing out
+ * metadata files.
+ */
+// TODO(kmb): There could conceivably be a "self-contained" version where we check at the end that
+// we actually saw all the companion classes (in recordDefaultMethods) that we "assumed"; useful
+// for one-shot runs over an entire binary.
+@SuppressWarnings("unused") // default implementations consist of empty method stubs
+public interface DependencyCollector {
+
+  /** Class name suffix used for interface companion classes. */
+  public String INTERFACE_COMPANION_SUFFIX = "$$CC";
+
+  /**
+   * Records that {@code origin} depends on companion class {@code target}.  For the resulting
+   * binary to be valid, {@code target} needs to exist, which isn't the case if the corresponding
+   * interface is only available as a compile-time ("neverlink") dependency.
+   */
+  default void assumeCompanionClass(String origin, String target) {}
+
+  /**
+   * Records that {@code origin} transitively implements {@code target} but {@code target} isn't
+   * in the classpath.  This can lead to wrong desugarings if {@code target} or an interface it
+   * extends defines default methods.
+   */
+  default void missingImplementedInterface(String origin, String target) {}
+
+  /**
+   * Records that the given interface extends the given interfaces.
+   *
+   * <p>This information is useful reference to double-check {@link #missingImplementedInterface}s
+   * without reading and parsing .class files, specifically if default methods are defined in
+   * interfaces that a missing interface transitively extends.
+   */
+  default void recordExtendedInterfaces(String origin, String... targets) {}
+
+  /**
+   * Records that the given interface has a companion class that includes the given number of
+   * default methods (0 if there were only static methods).  This method should not be called for
+   * purely abstract interfaces, to allow verifying available companion classes against this.
+   *
+   * <p>This information is useful reference to double-check {@link #missingImplementedInterface}s
+   * without reading and parsing .class files with better precision than just looking for
+   * companion classes on the runtime classpath (which may only contain static methods).
+   */
+  default void recordDefaultMethods(String origin, int count) {}
+
+  /**
+   * Returns metadata to include into the desugaring output or {@code null} if none.  Returning
+   * anything but {@code null} will cause an extra file to be written into the output, including
+   * an empty array.
+   */
+  @Nullable public byte[] toByteArray();
+
+  /** Simple collectors that don't collect any information. */
+  public enum NoWriteCollectors implements DependencyCollector {
+    /** Singleton instance that does nothing. */
+    NOOP,
+    /**
+     * Singleton instance that does nothing besides throwing if {@link #missingImplementedInterface}
+     * is called.
+     */
+    FAIL_ON_MISSING {
+      @Override
+      public void missingImplementedInterface(String origin, String target) {
+        throw new IllegalStateException(
+            String.format(
+                "Couldn't find interface %s on the classpath for desugaring %s", target, origin));
+      }
+    };
+
+    @Override
+    @Nullable
+    public final byte[] toByteArray() {
+      return null;
+    }
+  }
+}
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 31c362e..87aa0d7 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -176,6 +176,26 @@
     public int minSdkVersion;
 
     @Option(
+      name = "emit_dependency_metadata_as_needed",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Whether to emit META-INF/desugar_deps as needed for later consistency checking."
+    )
+    public boolean emitDependencyMetadata;
+
+    @Option(
+      name = "best_effort_tolerate_missing_deps",
+      defaultValue = "true",
+      category = "misc",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Whether to tolerate missing dependencies on the classpath in some cases.  You should "
+          + "strive to set this flag to false."
+    )
+    public boolean tolerateMissingDependencies;
+
+    @Option(
       name = "desugar_interface_method_bodies_if_needed",
       defaultValue = "true",
       category = "misc",
@@ -225,7 +245,6 @@
       defaultValue = "false",
       documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
       effectTags = {OptionEffectTag.UNKNOWN},
-      implicitRequirements = "--allow_empty_bootclasspath",
       help = "Enables rewriting to desugar java.* classes."
     )
     public boolean coreLibrary;
@@ -316,6 +335,7 @@
 
     try (OutputFileProvider outputFileProvider = toOutputFileProvider(outputPath);
         InputFileProvider inputFiles = toInputFileProvider(inputPath)) {
+      DependencyCollector depsCollector = createDepsCollector();
       IndexedInputs indexedInputFiles = new IndexedInputs(ImmutableList.of(inputFiles));
       // Prepend classpath with input file itself so LambdaDesugaring can load classes with
       // lambdas.
@@ -344,6 +364,7 @@
           outputFileProvider,
           loader,
           classpathReader,
+          depsCollector,
           bootclasspathReader,
           interfaceLambdaMethodCollector);
 
@@ -351,12 +372,18 @@
           outputFileProvider,
           loader,
           classpathReader,
+          depsCollector,
           bootclasspathReader,
           interfaceLambdaMethodCollector.build(),
           bridgeMethodReader);
 
-      desugarAndWriteGeneratedClasses(outputFileProvider);
+      desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader);
       copyThrowableExtensionClass(outputFileProvider);
+
+      byte[] depsInfo = depsCollector.toByteArray();
+      if (depsInfo != null) {
+        outputFileProvider.write(OutputFileProvider.DESUGAR_DEPS_FILENAME, depsInfo);
+      }
     }
 
     ImmutableMap<Path, LambdaInfo> lambdasLeftBehind = lambdas.drain();
@@ -365,6 +392,32 @@
     checkState(generatedLeftBehind.isEmpty(), "Didn't process %s", generatedLeftBehind.keySet());
   }
 
+  /**
+   * 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.
+   */
+  private DependencyCollector createDepsCollector() {
+    if (options.emitDependencyMetadata) {
+      try {
+        return (DependencyCollector)
+            Thread.currentThread()
+                .getContextClassLoader()
+                .loadClass(
+                    "com.google.devtools.build.android.desugar.dependencies.MetadataCollector")
+                .getConstructor(Boolean.TYPE)
+                .newInstance(options.tolerateMissingDependencies);
+      } catch (ReflectiveOperationException
+          | SecurityException e) {
+        throw new IllegalStateException("Can't emit desugaring metadata as requested");
+      }
+    } else if (options.tolerateMissingDependencies) {
+      return DependencyCollector.NoWriteCollectors.NOOP;
+    } else {
+      return DependencyCollector.NoWriteCollectors.FAIL_ON_MISSING;
+    }
+  }
+
   private void copyThrowableExtensionClass(OutputFileProvider outputFileProvider) {
     if (allowTryWithResources || options.desugarTryWithResourcesOmitRuntimeClasses) {
       // try-with-resources statements are okay in the output jar.
@@ -390,10 +443,15 @@
       OutputFileProvider outputFileProvider,
       ClassLoader loader,
       @Nullable ClassReaderFactory classpathReader,
+      DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
       Builder<String> interfaceLambdaMethodCollector)
       throws IOException {
     for (String filename : inputFiles) {
+      if (OutputFileProvider.DESUGAR_DEPS_FILENAME.equals(filename)) {
+        // 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)) {
         // 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
@@ -405,6 +463,7 @@
               createClassVisitorsForClassesInInputs(
                   loader,
                   classpathReader,
+                  depsCollector,
                   bootclasspathReader,
                   interfaceLambdaMethodCollector,
                   writer,
@@ -431,6 +490,7 @@
       OutputFileProvider outputFileProvider,
       ClassLoader loader,
       @Nullable ClassReaderFactory classpathReader,
+      DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
       ImmutableSet<String> interfaceLambdaMethods,
       @Nullable ClassReaderFactory bridgeMethodReader)
@@ -460,6 +520,7 @@
             createClassVisitorsForDumpedLambdaClasses(
                 loader,
                 classpathReader,
+                depsCollector,
                 bootclasspathReader,
                 interfaceLambdaMethods,
                 bridgeMethodReader,
@@ -473,7 +534,8 @@
     }
   }
 
-  private void desugarAndWriteGeneratedClasses(OutputFileProvider outputFileProvider)
+  private void desugarAndWriteGeneratedClasses(
+      OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader)
       throws IOException {
     // Write out any classes we generated along the way
     ImmutableMap<String, ClassNode> generatedClasses = store.drain();
@@ -485,7 +547,8 @@
       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);
+      ClassVisitor visitor =
+          new Java7Compatibility(writer, (ClassReaderFactory) null, bootclasspathReader);
       generated.getValue().accept(visitor);
       String filename = rewriter.unprefix(generated.getKey()) + ".class";
       outputFileProvider.write(filename, writer.toByteArray());
@@ -499,6 +562,7 @@
   private ClassVisitor createClassVisitorsForDumpedLambdaClasses(
       ClassLoader loader,
       @Nullable ClassReaderFactory classpathReader,
+      DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
       ImmutableSet<String> interfaceLambdaMethods,
       @Nullable ClassReaderFactory bridgeMethodReader,
@@ -520,12 +584,14 @@
     }
     if (outputJava7) {
       // null ClassReaderFactory b/c we don't expect to need it for lambda classes
-      visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null);
+      visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader);
       if (options.desugarInterfaceMethodBodiesIfNeeded) {
         visitor =
-            new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader, loader);
+            new DefaultMethodClassFixer(
+                visitor, classpathReader, depsCollector, bootclasspathReader, loader);
         visitor =
-            new InterfaceDesugaring(visitor, bootclasspathReader, store, options.legacyJacocoFix);
+            new InterfaceDesugaring(
+                visitor, depsCollector, bootclasspathReader, store, options.legacyJacocoFix);
       }
     }
     visitor =
@@ -553,6 +619,7 @@
   private ClassVisitor createClassVisitorsForClassesInInputs(
       ClassLoader loader,
       @Nullable ClassReaderFactory classpathReader,
+      DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
       Builder<String> interfaceLambdaMethodCollector,
       UnprefixingClassWriter writer,
@@ -571,12 +638,14 @@
     }
     if (!options.onlyDesugarJavac9ForLint) {
       if (outputJava7) {
-        visitor = new Java7Compatibility(visitor, classpathReader);
+        visitor = new Java7Compatibility(visitor, classpathReader, bootclasspathReader);
         if (options.desugarInterfaceMethodBodiesIfNeeded) {
           visitor =
-              new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader, loader);
+              new DefaultMethodClassFixer(
+                  visitor, classpathReader, depsCollector, bootclasspathReader, loader);
           visitor =
-              new InterfaceDesugaring(visitor, bootclasspathReader, store, options.legacyJacocoFix);
+              new InterfaceDesugaring(
+                  visitor, depsCollector, bootclasspathReader, store, options.legacyJacocoFix);
         }
       }
       // LambdaDesugaring is relatively expensive, so check first whether we need it.  Additionally,
diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
index 0d37a20..b93613c 100644
--- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -42,9 +42,9 @@
   static final String COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME = "$$triggerInterfaceInit";
   static final String COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC = "()V";
 
-  static final String COMPANION_SUFFIX = "$$CC";
   static final String INTERFACE_STATIC_COMPANION_METHOD_SUFFIX = "$$STATIC$$";
 
+  private final DependencyCollector depsCollector;
   private final ClassReaderFactory bootclasspath;
   private final GeneratedClassStore store;
   private final boolean legacyJaCoCo;
@@ -52,13 +52,18 @@
   private String internalName;
   private int bytecodeVersion;
   private int accessFlags;
+  private int numberOfDefaultMethods;
   @Nullable private ClassVisitor companion;
   @Nullable private FieldInfo interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit;
 
   public InterfaceDesugaring(
-      ClassVisitor dest, ClassReaderFactory bootclasspath, GeneratedClassStore store,
+      ClassVisitor dest,
+      DependencyCollector depsCollector,
+      ClassReaderFactory bootclasspath,
+      GeneratedClassStore store,
       boolean legacyJaCoCo) {
     super(Opcodes.ASM6, dest);
+    this.depsCollector = depsCollector;
     this.bootclasspath = bootclasspath;
     this.store = store;
     this.legacyJaCoCo = legacyJaCoCo;
@@ -73,15 +78,26 @@
       String superName,
       String[] interfaces) {
     companion = null;
+    numberOfDefaultMethods = 0;
     internalName = name;
     bytecodeVersion = version;
     accessFlags = access;
+    if (isInterface()) {
+      // Record interface hierarchy.  This helps avoid parsing .class files when double-checking
+      // desugaring results later using collected dependency information.
+      depsCollector.recordExtendedInterfaces(name, interfaces);
+    }
     super.visit(version, access, name, signature, superName, interfaces);
   }
 
   @Override
   public void visitEnd() {
     if (companion != null) {
+      // Record classes with default methods.  This increases precision when double-checking
+      // desugaring results later, without parsing .class files again, compared to just looking
+      // for companion classes in a given desugared Jar which may only contain static methods.
+      depsCollector.recordDefaultMethods(internalName, numberOfDefaultMethods);
+
       // Emit a method to access the fields of the interfaces that need initialization.
       emitInterfaceFieldAccessInCompanionMethodToTriggerInterfaceClinit();
       companion.visitEnd();
@@ -144,6 +160,7 @@
   @Override
   public MethodVisitor visitMethod(
       int access, String name, String desc, String signature, String[] exceptions) {
+    String codeOwner = internalName;
     MethodVisitor result;
     if (isInterface() && isStaticInitializer(name)) {
       result =
@@ -164,6 +181,8 @@
       name =
           normalizeInterfaceMethodName(
               name, isLambdaBody, BitFlags.isSet(access, Opcodes.ACC_STATIC));
+      codeOwner = getCompanionClassName(internalName);
+
       if (BitFlags.isSet(access, Opcodes.ACC_STATIC)) {
         // Completely move static interface methods, which requires rewriting call sites
         result =
@@ -185,6 +204,7 @@
               name,
               internalName,
               desc);
+          ++numberOfDefaultMethods;
           abstractDest =
               super.visitMethod(access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions);
         }
@@ -209,7 +229,7 @@
     }
     return result != null
         ? new InterfaceInvocationRewriter(
-            result, isInterface() ? internalName : null, bootclasspath)
+            result, isInterface() ? internalName : null, bootclasspath, depsCollector, codeOwner)
         : null;
   }
 
@@ -228,7 +248,7 @@
       // 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
       // its original name to include the interface name at this point.
-      suffix = COMPANION_SUFFIX;
+      suffix = DependencyCollector.INTERFACE_COMPANION_SUFFIX;
     } else if (isStatic) {
       suffix = INTERFACE_STATIC_COMPANION_METHOD_SUFFIX;
     } else {
@@ -238,7 +258,7 @@
   }
 
   static String getCompanionClassName(String interfaceName) {
-    return interfaceName + COMPANION_SUFFIX;
+    return interfaceName + DependencyCollector.INTERFACE_COMPANION_SUFFIX;
   }
 
   /**
@@ -313,25 +333,33 @@
     @Nullable private final String interfaceName;
 
     private final ClassReaderFactory bootclasspath;
+    private final DependencyCollector depsCollector;
+    /** Internal name that'll be used to record any dependencies on interface methods. */
+    private final String declaringClass;
 
     public InterfaceInvocationRewriter(
-        MethodVisitor dest, @Nullable String knownInterfaceName, ClassReaderFactory bootclasspath) {
+        MethodVisitor dest,
+        @Nullable String knownInterfaceName,
+        ClassReaderFactory bootclasspath,
+        DependencyCollector depsCollector,
+        String declaringClass) {
       super(Opcodes.ASM6, dest);
       this.interfaceName = knownInterfaceName;
       this.bootclasspath = bootclasspath;
+      this.depsCollector = depsCollector;
+      this.declaringClass = declaringClass;
     }
 
     @Override
     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
       // Assume that any static interface methods on the classpath are moved
-      if (itf || owner.equals(interfaceName)) {
+      if ((itf || owner.equals(interfaceName)) && !bootclasspath.isKnown(owner)) {
         boolean isLambda = name.startsWith("lambda$");
         name = normalizeInterfaceMethodName(name, isLambda, opcode == Opcodes.INVOKESTATIC);
         if (isLambda) {
           // Redirect lambda invocations to completely remove all lambda methods from interfaces.
-          checkArgument(
-              !owner.endsWith(COMPANION_SUFFIX), "shouldn't consider %s an interface", owner);
-          checkArgument(!bootclasspath.isKnown(owner)); // must be in current input
+          checkArgument(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX),
+              "shouldn't consider %s an interface", owner);
           if (opcode == Opcodes.INVOKEINTERFACE) {
             opcode = Opcodes.INVOKESTATIC;
             desc = companionDefaultMethodDescriptor(owner, desc);
@@ -344,7 +372,11 @@
                 name);
           }
           // Reflect that InterfaceDesugaring moves and renames the lambda body method
-          owner += COMPANION_SUFFIX;
+          owner += DependencyCollector.INTERFACE_COMPANION_SUFFIX;
+          itf = false;
+          // Record dependency on companion class
+          depsCollector.assumeCompanionClass(declaringClass, owner);
+
           String expectedLambdaMethodName = LambdaDesugaring.uniqueInPackage(owner, name);
           checkState(
               name.equals(expectedLambdaMethodName),
@@ -352,18 +384,18 @@
               owner,
               name,
               expectedLambdaMethodName);
-          itf = false;
-        } else if ((opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)
-            && !bootclasspath.isKnown(owner)) {
-          checkArgument(
-              !owner.endsWith(COMPANION_SUFFIX), "shouldn't consider %s an interface", owner);
+        } else if ((opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) {
+          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)
             opcode = Opcodes.INVOKESTATIC;
             desc = companionDefaultMethodDescriptor(owner, desc);
           }
-          owner += COMPANION_SUFFIX;
+          owner += DependencyCollector.INTERFACE_COMPANION_SUFFIX;
           itf = false;
+          // Record dependency on companion class
+          depsCollector.assumeCompanionClass(declaringClass, owner);
         }
       }
       super.visitMethodInsn(opcode, owner, name, desc, itf);
@@ -384,7 +416,8 @@
     @Override
     public void visitFieldInsn(int opcode, String owner, String name, String desc) {
       if ("$jacocoData".equals(name)) {
-        checkState(!owner.endsWith(COMPANION_SUFFIX), "Expected interface: %s", owner);
+        checkState(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX),
+            "Expected interface: %s", owner);
         owner = getCompanionClassName(owner);
       }
       super.visitFieldInsn(opcode, 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 752227e..30de63d 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 javax.annotation.Nullable;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.Attribute;
 import org.objectweb.asm.ClassReader;
@@ -27,19 +28,28 @@
 import org.objectweb.asm.TypePath;
 
 /**
- * Visitor that ensures bytecode version <= 51 (Java 7) and that throws if it sees default or static
- * interface methods (i.e., non-abstract interface methods), which don't exist in Java 7.
+ * Visitor that tries to ensures bytecode version <= 51 (Java 7) and that throws if it sees default
+ * or static interface methods (i.e., non-abstract interface methods), which don't exist in Java 7.
+ * <p>The class version will 52 iff static interface method from the bootclasspath is invoked.
+ * This is mostly ensure that the generated bytecode is valid.
  */
 public class Java7Compatibility extends ClassVisitor {
 
-  private final ClassReaderFactory factory;
+  @Nullable private final ClassReaderFactory factory;
+  @Nullable private final ClassReaderFactory bootclasspathReader;
 
   private boolean isInterface;
   private String internalName;
+  private int access;
+  private String signature;
+  private String superName;
+  private String[] interfaces;
 
-  public Java7Compatibility(ClassVisitor cv, ClassReaderFactory factory) {
+  public Java7Compatibility(
+      ClassVisitor cv, ClassReaderFactory factory, ClassReaderFactory bootclasspathReader) {
     super(Opcodes.ASM6, cv);
     this.factory = factory;
+    this.bootclasspathReader = bootclasspathReader;
   }
 
   @Override
@@ -51,6 +61,10 @@
       String superName,
       String[] interfaces) {
     internalName = name;
+    this.access = access;
+    this.signature = signature;
+    this.superName = superName;
+    this.interfaces = interfaces;
     isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
     super.visit(
         Math.min(version, Opcodes.V1_7),
@@ -83,7 +97,10 @@
         || "<clinit>".equals(name),
         "Interface %s defines non-abstract method %s%s, which is not supported",
         internalName, name, desc);
-    MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions);
+    MethodVisitor result =
+        new UpdateBytecodeVersionIfNecessary(
+            super.visitMethod(access, name, desc, signature, exceptions));
+
     return (isInterface && "<clinit>".equals(name)) ? new InlineJacocoInit(result) : result;
   }
 
@@ -96,6 +113,42 @@
     }
   }
 
+  /** This will rewrite class version to 52 if it sees invokestatic on an interface. */
+  private class UpdateBytecodeVersionIfNecessary extends MethodVisitor {
+
+    boolean updated = false;
+
+    public UpdateBytecodeVersionIfNecessary(MethodVisitor methodVisitor) {
+      super(Opcodes.ASM5, methodVisitor);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      if (itf && opcode == Opcodes.INVOKESTATIC) {
+        checkNotNull(bootclasspathReader);
+        checkState(
+            bootclasspathReader.isKnown(owner),
+            "%s contains invocation of static interface method that is "
+                + "not in the bootclasspath. Owner: %s, name: %s, desc: %s.",
+            Java7Compatibility.this.internalName,
+            owner,
+            name,
+            desc);
+        if (!updated) {
+          Java7Compatibility.this.cv.visit(
+              Opcodes.V1_8,
+              Java7Compatibility.this.access,
+              Java7Compatibility.this.internalName,
+              Java7Compatibility.this.signature,
+              Java7Compatibility.this.superName,
+              Java7Compatibility.this.interfaces);
+          updated = true;
+        }
+      }
+      super.visitMethodInsn(opcode, owner, name, desc, itf);
+    }
+  }
+
   private class InlineJacocoInit extends MethodVisitor {
     public InlineJacocoInit(MethodVisitor dest) {
       super(Opcodes.ASM6, dest);
@@ -106,6 +159,7 @@
       if (opcode == Opcodes.INVOKESTATIC
           && "$jacocoInit".equals(name)
           && internalName.equals(owner)) {
+        checkNotNull(factory);
         ClassReader bytecode = checkNotNull(factory.readIfKnown(internalName),
             "Couldn't load interface %s to inline $jacocoInit()", internalName);
         InlineOneMethod copier = new InlineOneMethod("$jacocoInit", this);
diff --git a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java b/java/com/google/devtools/build/android/desugar/OutputFileProvider.java
index bf3b710..7a590ef 100644
--- a/java/com/google/devtools/build/android/desugar/OutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/OutputFileProvider.java
@@ -18,6 +18,9 @@
 /** Output file provider allows to write files in directory or jar files. */
 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";
+
   /**
    * Copy {@code filename} from {@code inputFileProvider} to this output. If input file provider is
    * a {@link ZipInputFileProvider} then the metadata of the zip entry are kept.
diff --git a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java b/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
index 3f4f344..8d6501d 100644
--- a/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
+++ b/java/com/google/devtools/build/android/desugar/ZipOutputFileProvider.java
@@ -13,7 +13,8 @@
 // limitations under the License.
 package com.google.devtools.build.android.desugar;
 
-import com.google.common.base.Preconditions;
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.io.ByteStreams;
 import java.io.BufferedOutputStream;
 import java.io.IOException;
@@ -45,7 +46,8 @@
 
   @Override
   public void write(String filename, byte[] content) throws IOException {
-    Preconditions.checkArgument(filename.endsWith(".class"));
+    checkArgument(filename.equals(DESUGAR_DEPS_FILENAME) || filename.endsWith(".class"),
+        "Expect file to be copied: %s", filename);
     writeStoredEntry(out, filename, content);
   }
 
diff --git a/java/com/google/devtools/common/options/ExpansionContext.java b/java/com/google/devtools/common/options/ExpansionContext.java
deleted file mode 100644
index c6aecc7..0000000
--- a/java/com/google/devtools/common/options/ExpansionContext.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2017 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.common.options;
-
-import java.lang.reflect.Field;
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
-
-/**
- * Encapsulates the data given to {@link ExpansionFunction} objects. This lets {@link
- * ExpansionFunction} objects change how it expands flags based on the arguments given to the {@link
- * OptionsParser}.
- */
-@ThreadSafe
-public final class ExpansionContext {
-  private final IsolatedOptionsData optionsData;
-  private final OptionDefinition optionDefinition;
-  @Nullable private final String unparsedValue;
-
-  public ExpansionContext(
-      IsolatedOptionsData optionsData,
-      OptionDefinition optionDefinition,
-      @Nullable String unparsedValue) {
-    this.optionsData = optionsData;
-    this.optionDefinition = optionDefinition;
-    this.unparsedValue = unparsedValue;
-  }
-
-  /** Metadata for the option that is being expanded. */
-  public IsolatedOptionsData getOptionsData() {
-    return optionsData;
-  }
-
-  /** {@link Field} object for option that is being expanded. */
-  public OptionDefinition getOptionDefinition() {
-    return optionDefinition;
-  }
-
-  /** Argument given to this flag during options parsing. Will be null if no argument was given. */
-  @Nullable
-  public String getUnparsedValue() {
-    return unparsedValue;
-  }
-}
diff --git a/java/com/google/devtools/common/options/ExpansionFunction.java b/java/com/google/devtools/common/options/ExpansionFunction.java
index 09119b2..d2c2693 100644
--- a/java/com/google/devtools/common/options/ExpansionFunction.java
+++ b/java/com/google/devtools/common/options/ExpansionFunction.java
@@ -30,5 +30,5 @@
    *     information is computed
    * @return An expansion to use on an empty list
    */
-  ImmutableList<String> getExpansion(ExpansionContext context) throws OptionsParsingException;
+  ImmutableList<String> getExpansion(IsolatedOptionsData optionsData);
 }
diff --git a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
index 742acb6..a53ff5b 100644
--- a/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
+++ b/java/com/google/devtools/common/options/InvocationPolicyEnforcer.java
@@ -14,7 +14,6 @@
 package com.google.devtools.common.options;
 
 import com.google.common.base.Joiner;
-import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
@@ -27,6 +26,7 @@
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.SetValue;
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.UseDefault;
+import com.google.devtools.common.options.OptionPriority.PriorityCategory;
 import com.google.devtools.common.options.OptionsParser.OptionDescription;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -35,15 +35,14 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /**
- * Enforces the {@link FlagPolicy}s (from an {@link InvocationPolicy} proto) on an
- * {@link OptionsParser} by validating and changing the flag values in the given
- * {@link OptionsParser}.
+ * Enforces the {@link FlagPolicy}s (from an {@link InvocationPolicy} proto) on an {@link
+ * OptionsParser} by validating and changing the flag values in the given {@link OptionsParser}.
  *
  * <p>"Flag" and "Option" are used interchangeably in this file.
  */
@@ -52,9 +51,8 @@
   private static final Logger logger = Logger.getLogger(InvocationPolicyEnforcer.class.getName());
 
   private static final String INVOCATION_POLICY_SOURCE = "Invocation policy";
-  private static final Function<OptionDefinition, String> INVOCATION_POLICY_SOURCE_FUNCTION =
-      o -> INVOCATION_POLICY_SOURCE;
   @Nullable private final InvocationPolicy invocationPolicy;
+  private final Level loglevel;
 
   /**
    * Creates an InvocationPolicyEnforcer that enforces the given policy.
@@ -63,7 +61,33 @@
    *     nothing in calls to enforce().
    */
   public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy) {
+    this(invocationPolicy, Level.FINE);
+  }
+
+  /**
+   * Creates an InvocationPolicyEnforcer that enforces the given policy.
+   *
+   * @param invocationPolicy the policy to enforce. A null policy means this enforcer will do
+   *     nothing in calls to enforce().
+   * @param loglevel the level at which to log informational statements. Warnings and errors will
+   *     still be logged at the appropriate level.
+   */
+  public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy, Level loglevel) {
     this.invocationPolicy = invocationPolicy;
+    this.loglevel = loglevel;
+  }
+
+  private static final class FlagPolicyWithContext {
+    private final FlagPolicy policy;
+    private final OptionDescription description;
+    private final OptionInstanceOrigin origin;
+
+    public FlagPolicyWithContext(
+        FlagPolicy policy, OptionDescription description, OptionInstanceOrigin origin) {
+      this.policy = policy;
+      this.description = description;
+      this.origin = origin;
+    }
   }
 
   public InvocationPolicy getInvocationPolicy() {
@@ -97,10 +121,11 @@
 
     // The effective policy returned is expanded, filtered for applicable commands, and cleaned of
     // redundancies and conflicts.
-    List<FlagPolicy> effectivePolicies = getEffectivePolicies(invocationPolicy, parser, command);
+    List<FlagPolicyWithContext> effectivePolicies =
+        getEffectivePolicies(invocationPolicy, parser, command, loglevel);
 
-    for (FlagPolicy flagPolicy : effectivePolicies) {
-      String flagName = flagPolicy.getFlagName();
+    for (FlagPolicyWithContext flagPolicy : effectivePolicies) {
+      String flagName = flagPolicy.policy.getFlagName();
 
       OptionValueDescription valueDescription;
       try {
@@ -109,48 +134,53 @@
         // This flag doesn't exist. We are deliberately lenient if the flag policy has a flag
         // we don't know about. This is for better future proofing so that as new flags are added,
         // new policies can use the new flags without worrying about older versions of Bazel.
-        logger.info(
+        logger.log(
+            loglevel,
             String.format("Flag '%s' specified by invocation policy does not exist", flagName));
         continue;
       }
 
-      OptionDescription optionDescription =
-          parser.getOptionDescription(
-              flagName, OptionPriority.INVOCATION_POLICY, INVOCATION_POLICY_SOURCE);
       // getOptionDescription() will return null if the option does not exist, however
       // getOptionValueDescription() above would have thrown an IllegalArgumentException if that
       // were the case.
-      Verify.verifyNotNull(optionDescription);
+      Verify.verifyNotNull(flagPolicy.description);
 
-      switch (flagPolicy.getOperationCase()) {
+      switch (flagPolicy.policy.getOperationCase()) {
         case SET_VALUE:
-          applySetValueOperation(parser, flagPolicy, valueDescription, optionDescription);
+          applySetValueOperation(parser, flagPolicy, valueDescription, loglevel);
           break;
 
         case USE_DEFAULT:
-          applyUseDefaultOperation(parser, "UseDefault", optionDescription.getOptionDefinition());
+          applyUseDefaultOperation(
+              parser, "UseDefault", flagPolicy.description.getOptionDefinition(), loglevel);
           break;
 
         case ALLOW_VALUES:
-          AllowValues allowValues = flagPolicy.getAllowValues();
-          FilterValueOperation.ALLOW_VALUE_OPERATION.apply(
+          AllowValues allowValues = flagPolicy.policy.getAllowValues();
+          FilterValueOperation.AllowValueOperation allowValueOperation =
+              new FilterValueOperation.AllowValueOperation(loglevel);
+          allowValueOperation.apply(
               parser,
+              flagPolicy.origin,
               allowValues.getAllowedValuesList(),
               allowValues.hasNewValue() ? allowValues.getNewValue() : null,
               allowValues.hasUseDefault(),
               valueDescription,
-              optionDescription);
+              flagPolicy.description);
           break;
 
         case DISALLOW_VALUES:
-          DisallowValues disallowValues = flagPolicy.getDisallowValues();
-          FilterValueOperation.DISALLOW_VALUE_OPERATION.apply(
+          DisallowValues disallowValues = flagPolicy.policy.getDisallowValues();
+          FilterValueOperation.DisallowValueOperation disallowValueOperation =
+              new FilterValueOperation.DisallowValueOperation(loglevel);
+          disallowValueOperation.apply(
               parser,
+              flagPolicy.origin,
               disallowValues.getDisallowedValuesList(),
               disallowValues.hasNewValue() ? disallowValues.getNewValue() : null,
               disallowValues.hasUseDefault(),
               valueDescription,
-              optionDescription);
+              flagPolicy.description);
           break;
 
         case OPERATION_NOT_SET:
@@ -160,7 +190,7 @@
           logger.warning(
               String.format(
                   "Unknown operation '%s' from invocation policy for flag '%s'",
-                  flagPolicy.getOperationCase(), flagName));
+                  flagPolicy.policy.getOperationCase(), flagName));
           break;
       }
     }
@@ -182,13 +212,27 @@
     return !Collections.disjoint(policy.getCommandsList(), applicableCommands);
   }
 
+  /** Returns the expanded and filtered policy that would be enforced for the given command. */
+  public static InvocationPolicy getEffectiveInvocationPolicy(
+      InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)
+      throws OptionsParsingException {
+    ImmutableList<FlagPolicyWithContext> effectivePolicies =
+        getEffectivePolicies(invocationPolicy, parser, command, loglevel);
+
+    InvocationPolicy.Builder builder = InvocationPolicy.newBuilder();
+    for (FlagPolicyWithContext policyWithContext : effectivePolicies) {
+      builder.addFlagPolicies(policyWithContext.policy);
+    }
+    return builder.build();
+  }
+
   /**
    * Takes the provided policy and processes it to the form that can be used on the user options.
    *
    * <p>Expands any policies on expansion flags.
    */
-  public static ImmutableList<FlagPolicy> getEffectivePolicies(
-      InvocationPolicy invocationPolicy, OptionsParser parser, String command)
+  private static ImmutableList<FlagPolicyWithContext> getEffectivePolicies(
+      InvocationPolicy invocationPolicy, OptionsParser parser, String command, Level loglevel)
       throws OptionsParsingException {
     if (invocationPolicy == null) {
       return ImmutableList.of();
@@ -200,20 +244,42 @@
             : CommandNameCache.CommandNameCacheInstance.INSTANCE.get(command);
 
     // Expand all policies to transfer policies on expansion flags to policies on the child flags.
-    List<FlagPolicy> expandedPolicies = new ArrayList<>();
+    List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
+    OptionPriority nextPriority =
+        OptionPriority.lowestOptionPriorityAtCategory(PriorityCategory.INVOCATION_POLICY);
     for (FlagPolicy policy : invocationPolicy.getFlagPoliciesList()) {
+      // 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;
+      OptionInstanceOrigin origin =
+          new OptionInstanceOrigin(currentPriority, INVOCATION_POLICY_SOURCE, null, null);
+      nextPriority = OptionPriority.nextOptionPriority(currentPriority);
       if (!policyApplies(policy, commandAndParentCommands)) {
         // Only keep and expand policies that are applicable to the current command.
         continue;
       }
-      List<FlagPolicy> policies = expandPolicy(policy, parser);
+
+      OptionDescription optionDescription = parser.getOptionDescription(policy.getFlagName());
+      if (optionDescription == null) {
+        // InvocationPolicy ignores policy on non-existing flags by design, for version
+        // compatibility.
+        logger.log(
+            loglevel,
+            String.format(
+                "Flag '%s' specified by invocation policy does not exist, and will be ignored",
+                policy.getFlagName()));
+        continue;
+      }
+      FlagPolicyWithContext policyWithContext =
+          new FlagPolicyWithContext(policy, optionDescription, origin);
+      List<FlagPolicyWithContext> policies = expandPolicy(policyWithContext, parser, loglevel);
       expandedPolicies.addAll(policies);
     }
 
     // Only keep that last policy for each flag.
-    Map<String, FlagPolicy> effectivePolicy = new HashMap<>();
-    for (FlagPolicy expandedPolicy : expandedPolicies) {
-      String flagName = expandedPolicy.getFlagName();
+    Map<String, FlagPolicyWithContext> effectivePolicy = new HashMap<>();
+    for (FlagPolicyWithContext expandedPolicy : expandedPolicies) {
+      String flagName = expandedPolicy.policy.getFlagName();
       effectivePolicy.put(flagName, expandedPolicy);
     }
 
@@ -232,78 +298,6 @@
         String.format("Disallow_Values on expansion flags like %s is not allowed.", flagName));
   }
 
-  private static ImmutableList<ParsedOptionDescription> getExpansionsFromFlagPolicy(
-      FlagPolicy expansionPolicy, OptionDescription optionDescription, OptionsParser parser)
-      throws OptionsParsingException {
-    if (!optionDescription.isExpansion()) {
-      return ImmutableList.of();
-    }
-
-    Preconditions.checkArgument(
-        expansionPolicy
-            .getFlagName()
-            .equals(optionDescription.getOptionDefinition().getOptionName()),
-        String.format(
-            "The optionDescription provided (for flag %s) does not match the policy for flag %s.",
-            optionDescription.getOptionDefinition().getOptionName(),
-            expansionPolicy.getFlagName()));
-
-    ImmutableList.Builder<ParsedOptionDescription> resultsBuilder = ImmutableList.builder();
-    switch (expansionPolicy.getOperationCase()) {
-      case SET_VALUE:
-        {
-          SetValue setValue = expansionPolicy.getSetValue();
-          if (setValue.getFlagValueCount() > 0) {
-            for (String value : setValue.getFlagValueList()) {
-              resultsBuilder.addAll(
-                  parser.getExpansionOptionValueDescriptions(
-                      optionDescription.getOptionDefinition(),
-                      value,
-                      OptionPriority.INVOCATION_POLICY,
-                      INVOCATION_POLICY_SOURCE));
-            }
-          } else {
-            resultsBuilder.addAll(
-                parser.getExpansionOptionValueDescriptions(
-                    optionDescription.getOptionDefinition(),
-                    null,
-                    OptionPriority.INVOCATION_POLICY,
-                    INVOCATION_POLICY_SOURCE));
-          }
-        }
-        break;
-      case USE_DEFAULT:
-        resultsBuilder.addAll(
-            parser.getExpansionOptionValueDescriptions(
-                optionDescription.getOptionDefinition(),
-                null,
-                OptionPriority.INVOCATION_POLICY,
-                INVOCATION_POLICY_SOURCE));
-        break;
-      case ALLOW_VALUES:
-        // All expansions originally given to the parser have been expanded by now, so these two
-        // cases aren't necessary (the values given in the flag policy shouldn't need to be
-        // checked). If you care about blocking specific flag values you should block the behavior
-        // on the specific ones, not the expansion that contains them.
-        throwAllowValuesOnExpansionFlagException(expansionPolicy.getFlagName());
-        break;
-      case DISALLOW_VALUES:
-        throwDisallowValuesOnExpansionFlagException(expansionPolicy.getFlagName());
-        break;
-      case OPERATION_NOT_SET:
-        throw new PolicyOperationNotSetException(expansionPolicy.getFlagName());
-      default:
-        logger.warning(
-            String.format(
-                "Unknown operation '%s' from invocation policy for flag '%s'",
-                expansionPolicy.getOperationCase(),
-                optionDescription.getOptionDefinition().getOptionName()));
-        break;
-    }
-
-    return resultsBuilder.build();
-  }
-
   /**
    * Expand a single policy. If the policy is not about an expansion flag, this will simply return a
    * list with a single element, oneself. If the policy is for an expansion flag, the policy will
@@ -311,83 +305,95 @@
    *
    * <p>None of the flagPolicies returned should be on expansion flags.
    */
-  private static List<FlagPolicy> expandPolicy(
-      FlagPolicy originalPolicy,
-      OptionsParser parser)
+  private static List<FlagPolicyWithContext> expandPolicy(
+      FlagPolicyWithContext originalPolicy,
+      OptionsParser parser,
+      Level loglevel)
       throws OptionsParsingException {
-    List<FlagPolicy> expandedPolicies = new ArrayList<>();
+    List<FlagPolicyWithContext> expandedPolicies = new ArrayList<>();
 
-    OptionDescription originalOptionDescription =
-        parser.getOptionDescription(
-            originalPolicy.getFlagName(),
-            OptionPriority.INVOCATION_POLICY,
-            INVOCATION_POLICY_SOURCE);
-    if (originalOptionDescription == null) {
-      // InvocationPolicy ignores policy on non-existing flags by design, for version compatibility.
-      return expandedPolicies;
+    boolean isExpansion = originalPolicy.description.isExpansion();
+    ImmutableList<ParsedOptionDescription> subflags =
+        parser.getExpansionValueDescriptions(
+            originalPolicy.description.getOptionDefinition(), originalPolicy.origin);
+
+    // If we have nothing to expand to, no need to do any further work.
+    if (subflags.isEmpty()) {
+      return ImmutableList.of(originalPolicy);
     }
 
-    ImmutableList<ParsedOptionDescription> expansions =
-        getExpansionsFromFlagPolicy(originalPolicy, originalOptionDescription, parser);
-    ImmutableList.Builder<ParsedOptionDescription> subflagBuilder = ImmutableList.builder();
-    ImmutableList<ParsedOptionDescription> subflags =
-        subflagBuilder
-            .addAll(originalOptionDescription.getImplicitRequirements())
-            .addAll(expansions)
-            .build();
-    boolean isExpansion = originalOptionDescription.isExpansion();
-
-    if (!subflags.isEmpty() && logger.isLoggable(Level.FINE)) {
-      // Log the expansion. Since this is logged regardless of user provided command line, it is
-      // only really useful for understanding the invocation policy itself. Most of the time,
-      // invocation policy does not change, so this can be a log level fine.
+    if (logger.isLoggable(loglevel)) {
+      // Log the expansion. This is only really useful for understanding the invocation policy
+      // itself.
       List<String> subflagNames = new ArrayList<>(subflags.size());
       for (ParsedOptionDescription subflag : subflags) {
         subflagNames.add("--" + subflag.getOptionDefinition().getOptionName());
       }
 
       logger.logp(
-          Level.FINE,
+          loglevel,
           "InvocationPolicyEnforcer",
           "expandPolicy",
           String.format(
               "Expanding %s on option %s to its %s: %s.",
-              originalPolicy.getOperationCase(),
-              originalPolicy.getFlagName(),
+              originalPolicy.policy.getOperationCase(),
+              originalPolicy.policy.getFlagName(),
               isExpansion ? "expansions" : "implied flags",
               Joiner.on("; ").join(subflagNames)));
     }
 
     // Repeated flags are special, and could set multiple times in an expansion, with the user
     // expecting both values to be valid. Collect these separately.
-    Multimap<OptionDefinition, ParsedOptionDescription> repeatableSubflagsInSetValues =
+    Multimap<OptionDescription, ParsedOptionDescription> repeatableSubflagsInSetValues =
         ArrayListMultimap.create();
 
     // Create a flag policy for the child that looks like the parent's policy "transferred" to its
     // child. Note that this only makes sense for SetValue, when setting an expansion flag, or
     // UseDefault, when preventing it from being set.
     for (ParsedOptionDescription currentSubflag : subflags) {
+      OptionDescription subflagOptionDescription =
+          parser.getOptionDescription(currentSubflag.getOptionDefinition().getOptionName());
+
       if (currentSubflag.getOptionDefinition().allowsMultiple()
-          && originalPolicy.getOperationCase().equals(OperationCase.SET_VALUE)) {
-        repeatableSubflagsInSetValues.put(currentSubflag.getOptionDefinition(), currentSubflag);
+          && originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE)) {
+        repeatableSubflagsInSetValues.put(subflagOptionDescription, currentSubflag);
       } else {
-        FlagPolicy subflagAsPolicy =
-            getSingleValueSubflagAsPolicy(currentSubflag, originalPolicy, isExpansion);
+        FlagPolicyWithContext subflagAsPolicy =
+            getSingleValueSubflagAsPolicy(
+                subflagOptionDescription, currentSubflag, originalPolicy, isExpansion);
         // In case any of the expanded flags are themselves expansions, recurse.
-        expandedPolicies.addAll(expandPolicy(subflagAsPolicy, parser));
+        expandedPolicies.addAll(expandPolicy(subflagAsPolicy, parser, loglevel));
       }
     }
 
     // If there are any repeatable flag SetValues, deal with them together now.
     // Note that expansion flags have no value, and so cannot have multiple values either.
     // Skipping the recursion above is fine.
-    for (OptionDefinition repeatableFlag : repeatableSubflagsInSetValues.keySet()) {
+    for (OptionDescription repeatableFlag : repeatableSubflagsInSetValues.keySet()) {
       int numValues = repeatableSubflagsInSetValues.get(repeatableFlag).size();
       ArrayList<String> newValues = new ArrayList<>(numValues);
+      ArrayList<OptionInstanceOrigin> origins = new ArrayList<>(numValues);
       for (ParsedOptionDescription setValue : repeatableSubflagsInSetValues.get(repeatableFlag)) {
         newValues.add(setValue.getUnconvertedValue());
+        origins.add(setValue.getOrigin());
       }
-      expandedPolicies.add(getSetValueSubflagAsPolicy(repeatableFlag, newValues, originalPolicy));
+      // These options come from expanding a single policy, so they have effectively the same
+      // priority. They could have come from different expansions or implicit requirements in the
+      // recursive resolving of the option list, so just pick the first one. Do collapse the source
+      // strings though, in case there are different sources.
+      OptionInstanceOrigin arbitraryFirstOptionOrigin = origins.get(0);
+      OptionInstanceOrigin originOfSubflags =
+          new OptionInstanceOrigin(
+              arbitraryFirstOptionOrigin.getPriority(),
+              origins
+                  .stream()
+                  .map(OptionInstanceOrigin::getSource)
+                  .distinct()
+                  .collect(Collectors.joining(", ")),
+              arbitraryFirstOptionOrigin.getImplicitDependent(),
+              arbitraryFirstOptionOrigin.getExpandedFrom());
+      expandedPolicies.add(
+          getSetValueSubflagAsPolicy(repeatableFlag, newValues, originOfSubflags, originalPolicy));
     }
 
     // Don't add the original policy if it was an expansion flag, which have no value, but do add
@@ -404,16 +410,20 @@
    * policies that set the flag, and so interact with repeatable flags, flags that can be set
    * multiple times, in subtle ways.
    *
-   * @param subflag, the definition of the flag the SetValue'd expansion flag expands to.
+   * @param subflagDesc, the description of the flag the SetValue'd expansion flag expands to.
    * @param subflagValue, the values that the SetValue'd expansion flag expands to for this flag.
    * @param originalPolicy, the original policy on the expansion flag.
    * @return the flag policy for the subflag given, this will be part of the expanded form of the
    *     SetValue policy on the original flag.
    */
-  private static FlagPolicy getSetValueSubflagAsPolicy(
-      OptionDefinition subflag, List<String> subflagValue, FlagPolicy originalPolicy) {
+  private static FlagPolicyWithContext getSetValueSubflagAsPolicy(
+      OptionDescription subflagDesc,
+      List<String> subflagValue,
+      OptionInstanceOrigin subflagOrigin,
+      FlagPolicyWithContext originalPolicy) {
     // Some sanity checks.
-    Verify.verify(originalPolicy.getOperationCase().equals(OperationCase.SET_VALUE));
+    OptionDefinition subflag = subflagDesc.getOptionDefinition();
+    Verify.verify(originalPolicy.policy.getOperationCase().equals(OperationCase.SET_VALUE));
     if (!subflag.allowsMultiple()) {
       Verify.verify(subflagValue.size() <= 1);
     }
@@ -425,28 +435,34 @@
       setValueExpansion.addFlagValue(value);
     }
     if (subflag.allowsMultiple()) {
-      setValueExpansion.setAppend(originalPolicy.getSetValue().getOverridable());
+      setValueExpansion.setAppend(originalPolicy.policy.getSetValue().getOverridable());
     } else {
-      setValueExpansion.setOverridable(originalPolicy.getSetValue().getOverridable());
+      setValueExpansion.setOverridable(originalPolicy.policy.getSetValue().getOverridable());
     }
 
     // Commands from the original policy, flag name of the expansion
-    return FlagPolicy.newBuilder()
-        .addAllCommands(originalPolicy.getCommandsList())
-        .setFlagName(subflag.getOptionName())
-        .setSetValue(setValueExpansion)
-        .build();
+    return new FlagPolicyWithContext(
+        FlagPolicy.newBuilder()
+            .addAllCommands(originalPolicy.policy.getCommandsList())
+            .setFlagName(subflag.getOptionName())
+            .setSetValue(setValueExpansion)
+            .build(),
+        subflagDesc,
+        subflagOrigin);
   }
 
   /**
    * For an expansion flag in an invocation policy, each flag it expands to must be given a
    * corresponding policy.
    */
-  private static FlagPolicy getSingleValueSubflagAsPolicy(
-      ParsedOptionDescription currentSubflag, FlagPolicy originalPolicy, boolean isExpansion)
+  private static FlagPolicyWithContext getSingleValueSubflagAsPolicy(
+      OptionDescription subflagContext,
+      ParsedOptionDescription currentSubflag,
+      FlagPolicyWithContext originalPolicy,
+      boolean isExpansion)
       throws OptionsParsingException {
-    FlagPolicy subflagAsPolicy = null;
-    switch (originalPolicy.getOperationCase()) {
+    FlagPolicyWithContext subflagAsPolicy = null;
+    switch (originalPolicy.policy.getOperationCase()) {
       case SET_VALUE:
         if (currentSubflag.getOptionDefinition().allowsMultiple()) {
           throw new AssertionError(
@@ -463,22 +479,25 @@
         }
         subflagAsPolicy =
             getSetValueSubflagAsPolicy(
-                currentSubflag.getOptionDefinition(), subflagValue, originalPolicy);
+                subflagContext, subflagValue, currentSubflag.getOrigin(), originalPolicy);
         break;
 
       case USE_DEFAULT:
         // Commands from the original policy, flag name of the expansion
         subflagAsPolicy =
-            FlagPolicy.newBuilder()
-                .addAllCommands(originalPolicy.getCommandsList())
-                .setFlagName(currentSubflag.getOptionDefinition().getOptionName())
-                .setUseDefault(UseDefault.getDefaultInstance())
-                .build();
+            new FlagPolicyWithContext(
+                FlagPolicy.newBuilder()
+                    .addAllCommands(originalPolicy.policy.getCommandsList())
+                    .setFlagName(currentSubflag.getOptionDefinition().getOptionName())
+                    .setUseDefault(UseDefault.getDefaultInstance())
+                    .build(),
+                subflagContext,
+                currentSubflag.getOrigin());
         break;
 
       case ALLOW_VALUES:
         if (isExpansion) {
-          throwAllowValuesOnExpansionFlagException(originalPolicy.getFlagName());
+          throwAllowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName());
         }
         // If this flag is an implicitRequirement, and some values for the parent flag are
         // allowed, nothing needs to happen on the implicitRequirement that is set for all
@@ -487,7 +506,7 @@
 
       case DISALLOW_VALUES:
         if (isExpansion) {
-          throwDisallowValuesOnExpansionFlagException(originalPolicy.getFlagName());
+          throwDisallowValuesOnExpansionFlagException(originalPolicy.policy.getFlagName());
         }
         // If this flag is an implicitRequirement, and some values for the parent flag are
         // disallowed, that implies that all others are allowed, so nothing needs to happen
@@ -495,7 +514,7 @@
         break;
 
       case OPERATION_NOT_SET:
-        throw new PolicyOperationNotSetException(originalPolicy.getFlagName());
+        throw new PolicyOperationNotSetException(originalPolicy.policy.getFlagName());
 
       default:
         return null;
@@ -503,11 +522,12 @@
     return subflagAsPolicy;
   }
 
-  private static void logInApplySetValueOperation(String formattingString, Object... objects) {
+  private static void logInApplySetValueOperation(
+      Level loglevel, String formattingString, Object... objects) {
     // Finding the caller here is relatively expensive and shows up in profiling, so provide it
     // manually.
     logger.logp(
-        Level.INFO,
+        loglevel,
         "InvocationPolicyEnforcer",
         "applySetValueOperation",
         String.format(formattingString, objects));
@@ -515,85 +535,86 @@
 
   private static void applySetValueOperation(
       OptionsParser parser,
-      FlagPolicy flagPolicy,
+      FlagPolicyWithContext flagPolicy,
       OptionValueDescription valueDescription,
-      OptionDescription optionDescription)
+      Level loglevel)
       throws OptionsParsingException {
-    SetValue setValue = flagPolicy.getSetValue();
-    OptionDefinition optionDefinition = optionDescription.getOptionDefinition();
+    SetValue setValue = flagPolicy.policy.getSetValue();
+    OptionDefinition optionDefinition = flagPolicy.description.getOptionDefinition();
 
     // SetValue.flag_value must have at least 1 value.
     if (setValue.getFlagValueCount() == 0) {
       throw new OptionsParsingException(
           String.format(
-              "SetValue operation from invocation policy for flag '%s' does not have a value",
-              optionDefinition.getOptionName()));
+              "SetValue operation from invocation policy for %s does not have a value",
+              optionDefinition));
     }
 
     // Flag must allow multiple values if multiple values are specified by the policy.
     if (setValue.getFlagValueCount() > 1
-        && !optionDescription.getOptionDefinition().allowsMultiple()) {
+        && !flagPolicy.description.getOptionDefinition().allowsMultiple()) {
       throw new OptionsParsingException(
           String.format(
-              "SetValue operation from invocation policy sets multiple values for flag '%s' which "
+              "SetValue operation from invocation policy sets multiple values for %s which "
                   + "does not allow multiple values",
-              optionDefinition.getOptionName()));
+              optionDefinition));
     }
 
     if (setValue.getOverridable() && valueDescription != null) {
       // The user set the value for the flag but the flag policy is overridable, so keep the user's
       // value.
       logInApplySetValueOperation(
-          "Keeping value '%s' from source '%s' for flag '%s' "
-              + "because the invocation policy specifying the value(s) '%s' is overridable",
+          loglevel,
+          "Keeping value '%s' from source '%s' for %s because the invocation policy specifying "
+              + "the value(s) '%s' is overridable",
           valueDescription.getValue(),
           valueDescription.getSourceString(),
-          optionDefinition.getOptionName(),
+          optionDefinition,
           setValue.getFlagValueList());
     } else {
 
       if (!setValue.getAppend()) {
         // Clear the value in case the flag is a repeated flag so that values don't accumulate.
-        parser.clearValue(optionDescription.getOptionDefinition());
+        parser.clearValue(flagPolicy.description.getOptionDefinition());
       }
 
       // Set all the flag values from the policy.
       for (String flagValue : setValue.getFlagValueList()) {
         if (valueDescription == null) {
           logInApplySetValueOperation(
-              "Setting value for flag '%s' from invocation policy to '%s', overriding the "
-                  + "default value '%s'",
-              optionDefinition.getOptionName(), flagValue, optionDefinition.getDefaultValue());
+              loglevel,
+              "Setting value for %s from invocation policy to '%s', overriding the default value "
+                  + "'%s'",
+              optionDefinition,
+              flagValue,
+              optionDefinition.getDefaultValue());
         } else {
           logInApplySetValueOperation(
-              "Setting value for flag '%s' from invocation policy to '%s', overriding "
-                  + "value '%s' from '%s'",
-              optionDefinition.getOptionName(),
+              loglevel,
+              "Setting value for %s from invocation policy to '%s', overriding value '%s' from "
+                  + "'%s'",
+              optionDefinition,
               flagValue,
               valueDescription.getValue(),
               valueDescription.getSourceString());
         }
-        setFlagValue(parser, optionDefinition, flagValue);
+
+        parser.addOptionValueAtSpecificPriority(flagPolicy.origin, optionDefinition, flagValue);
       }
     }
   }
 
   private static void applyUseDefaultOperation(
-      OptionsParser parser, String policyType, OptionDefinition option)
+      OptionsParser parser, String policyType, OptionDefinition option, Level loglevel)
       throws OptionsParsingException {
     OptionValueDescription clearedValueDescription = parser.clearValue(option);
     if (clearedValueDescription != null) {
       // Log the removed value.
       String clearedFlagName = clearedValueDescription.getOptionDefinition().getOptionName();
-
-      OptionDescription desc =
-          parser.getOptionDescription(
-              clearedFlagName, OptionPriority.INVOCATION_POLICY, INVOCATION_POLICY_SOURCE);
-      Object clearedFlagDefaultValue = null;
-      if (desc != null) {
-        clearedFlagDefaultValue = desc.getOptionDefinition().getDefaultValue();
-      }
-      logger.info(
+      Object clearedFlagDefaultValue =
+          clearedValueDescription.getOptionDefinition().getDefaultValue();
+      logger.log(
+          loglevel,
           String.format(
               "Using default value '%s' for flag '%s' as specified by %s invocation policy, "
                   + "overriding original value '%s' from '%s'",
@@ -605,34 +626,40 @@
     }
   }
 
-  /**
-   * Checks the user's flag values against a filtering function.
-   */
+  /** Checks the user's flag values against a filtering function. */
   private abstract static class FilterValueOperation {
 
-    private static final FilterValueOperation ALLOW_VALUE_OPERATION =
-        new FilterValueOperation("Allow") {
-          @Override
-          boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
-            return convertedPolicyValues.contains(value);
-          }
-        };
+    private static final class AllowValueOperation extends FilterValueOperation {
+      AllowValueOperation(Level loglevel) {
+        super("Allow", loglevel);
+      }
 
-    private static final FilterValueOperation DISALLOW_VALUE_OPERATION =
-        new FilterValueOperation("Disallow") {
-          @Override
-          boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
-            // In a disallow operation, the values that the flag policy specifies are not allowed,
-            // so the value is allowed if the set of policy values does not contain the current
-            // flag value.
-            return !convertedPolicyValues.contains(value);
-          }
-        };
+      @Override
+      boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
+        return convertedPolicyValues.contains(value);
+      }
+    }
+
+    private static final class DisallowValueOperation extends FilterValueOperation {
+      DisallowValueOperation(Level loglevel) {
+        super("Disalllow", loglevel);
+      }
+
+      @Override
+      boolean isFlagValueAllowed(Set<Object> convertedPolicyValues, Object value) {
+        // In a disallow operation, the values that the flag policy specifies are not allowed,
+        // so the value is allowed if the set of policy values does not contain the current
+        // flag value.
+        return !convertedPolicyValues.contains(value);
+      }
+    }
 
     private final String policyType;
+    private final Level loglevel;
 
-    FilterValueOperation(String policyType) {
+    FilterValueOperation(String policyType, Level loglevel) {
       this.policyType = policyType;
+      this.loglevel = loglevel;
     }
 
     /**
@@ -646,6 +673,7 @@
 
     void apply(
         OptionsParser parser,
+        OptionInstanceOrigin origin,
         List<String> policyValues,
         String newValue,
         boolean useDefault,
@@ -684,11 +712,9 @@
         if (!defaultValueAllowed && useDefault) {
           throw new OptionsParsingException(
               String.format(
-                  "%sValues policy disallows the default value '%s' for flag '%s' but also "
-                      + "specifies to use the default value",
-                  policyType,
-                  optionDefinition.getDefaultValue(),
-                  optionDefinition.getOptionName()));
+                  "%sValues policy disallows the default value '%s' for %s but also specifies to "
+                      + "use the default value",
+                  policyType, optionDefinition.getDefaultValue(), optionDefinition));
         }
       }
 
@@ -698,10 +724,12 @@
         // the flag allowing multiple values, however, flags that allow multiple values cannot have
         // default values, and their value is always the empty list if they haven't been specified,
         // which is why new_default_value is not a repeated field.
-        checkDefaultValue(parser, optionDescription, policyValues, newValue, convertedPolicyValues);
+        checkDefaultValue(
+            parser, origin, optionDescription, policyValues, newValue, convertedPolicyValues);
       } else {
         checkUserValue(
             parser,
+            origin,
             optionDescription,
             valueDescription,
             policyValues,
@@ -713,6 +741,7 @@
 
     void checkDefaultValue(
         OptionsParser parser,
+        OptionInstanceOrigin origin,
         OptionDescription optionDescription,
         List<String> policyValues,
         String newValue,
@@ -723,26 +752,27 @@
       if (!isFlagValueAllowed(
           convertedPolicyValues, optionDescription.getOptionDefinition().getDefaultValue())) {
         if (newValue != null) {
-          // Use the default value from the policy.
-          logger.info(
+          // Use the default value from the policy, since the original default is not allowed
+          logger.log(
+              loglevel,
               String.format(
-                  "Overriding default value '%s' for flag '%s' with value '%s' specified by "
-                      + "invocation policy. %sed values are: %s",
+                  "Overriding default value '%s' for %s with value '%s' specified by invocation "
+                      + "policy. %sed values are: %s",
                   optionDefinition.getDefaultValue(),
-                  optionDefinition.getOptionName(),
+                  optionDefinition,
                   newValue,
                   policyType,
                   policyValues));
           parser.clearValue(optionDefinition);
-          setFlagValue(parser, optionDefinition, newValue);
+          parser.addOptionValueAtSpecificPriority(origin, optionDefinition, newValue);
         } else {
           // The operation disallows the default value, but doesn't supply a new value.
           throw new OptionsParsingException(
               String.format(
-                  "Default flag value '%s' for flag '%s' is not allowed by invocation policy, but "
+                  "Default flag value '%s' for %s is not allowed by invocation policy, but "
                       + "the policy does not provide a new value. %sed values are: %s",
                   optionDescription.getOptionDefinition().getDefaultValue(),
-                  optionDefinition.getOptionName(),
+                  optionDefinition,
                   policyType,
                   policyValues));
         }
@@ -751,6 +781,7 @@
 
     void checkUserValue(
         OptionsParser parser,
+        OptionInstanceOrigin origin,
         OptionDescription optionDescription,
         OptionValueDescription valueDescription,
         List<String> policyValues,
@@ -766,13 +797,13 @@
         for (Object value : optionValues) {
           if (!isFlagValueAllowed(convertedPolicyValues, value)) {
             if (useDefault) {
-              applyUseDefaultOperation(parser, policyType + "Values", option);
+              applyUseDefaultOperation(parser, policyType + "Values", option, loglevel);
             } else {
               throw new OptionsParsingException(
                   String.format(
-                      "Flag value '%s' for flag '%s' is not allowed by invocation policy. "
-                          + "%sed values are: %s",
-                      value, option.getOptionName(), policyType, policyValues));
+                      "Flag value '%s' for %s is not allowed by invocation policy. %sed values "
+                          + "are: %s",
+                      value, option, policyType, policyValues));
             }
           }
         }
@@ -781,38 +812,25 @@
 
         if (!isFlagValueAllowed(convertedPolicyValues, valueDescription.getValue())) {
           if (newValue != null) {
-            logger.info(
+            logger.log(
+                loglevel,
                 String.format(
-                    "Overriding disallowed value '%s' for flag '%s' with value '%s' "
+                    "Overriding disallowed value '%s' for %s with value '%s' "
                         + "specified by invocation policy. %sed values are: %s",
-                    valueDescription.getValue(),
-                    option.getOptionName(),
-                    newValue,
-                    policyType,
-                    policyValues));
+                    valueDescription.getValue(), option, newValue, policyType, policyValues));
             parser.clearValue(option);
-            setFlagValue(parser, option, newValue);
+            parser.addOptionValueAtSpecificPriority(origin, option, newValue);
           } else if (useDefault) {
-            applyUseDefaultOperation(parser, policyType + "Values", option);
+            applyUseDefaultOperation(parser, policyType + "Values", option, loglevel);
           } else {
             throw new OptionsParsingException(
                 String.format(
-                    "Flag value '%s' for flag '%s' is not allowed by invocation policy and the "
+                    "Flag value '%s' for %s is not allowed by invocation policy and the "
                         + "policy does not specify a new value. %sed values are: %s",
-                    valueDescription.getValue(), option.getOptionName(), policyType, policyValues));
+                    valueDescription.getValue(), option, policyType, policyValues));
           }
         }
       }
     }
   }
-
-  private static void setFlagValue(OptionsParser parser, OptionDefinition flag, String flagValue)
-      throws OptionsParsingException {
-
-    parser.parseWithSourceFunction(
-        OptionPriority.INVOCATION_POLICY,
-        INVOCATION_POLICY_SOURCE_FUNCTION,
-        ImmutableList.of(String.format("--%s=%s", flag.getOptionName(), flagValue)));
-  }
 }
-
diff --git a/java/com/google/devtools/common/options/LegacyParamsFilePreProcessor.java b/java/com/google/devtools/common/options/LegacyParamsFilePreProcessor.java
index 9e8eeb0..56a7d2c 100644
--- a/java/com/google/devtools/common/options/LegacyParamsFilePreProcessor.java
+++ b/java/com/google/devtools/common/options/LegacyParamsFilePreProcessor.java
@@ -27,7 +27,11 @@
  * A {@link ParamsFilePreProcessor} that processes a parameter file using a custom format. This
  * format assumes each parameter is separated by whitespace and allows arguments to use single and
  * double quotes and quote and whitespace escaping.
+ *
+ * <p><em>NOTE:</em> This class is deprecated; use either {@link ShellQuotedParamsFilePreProcessor}
+ * or {@link UnquotedParamsFilePreProcessor} depending on the format of the provided params file.
  */
+@Deprecated
 public class LegacyParamsFilePreProcessor extends ParamsFilePreProcessor {
 
   public LegacyParamsFilePreProcessor(FileSystem fs) {
diff --git a/java/com/google/devtools/common/options/Option.java b/java/com/google/devtools/common/options/Option.java
index 92436fd..45f320a 100644
--- a/java/com/google/devtools/common/options/Option.java
+++ b/java/com/google/devtools/common/options/Option.java
@@ -160,12 +160,17 @@
   Class<? extends ExpansionFunction> expansionFunction() default ExpansionFunction.class;
 
   /**
-   * If the option requires that additional options be implicitly appended, this field will contain
-   * the additional options. Implicit dependencies are parsed at the end of each {@link
-   * OptionsParser#parse} invocation, and override options specified in the same call. However, they
-   * can be overridden by options specified in a later call or by options with a higher priority.
+   * Additional options that need to be implicitly added for this option.
    *
-   * @see OptionPriority
+   * <p>Nothing guarantees that these options are not overridden by later or higher-priority values
+   * for the same options, so if this is truly a requirement, the user should check that the correct
+   * set of options is set.
+   *
+   * <p>These requirements are added for ANY mention of this option, so may not work as intended: in
+   * the case where a user is trying to explicitly turn off a flag (say, by setting a boolean flag
+   * to its default value of false), the mention will still turn on its requirements. For this
+   * reason, it is best not to use this feature, and rely on expansion flags if multi-flag groupings
+   * are needed.
    */
   String[] implicitRequirements() default {};
 
diff --git a/java/com/google/devtools/common/options/OptionDefinition.java b/java/com/google/devtools/common/options/OptionDefinition.java
index 40da044..1c01932 100644
--- a/java/com/google/devtools/common/options/OptionDefinition.java
+++ b/java/com/google/devtools/common/options/OptionDefinition.java
@@ -303,6 +303,11 @@
     return field.hashCode();
   }
 
+  @Override
+  public String toString() {
+    return String.format("option '--%s'", getOptionName());
+  }
+
   static final Comparator<OptionDefinition> BY_OPTION_NAME =
       Comparator.comparing(OptionDefinition::getOptionName);
 
diff --git a/java/com/google/devtools/common/options/OptionDocumentationCategory.java b/java/com/google/devtools/common/options/OptionDocumentationCategory.java
index 1f27046..dd5420c 100644
--- a/java/com/google/devtools/common/options/OptionDocumentationCategory.java
+++ b/java/com/google/devtools/common/options/OptionDocumentationCategory.java
@@ -54,6 +54,12 @@
   /** This option's primary purpose is to affect the verbosity, format or location of logging. */
   LOGGING,
 
+  /**
+   * This option affects how strictly Bazel enforces valid build inputs (rule definitions,
+   * flag combinations, etc).
+   */
+  INPUT_STRICTNESS,
+
   /** This option deals with how to go about executing the build. */
   EXECUTION_STRATEGY,
 
diff --git a/java/com/google/devtools/common/options/OptionEffectTag.java b/java/com/google/devtools/common/options/OptionEffectTag.java
index 136929a..500643f 100644
--- a/java/com/google/devtools/common/options/OptionEffectTag.java
+++ b/java/com/google/devtools/common/options/OptionEffectTag.java
@@ -139,8 +139,12 @@
 
   /**
    * This option is used to change command line arguments of one or more actions during the build.
+   *
+   * <p>Even though many options implicitly change command line arguments because they change
+   * configured target analysis, this setting is intended for options specifically meant for
+   * for that purpose.
    */
-  ACTION_OPTIONS(13),
+  ACTION_COMMAND_LINES(13),
 
   /** This option is used to change the testrunner environment of the build. */
   TEST_RUNNER(14);
diff --git a/java/com/google/devtools/common/options/OptionFilterDescriptions.java b/java/com/google/devtools/common/options/OptionFilterDescriptions.java
new file mode 100644
index 0000000..2a7999d
--- /dev/null
+++ b/java/com/google/devtools/common/options/OptionFilterDescriptions.java
@@ -0,0 +1,180 @@
+// Copyright 2017 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.common.options;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Provides descriptions of the options filters, for use in generated documentation and usage text.
+ */
+public class OptionFilterDescriptions {
+
+  /** The order that the categories should be listed in. */
+  static OptionDocumentationCategory[] documentationOrder = {
+      OptionDocumentationCategory.BAZEL_CLIENT_OPTIONS,
+      OptionDocumentationCategory.EXECUTION_STRATEGY,
+      OptionDocumentationCategory.TOOLCHAIN,
+      OptionDocumentationCategory.OUTPUT_SELECTION,
+      OptionDocumentationCategory.OUTPUT_PARAMETERS,
+      OptionDocumentationCategory.INPUT_STRICTNESS,
+      OptionDocumentationCategory.SIGNING,
+      OptionDocumentationCategory.TESTING,
+      OptionDocumentationCategory.QUERY,
+      OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION,
+      OptionDocumentationCategory.LOGGING,
+      OptionDocumentationCategory.GENERIC_INPUTS,
+      OptionDocumentationCategory.UNCATEGORIZED
+  };
+
+  static ImmutableMap<OptionDocumentationCategory, String> getOptionCategoriesEnumDescription(
+      String productName) {
+    ImmutableMap.Builder<OptionDocumentationCategory, String> optionCategoriesBuilder =
+        ImmutableMap.builder();
+    optionCategoriesBuilder
+        .put(
+            OptionDocumentationCategory.UNCATEGORIZED,
+            "Miscellaneous options, not otherwise categorized.")
+        .put( // Here for completeness, the help output should not include this option.
+            OptionDocumentationCategory.UNDOCUMENTED,
+            "This feature should not be documented, as it is not meant for general use")
+        .put(
+            OptionDocumentationCategory.BAZEL_CLIENT_OPTIONS,
+            "Options that appear before the command and are parsed by the client")
+        .put(
+            OptionDocumentationCategory.LOGGING,
+            "Options that affect the verbosity, format or location of logging")
+        .put(OptionDocumentationCategory.EXECUTION_STRATEGY, "Options that control build execution")
+        .put(
+            OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION,
+            "Options that trigger optimizations of the build time")
+        .put(
+            OptionDocumentationCategory.OUTPUT_SELECTION,
+            "Options that control the output of the command")
+        .put(
+            OptionDocumentationCategory.OUTPUT_PARAMETERS,
+            "Options that let the user configure the intended output, affecting its value, as "
+                + "opposed to its existence")
+        .put(
+            OptionDocumentationCategory.INPUT_STRICTNESS,
+            "Options that affect how strictly Bazel enforces valid build inputs (rule definitions, "
+                + " flag combinations, etc.)")
+        .put(
+            OptionDocumentationCategory.SIGNING,
+            "Options that affect the signing outputs of a build")
+        .put(
+            OptionDocumentationCategory.TESTING,
+            "Options that govern the behavior of the test environment or test runner")
+        .put(
+            OptionDocumentationCategory.TOOLCHAIN,
+            "Options that configure the toolchain used for action execution")
+        .put(OptionDocumentationCategory.QUERY, "Options relating to query output and semantics")
+        .put(
+            OptionDocumentationCategory.GENERIC_INPUTS,
+            "Options specifying or altering a generic input to a Bazel command that does not fall "
+                + "into other categories.");
+    return optionCategoriesBuilder.build();
+  }
+
+  public static ImmutableMap<OptionEffectTag, String> getOptionEffectTagDescription(
+      String productName) {
+    ImmutableMap.Builder<OptionEffectTag, String> effectTagDescriptionBuilder =
+        ImmutableMap.builder();
+    effectTagDescriptionBuilder
+        .put(OptionEffectTag.UNKNOWN, "This option has unknown, or undocumented, effect.")
+        .put(OptionEffectTag.NO_OP, "This option has literally no effect.")
+        .put(
+            OptionEffectTag.LOSES_INCREMENTAL_STATE,
+            "Changing the value of this option can cause significant loss of incremental "
+                + "state, which slows builds. State could be lost due to a server restart or to "
+                + "invalidation of a large part of the dependency graph.")
+        .put(
+            OptionEffectTag.CHANGES_INPUTS,
+            "This option actively changes the inputs that "
+                + productName
+                + " considers for the build, such as filesystem restrictions, repository versions, "
+                + "or other options.")
+        .put(
+            OptionEffectTag.AFFECTS_OUTPUTS,
+            "This option affects "
+                + productName
+                + "'s outputs. This tag is intentionally broad, can include transitive affects, "
+                + "and does not specify the type of output it affects.")
+        .put(
+            OptionEffectTag.BUILD_FILE_SEMANTICS,
+            "This option affects the semantics of BUILD or .bzl files.")
+        .put(
+            OptionEffectTag.BAZEL_INTERNAL_CONFIGURATION,
+            "This option affects settings of "
+                + productName
+                + "-internal machinery. This tag does not, on its own, mean that build artifacts "
+                + "are affected.")
+        .put(
+            OptionEffectTag.LOADING_AND_ANALYSIS,
+            "This option affects the loading and analysis of dependencies, and the building "
+                + "of the dependency graph.")
+        .put(
+            OptionEffectTag.EXECUTION,
+            "This option affects the execution phase, such as sandboxing or remote execution "
+                + "related options.")
+        .put(
+            OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS,
+            "This option triggers an optimization that may be machine specific and is not "
+                + "guaranteed to work on all machines. The optimization could include a tradeoff "
+                + "with other aspects of performance, such as memory or cpu cost.")
+        .put(
+            OptionEffectTag.EAGERNESS_TO_EXIT,
+            "This option changes how eagerly "
+                + productName
+                + " will exit from a failure, where a choice between continuing despite the "
+                + "failure and ending the invocation exists.")
+        .put(
+            OptionEffectTag.BAZEL_MONITORING,
+            "This option is used to monitor " + productName + "'s behavior and performance.")
+        .put(
+            OptionEffectTag.TERMINAL_OUTPUT,
+            "This option affects " + productName + "'s terminal output.")
+        .put(
+            OptionEffectTag.ACTION_COMMAND_LINES,
+            "This option changes the command line arguments of one or more build actions.")
+        .put(
+            OptionEffectTag.TEST_RUNNER,
+            "This option changes the testrunner environment of the build.");
+    return effectTagDescriptionBuilder.build();
+  }
+
+  public static ImmutableMap<OptionMetadataTag, String> getOptionMetadataTagDescription(
+      String productName) {
+    ImmutableMap.Builder<OptionMetadataTag, String> effectTagDescriptionBuilder =
+        ImmutableMap.builder();
+    effectTagDescriptionBuilder
+        .put(
+            OptionMetadataTag.EXPERIMENTAL,
+            "This option triggers an experimental feature with no guarantees of functionality.")
+        .put(
+            OptionMetadataTag.INCOMPATIBLE_CHANGE,
+            "This option triggers a breaking change. Use this option to test your migration "
+                + "readiness or get early access to the new feature")
+        .put(
+            OptionMetadataTag.DEPRECATED,
+            "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.HIDDEN, // Here for completeness, these options are UNDOCUMENTED.
+            "This option should not be used by a user, and should not be logged.")
+        .put(
+            OptionMetadataTag.INTERNAL, // Here for completeness, these options are UNDOCUMENTED.
+            "This option isn't even a option, and should not be logged.");
+    return effectTagDescriptionBuilder.build();
+  }
+}
diff --git a/java/com/google/devtools/common/options/OptionPriority.java b/java/com/google/devtools/common/options/OptionPriority.java
index a28f012..96c471e 100644
--- a/java/com/google/devtools/common/options/OptionPriority.java
+++ b/java/com/google/devtools/common/options/OptionPriority.java
@@ -13,50 +13,98 @@
 // limitations under the License.
 package com.google.devtools.common.options;
 
+import java.util.Objects;
+
 /**
- * The priority of option values, in order of increasing priority.
+ * The position of an option in the interpretation order. Options are interpreted using a
+ * last-option-wins system for single valued options, and are listed in that order for
+ * multiple-valued options.
  *
- * <p>In general, new values for options can only override values with a lower or
- * equal priority. Option values provided in annotations in an options class are
- * implicitly at the priority {@code DEFAULT}.
- *
- * <p>The ordering of the priorities is the source-code order. This is consistent
- * with the automatically generated {@code compareTo} method as specified by the
- * Java Language Specification. DO NOT change the source-code order of these
- * values, or you will break code that relies on the ordering.
+ * <p>The position of the option is in category order, and within the priority category in index
+ * order.
  */
-public enum OptionPriority {
+public class OptionPriority implements Comparable<OptionPriority> {
+  private final PriorityCategory priorityCategory;
+  private final int index;
+
+  private OptionPriority(PriorityCategory priorityCategory, int index) {
+    this.priorityCategory = priorityCategory;
+    this.index = index;
+  }
+
+  public static OptionPriority lowestOptionPriorityAtCategory(PriorityCategory category) {
+    return new OptionPriority(category, 0);
+  }
+
+  public static OptionPriority nextOptionPriority(OptionPriority priority) {
+    return new OptionPriority(priority.priorityCategory, priority.index + 1);
+  }
+
+  public PriorityCategory getPriorityCategory() {
+    return priorityCategory;
+  }
+
+  @Override
+  public int compareTo(OptionPriority o) {
+    if (priorityCategory.equals(o.priorityCategory)) {
+      return index - o.index;
+    }
+    return 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 false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(priorityCategory, index);
+  }
 
   /**
-   * The priority of values specified in the {@link Option} annotation. This
-   * should never be specified in calls to {@link OptionsParser#parse}.
+   * The priority of option values, in order of increasing priority.
+   *
+   * <p>In general, new values for options can only override values with a lower or equal priority.
+   * Option values provided in annotations in an options class are implicitly at the priority {@code
+   * DEFAULT}.
+   *
+   * <p>The ordering of the priorities is the source-code order. This is consistent with the
+   * automatically generated {@code compareTo} method as specified by the Java Language
+   * Specification. DO NOT change the source-code order of these values, or you will break code that
+   * relies on the ordering.
    */
-  DEFAULT,
+  public enum PriorityCategory {
 
-  /**
-   * Overrides default options at runtime, while still allowing the values to be
-   * overridden manually.
-   */
-  COMPUTED_DEFAULT,
+    /**
+     * The priority of values specified in the {@link Option} annotation. This should never be
+     * specified in calls to {@link OptionsParser#parse}.
+     */
+    DEFAULT,
 
-  /**
-   * For options coming from a configuration file or rc file.
-   */
-  RC_FILE,
+    /**
+     * Overrides default options at runtime, while still allowing the values to be overridden
+     * manually.
+     */
+    COMPUTED_DEFAULT,
 
-  /**
-   * For options coming from the command line.
-   */
-  COMMAND_LINE,
+    /** For options coming from a configuration file or rc file. */
+    RC_FILE,
 
-  /**
-   * For options coming from invocation policy.
-   */
-  INVOCATION_POLICY,
+    /** For options coming from the command line. */
+    COMMAND_LINE,
 
-  /**
-   * This priority can be used to unconditionally override any user-provided options.
-   * This should be used rarely and with caution!
-   */
-  SOFTWARE_REQUIREMENT;
+    /** For options coming from invocation policy. */
+    INVOCATION_POLICY,
+
+    /**
+     * This priority can be used to unconditionally override any user-provided options. This should
+     * be used rarely and with caution!
+     */
+    SOFTWARE_REQUIREMENT
+  }
 }
diff --git a/java/com/google/devtools/common/options/OptionValueDescription.java b/java/com/google/devtools/common/options/OptionValueDescription.java
index 0d81d49..0d31137 100644
--- a/java/com/google/devtools/common/options/OptionValueDescription.java
+++ b/java/com/google/devtools/common/options/OptionValueDescription.java
@@ -15,12 +15,15 @@
 package com.google.devtools.common.options;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
 import com.google.devtools.common.options.OptionsParser.ConstructionException;
-import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map.Entry;
 import java.util.stream.Collectors;
 
 /**
@@ -46,10 +49,29 @@
   /** Returns the source(s) of this option, if there were multiple, duplicates are removed. */
   public abstract String getSourceString();
 
-  abstract void addOptionInstance(
-      ParsedOptionDescription parsedOption,
-      List<String> warnings)
-      throws OptionsParsingException;
+  /**
+   * Add an instance of the option to this value. The various types of options are in charge of
+   * making sure that the value is correctly stored, with proper tracking of its priority and
+   * placement amongst other options.
+   *
+   * @return a bundle containing arguments that need to be parsed further.
+   */
+  abstract ExpansionBundle addOptionInstance(
+      ParsedOptionDescription parsedOption, List<String> warnings) throws OptionsParsingException;
+
+  /**
+   * Grouping of convenience for the options that expand to other options, to attach an
+   * option-appropriate source string along with the options that need to be parsed.
+   */
+  public static class ExpansionBundle {
+    List<String> expansionArgs;
+    String sourceOfExpansionArgs;
+
+    public ExpansionBundle(List<String> args, String source) {
+      expansionArgs = args;
+      sourceOfExpansionArgs = source;
+    }
+  }
 
   /**
    * For the given option, returns the correct type of OptionValueDescription, to which unparsed
@@ -58,11 +80,12 @@
    * <p>The categories of option types are non-overlapping, an invariant checked by the
    * OptionProcessor at compile time.
    */
-  public static OptionValueDescription createOptionValueDescription(OptionDefinition option) {
-    if (option.allowsMultiple()) {
+  public static OptionValueDescription createOptionValueDescription(
+      OptionDefinition option, OptionsData optionsData) {
+    if (option.isExpansionOption()) {
+      return new ExpansionOptionValueDescription(option, optionsData);
+    } else if (option.allowsMultiple()) {
       return new RepeatableOptionValueDescription(option);
-    } else if (option.isExpansionOption()) {
-      return new ExpansionOptionValueDescription(option);
     } else if (option.hasImplicitRequirements()) {
       return new OptionWithImplicitRequirementsValueDescription(option);
     } else if (option.isWrapperOption()) {
@@ -97,9 +120,7 @@
     }
 
     @Override
-    void addOptionInstance(
-        ParsedOptionDescription parsedOption,
-        List<String> warnings) {
+    ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) {
       throw new IllegalStateException(
           "Cannot add values to the default option value. Create a modifiable "
               + "OptionValueDescription using createOptionValueDescription() instead.");
@@ -138,15 +159,13 @@
 
     // Warnings should not end with a '.' because the internal reporter adds one automatically.
     @Override
-    void addOptionInstance(
-        ParsedOptionDescription parsedOption,
-        List<String> warnings)
+    ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
         throws OptionsParsingException {
       // This might be the first value, in that case, just store it!
       if (effectiveOptionInstance == null) {
         effectiveOptionInstance = parsedOption;
         effectiveValue = effectiveOptionInstance.getConvertedValue();
-        return;
+        return null;
       }
 
       // If there was another value, check whether the new one will override it, and if so,
@@ -160,57 +179,52 @@
         OptionDefinition optionThatExpandedToEffectiveValue =
             effectiveOptionInstance.getExpandedFrom();
 
-        // Output warnings:
-        if ((implicitDependent != null) && (optionThatDependsOnEffectiveValue != null)) {
-          if (!implicitDependent.equals(optionThatDependsOnEffectiveValue)) {
+        Object newValue = parsedOption.getConvertedValue();
+        // Output warnings if there is conflicting options set different values in a way that might
+        // not have been obvious to the user, such as through expansions and implicit requirements.
+        if (!effectiveValue.equals(newValue)) {
+          if ((implicitDependent != null) && (optionThatDependsOnEffectiveValue != null)) {
+            if (!implicitDependent.equals(optionThatDependsOnEffectiveValue)) {
+              warnings.add(
+                  String.format(
+                      "%s is implicitly defined by both %s and %s",
+                      optionDefinition, optionThatDependsOnEffectiveValue, implicitDependent));
+            }
+          } else if ((implicitDependent != null)
+              && parsedOption.getPriority().equals(effectiveOptionInstance.getPriority())) {
             warnings.add(
                 String.format(
-                    "Option '%s' is implicitly defined by both option '%s' and option '%s'",
-                    optionDefinition.getOptionName(),
-                    optionThatDependsOnEffectiveValue.getOptionName(),
-                    implicitDependent.getOptionName()));
+                    "%s is implicitly defined by %s; the implicitly set value "
+                        + "overrides the previous one",
+                    optionDefinition, implicitDependent));
+          } else if (optionThatDependsOnEffectiveValue != null) {
+            warnings.add(
+                String.format(
+                    "A new value for %s overrides a previous implicit setting of that "
+                        + "option by %s",
+                    optionDefinition, optionThatDependsOnEffectiveValue));
+          } else if ((parsedOption.getPriority().equals(effectiveOptionInstance.getPriority()))
+              && ((optionThatExpandedToEffectiveValue == null) && (expandedFrom != null))) {
+            // 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",
+                    expandedFrom,
+                    effectiveOptionInstance.getCommandLineForm(),
+                    parsedOption.getCommandLineForm()));
+          } else if ((optionThatExpandedToEffectiveValue != null) && (expandedFrom != null)) {
+            warnings.add(
+                String.format(
+                    "%s was expanded to from both %s and %s",
+                    optionDefinition, optionThatExpandedToEffectiveValue, expandedFrom));
           }
-        } else if ((implicitDependent != null)
-            && parsedOption.getPriority().equals(effectiveOptionInstance.getPriority())) {
-          warnings.add(
-              String.format(
-                  "Option '%s' is implicitly defined by option '%s'; the implicitly set value "
-                      + "overrides the previous one",
-                  optionDefinition.getOptionName(), implicitDependent.getOptionName()));
-        } else if (optionThatDependsOnEffectiveValue != null) {
-          warnings.add(
-              String.format(
-                  "A new value for option '%s' overrides a previous implicit setting of that "
-                      + "option by option '%s'",
-                  optionDefinition.getOptionName(),
-                  optionThatDependsOnEffectiveValue.getOptionName()));
-        } else if ((parsedOption.getPriority() == effectiveOptionInstance.getPriority())
-            && ((optionThatExpandedToEffectiveValue == null) && (expandedFrom != null))) {
-          // Create a warning if an expansion option overrides an explicit option:
-          warnings.add(
-              String.format(
-                  "The option '%s' was expanded and now overrides a previous explicitly specified "
-                      + "option '%s'",
-                  expandedFrom.getOptionName(), optionDefinition.getOptionName()));
-        } else if ((optionThatExpandedToEffectiveValue != null) && (expandedFrom != null)) {
-          warnings.add(
-              String.format(
-                  "The option '%s' was expanded to from both options '%s' and '%s'",
-                  optionDefinition.getOptionName(),
-                  optionThatExpandedToEffectiveValue.getOptionName(),
-                  expandedFrom.getOptionName()));
         }
 
         // Record the new value:
         effectiveOptionInstance = parsedOption;
-        effectiveValue = parsedOption.getConvertedValue();
-      } else {
-        // The new value does not override the old value, as it has lower priority.
-        warnings.add(
-            String.format(
-                "The lower priority option '%s' does not override the previous value '%s'",
-                parsedOption.getCommandLineForm(), effectiveOptionInstance.getCommandLineForm()));
+        effectiveValue = newValue;
       }
+      return null;
     }
 
     @VisibleForTesting
@@ -249,23 +263,19 @@
     @Override
     public List<Object> getValue() {
       // Sort the results by option priority and return them in a new list. The generic type of
-      // the list is not known at runtime, so we can't use it here. It was already checked in
-      // the constructor, so this is type-safe.
-      List<Object> result = new ArrayList<>();
-      for (OptionPriority priority : OptionPriority.values()) {
-        // If there is no mapping for this key, this check avoids object creation (because
-        // ListMultimap has to return a new object on get) and also an unnecessary addAll call.
-        if (optionValues.containsKey(priority)) {
-          result.addAll(optionValues.get(priority));
-        }
-      }
-      return result;
+      // the list is not known at runtime, so we can't use it here.
+      return optionValues
+          .asMap()
+          .entrySet()
+          .stream()
+          .sorted(Comparator.comparing(Entry::getKey))
+          .map(Entry::getValue)
+          .flatMap(Collection::stream)
+          .collect(Collectors.toList());
     }
 
     @Override
-    void addOptionInstance(
-        ParsedOptionDescription parsedOption,
-        List<String> warnings)
+    ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
         throws OptionsParsingException {
       // For repeatable options, we allow flags that take both single values and multiple values,
       // potentially collapsing them down.
@@ -277,6 +287,7 @@
       } else {
         optionValues.put(priority, convertedValue);
       }
+      return null;
     }
   }
 
@@ -286,9 +297,12 @@
    * in {@link Option#expansion()} and flags with an {@link Option#expansionFunction()}.
    */
   static class ExpansionOptionValueDescription extends OptionValueDescription {
+    private final List<String> expansion;
 
-    private ExpansionOptionValueDescription(OptionDefinition optionDefinition) {
+    private ExpansionOptionValueDescription(
+        OptionDefinition optionDefinition, OptionsData optionsData) {
       super(optionDefinition);
+      this.expansion = optionsData.getEvaluatedExpansion(optionDefinition);
       if (!optionDefinition.isExpansionOption()) {
         throw new ConstructionException(
             "Options without expansions can't be tracked using ExpansionOptionValueDescription");
@@ -306,11 +320,22 @@
     }
 
     @Override
-    void addOptionInstance(
-        ParsedOptionDescription parsedOption,
-        List<String> warnings) {
-      // TODO(b/65540004) Deal with expansion options here instead of in parse(), and track their
-      // link to the options they expanded to to.
+    ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) {
+      if (parsedOption.getUnconvertedValue() != null
+          && !parsedOption.getUnconvertedValue().isEmpty()) {
+        warnings.add(
+            String.format(
+                "%s is an expansion option. It does not accept values, and does not change its "
+                    + "expansion based on the value provided. Value '%s' will be ignored.",
+                optionDefinition, parsedOption.getUnconvertedValue()));
+      }
+
+      return new ExpansionBundle(
+          expansion,
+          (parsedOption.getSource() == null)
+              ? String.format("expanded from %s", optionDefinition)
+              : String.format(
+                  "expanded from %s (source %s)", optionDefinition, parsedOption.getSource()));
     }
   }
 
@@ -327,17 +352,34 @@
     }
 
     @Override
-    void addOptionInstance(
-        ParsedOptionDescription parsedOption,
-        List<String> warnings)
+    ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
         throws OptionsParsingException {
       // This is a valued flag, its value is handled the same way as a normal
-      // SingleOptionValueDescription.
-      super.addOptionInstance(parsedOption, warnings);
+      // SingleOptionValueDescription. (We check at compile time that these flags aren't
+      // "allowMultiple")
+      ExpansionBundle superExpansion = super.addOptionInstance(parsedOption, warnings);
+      Preconditions.checkArgument(
+          superExpansion == null, "SingleOptionValueDescription should not expand to anything.");
+      if (parsedOption.getConvertedValue().equals(optionDefinition.getDefaultValue())) {
+        warnings.add(
+            String.format(
+                "%s sets %s to its default value. Since this option has implicit requirements that "
+                    + "are set whenever the option is explicitly provided, regardless of the "
+                    + "value, this will behave differently than letting a default be a default. "
+                    + "Specifically, this options expands to {%s}.",
+                parsedOption.getCommandLineForm(),
+                optionDefinition,
+                String.join(" ", optionDefinition.getImplicitRequirements())));
+      }
 
       // Now deal with the implicit requirements.
-      // TODO(b/65540004) Deal with options with implicit requirements here instead of in parse(),
-      // and track their link to the options they implicitly expanded to to.
+      return new ExpansionBundle(
+          ImmutableList.copyOf(optionDefinition.getImplicitRequirements()),
+          (parsedOption.getSource() == null)
+              ? String.format("implicit requirement of %s", optionDefinition)
+              : String.format(
+                  "implicit requirement of %s (source %s)",
+                  optionDefinition, parsedOption.getSource()));
     }
   }
 
@@ -359,12 +401,22 @@
     }
 
     @Override
-    void addOptionInstance(
-        ParsedOptionDescription parsedOption,
-        List<String> warnings)
+    ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
         throws OptionsParsingException {
-      // TODO(b/65540004) Deal with options with implicit requirements here instead of in parse(),
-      // and track their link to the options they implicitly expanded to to.
+      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()));
     }
   }
 }
diff --git a/java/com/google/devtools/common/options/Options.java b/java/com/google/devtools/common/options/Options.java
index b636c09..0783480 100644
--- a/java/com/google/devtools/common/options/Options.java
+++ b/java/com/google/devtools/common/options/Options.java
@@ -51,7 +51,7 @@
   public static <O extends OptionsBase> Options<O> parse(Class<O> optionsClass, String... args)
       throws OptionsParsingException {
     OptionsParser parser = OptionsParser.newOptionsParser(optionsClass);
-    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args));
+    parser.parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList(args));
     List<String> remainingArgs = parser.getResidue();
     return new Options<>(parser.getOptions(optionsClass), remainingArgs.toArray(new String[0]));
   }
diff --git a/java/com/google/devtools/common/options/OptionsData.java b/java/com/google/devtools/common/options/OptionsData.java
index 5b9a436..63cac24 100644
--- a/java/com/google/devtools/common/options/OptionsData.java
+++ b/java/com/google/devtools/common/options/OptionsData.java
@@ -14,14 +14,12 @@
 
 package com.google.devtools.common.options;
 
-import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Modifier;
 import java.util.Collection;
 import java.util.Map;
-import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
 /**
@@ -32,87 +30,26 @@
 @Immutable
 final class OptionsData extends IsolatedOptionsData {
 
-  /**
-   * Keeps track of all the information needed to calculate expansion flags, whether they come from
-   * a static list or a @{link ExpansionFunction} object.
-   */
-  static class ExpansionData {
-    private final ImmutableList<String> staticExpansion;
-    @Nullable private final ExpansionFunction dynamicExpansions;
-
-    ExpansionData(ImmutableList<String> staticExpansion) {
-      Preconditions.checkArgument(staticExpansion != null);
-      this.staticExpansion = staticExpansion;
-      this.dynamicExpansions = null;
-    }
-
-    ExpansionData(ExpansionFunction dynamicExpansions) {
-      Preconditions.checkArgument(dynamicExpansions != null);
-      this.staticExpansion = EMPTY_EXPANSION;
-      this.dynamicExpansions = dynamicExpansions;
-    }
-
-    ImmutableList<String> getExpansion(ExpansionContext context) throws OptionsParsingException {
-      Preconditions.checkArgument(context != null);
-      if (dynamicExpansions != null) {
-        ImmutableList<String> result = dynamicExpansions.getExpansion(context);
-        if (result == null) {
-          String valueString =
-              context.getUnparsedValue() != null ? context.getUnparsedValue() : "(null)";
-          String name = context.getOptionDefinition().getOptionName();
-          throw new OptionsParsingException(
-              "Error expanding option '"
-                  + name
-                  + "': no expansions defined for value: "
-                  + valueString,
-              name);
-        }
-        return result;
-      } else {
-        return staticExpansion;
-      }
-    }
-
-    boolean isEmpty() {
-      return staticExpansion.isEmpty() && (dynamicExpansions == null);
-    }
-  }
-
-  /**
-   * Mapping from each Option-annotated field with expansion information to the {@link
-   * ExpansionData} needed to caclulate it.
-   */
-  private final ImmutableMap<OptionDefinition, ExpansionData> expansionDataForFields;
+  /** Mapping from each option to the (unparsed) options it expands to, if any. */
+  private final ImmutableMap<OptionDefinition, ImmutableList<String>> evaluatedExpansions;
 
   /** Construct {@link OptionsData} by extending an {@link IsolatedOptionsData} with new info. */
   private OptionsData(
-      IsolatedOptionsData base, Map<OptionDefinition, ExpansionData> expansionDataForFields) {
+      IsolatedOptionsData base, Map<OptionDefinition, ImmutableList<String>> evaluatedExpansions) {
     super(base);
-    this.expansionDataForFields = ImmutableMap.copyOf(expansionDataForFields);
+    this.evaluatedExpansions = ImmutableMap.copyOf(evaluatedExpansions);
   }
 
   private static final ImmutableList<String> EMPTY_EXPANSION = ImmutableList.<String>of();
-  private static final ExpansionData EMPTY_EXPANSION_DATA = new ExpansionData(EMPTY_EXPANSION);
 
   /**
    * Returns the expansion of an options field, regardless of whether it was defined using {@link
    * Option#expansion} or {@link Option#expansionFunction}. If the field is not an expansion option,
    * returns an empty array.
    */
-  public ImmutableList<String> getEvaluatedExpansion(
-      OptionDefinition optionDefinition, @Nullable String unparsedValue)
-      throws OptionsParsingException {
-    ExpansionData expansionData = expansionDataForFields.get(optionDefinition);
-    if (expansionData == null) {
-      return EMPTY_EXPANSION;
-    }
-
-    return expansionData.getExpansion(new ExpansionContext(this, optionDefinition, unparsedValue));
-  }
-
-  ExpansionData getExpansionDataForField(OptionDefinition optionDefinition) {
-    ExpansionData result = expansionDataForFields.get(optionDefinition);
-    return result != null ? result : EMPTY_EXPANSION_DATA;
+  public ImmutableList<String> getEvaluatedExpansion(OptionDefinition optionDefinition) {
+    ImmutableList<String> result = evaluatedExpansions.get(optionDefinition);
+    return result != null ? result : EMPTY_EXPANSION;
   }
 
   /**
@@ -126,8 +63,8 @@
     IsolatedOptionsData isolatedData = IsolatedOptionsData.from(classes);
 
     // All that's left is to compute expansions.
-    ImmutableMap.Builder<OptionDefinition, ExpansionData> expansionDataBuilder =
-        ImmutableMap.<OptionDefinition, ExpansionData>builder();
+    ImmutableMap.Builder<OptionDefinition, ImmutableList<String>> evaluatedExpansionsBuilder =
+        ImmutableMap.builder();
     for (Map.Entry<String, OptionDefinition> entry : isolatedData.getAllOptionDefinitions()) {
       OptionDefinition optionDefinition = entry.getValue();
       // Determine either the hard-coded expansion, or the ExpansionFunction class. The
@@ -136,8 +73,7 @@
       Class<? extends ExpansionFunction> expansionFunctionClass =
           optionDefinition.getExpansionFunction();
       if (constExpansion.length > 0) {
-        expansionDataBuilder.put(
-            optionDefinition, new ExpansionData(ImmutableList.copyOf(constExpansion)));
+        evaluatedExpansionsBuilder.put(optionDefinition, ImmutableList.copyOf(constExpansion));
       } else if (optionDefinition.usesExpansionFunction()) {
         if (Modifier.isAbstract(expansionFunctionClass.getModifiers())) {
           throw new AssertionError(
@@ -153,25 +89,10 @@
           // time it is used.
           throw new AssertionError(e);
         }
-
-        ImmutableList<String> staticExpansion;
-        try {
-          staticExpansion =
-              instance.getExpansion(new ExpansionContext(isolatedData, optionDefinition, null));
-          Preconditions.checkState(
-              staticExpansion != null,
-              "Error calling expansion function for option: %s",
-              optionDefinition.getOptionName());
-          expansionDataBuilder.put(optionDefinition, new ExpansionData(staticExpansion));
-        } catch (ExpansionNeedsValueException e) {
-          // This expansion function needs data that isn't available yet. Save the instance and call
-          // it later.
-          expansionDataBuilder.put(optionDefinition, new ExpansionData(instance));
-        } catch (OptionsParsingException e) {
-          throw new IllegalStateException("Error expanding void expansion function: ", e);
-        }
+        ImmutableList<String> expansion = instance.getExpansion(isolatedData);
+        evaluatedExpansionsBuilder.put(optionDefinition, expansion);
       }
     }
-    return new OptionsData(isolatedData, expansionDataBuilder.build());
+    return new OptionsData(isolatedData, evaluatedExpansionsBuilder.build());
   }
 }
diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java
index 28c2206..f84ee47 100644
--- a/java/com/google/devtools/common/options/OptionsParser.java
+++ b/java/com/google/devtools/common/options/OptionsParser.java
@@ -17,13 +17,14 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Throwables;
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
 import com.google.common.escape.Escaper;
 import com.google.devtools.common.options.OptionDefinition.NotAnOptionException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
-import java.nio.file.FileSystem;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -32,12 +33,12 @@
 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;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
-import javax.annotation.Nullable;
 
 /**
  * A parser for options. Typical use case in a main method:
@@ -53,9 +54,10 @@
  * <p>FooOptions and BarOptions would be options specification classes, derived from OptionsBase,
  * that contain fields annotated with @Option(...).
  *
- * <p>Alternatively, rather than calling {@link #parseAndExitUponError(OptionPriority, String,
- * String[])}, client code may call {@link #parse(OptionPriority,String,List)}, and handle parser
- * exceptions usage messages themselves.
+ * <p>Alternatively, rather than calling {@link
+ * #parseAndExitUponError(OptionPriority.PriorityCategory, String, String[])}, client code may call
+ * {@link #parse(OptionPriority.PriorityCategory,String,List)}, and handle parser exceptions usage
+ * messages themselves.
  *
  * <p>This options parsing implementation has (at least) one design flaw. It allows both '--foo=baz'
  * and '--foo baz' for all options except void, boolean and tristate options. For these, the 'baz'
@@ -150,11 +152,9 @@
     return newOptionsParser(ImmutableList.<Class<? extends OptionsBase>>of(class1));
   }
 
-  /**
-   * @see #newOptionsParser(Iterable)
-   */
-  public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1,
-                                               Class<? extends OptionsBase> class2)
+  /** @see #newOptionsParser(Iterable) */
+  public static OptionsParser newOptionsParser(
+      Class<? extends OptionsBase> class1, Class<? extends OptionsBase> class2)
       throws ConstructionException {
     return newOptionsParser(ImmutableList.of(class1, class2));
   }
@@ -200,11 +200,6 @@
     this.impl.setAllowSingleDashLongOptions(allowSingleDashLongOptions);
   }
 
-  /** Enables the Parser to handle params files located inside the provided {@link FileSystem}. */
-  public void enableParamsFileSupport(FileSystem fs) {
-    enableParamsFileSupport(new LegacyParamsFilePreProcessor(fs));
-  }
-
   /**
    * Enables the Parser to handle params files using the provided {@link ParamsFilePreProcessor}.
    */
@@ -213,18 +208,21 @@
   }
 
   public void parseAndExitUponError(String[] args) {
-    parseAndExitUponError(OptionPriority.COMMAND_LINE, "unknown", args);
+    parseAndExitUponError(OptionPriority.PriorityCategory.COMMAND_LINE, "unknown", args);
   }
 
   /**
-   * A convenience function for use in main methods. Parses the command line
-   * parameters, and exits upon error. Also, prints out the usage message
-   * if "--help" appears anywhere within {@code args}.
+   * A convenience function for use in main methods. Parses the command line parameters, and exits
+   * upon error. Also, prints out the usage message if "--help" appears anywhere within {@code
+   * args}.
    */
-  public void parseAndExitUponError(OptionPriority priority, String source, String[] args) {
+  public void parseAndExitUponError(
+      OptionPriority.PriorityCategory priority, String source, String[] args) {
     for (String arg : args) {
       if (arg.equals("--help")) {
-        System.out.println(describeOptions(ImmutableMap.of(), HelpVerbosity.LONG));
+        System.out.println(
+            describeOptionsWithDeprecatedCategories(ImmutableMap.of(), HelpVerbosity.LONG));
+
         System.exit(0);
       }
     }
@@ -239,36 +237,41 @@
 
   /** The metadata about an option, in the context of this options parser. */
   public static final class OptionDescription {
-
     private final OptionDefinition optionDefinition;
-    private final OptionsData.ExpansionData expansionData;
-    private final ImmutableList<ParsedOptionDescription> implicitRequirements;
+    private final ImmutableList<String> evaluatedExpansion;
 
-    OptionDescription(
-        OptionDefinition definition,
-        OptionsData.ExpansionData expansionData,
-        ImmutableList<ParsedOptionDescription> implicitRequirements) {
+    OptionDescription(OptionDefinition definition, OptionsData optionsData) {
       this.optionDefinition = definition;
-      this.expansionData = expansionData;
-      this.implicitRequirements = implicitRequirements;
+      this.evaluatedExpansion = optionsData.getEvaluatedExpansion(optionDefinition);
     }
 
     public OptionDefinition getOptionDefinition() {
       return optionDefinition;
     }
 
-    public ImmutableList<ParsedOptionDescription> getImplicitRequirements() {
-      return implicitRequirements;
-    }
-
     public boolean isExpansion() {
-      return !expansionData.isEmpty();
+      return optionDefinition.isExpansionOption();
     }
 
     /** Return a list of flags that this option expands to. */
-    public ImmutableList<String> getExpansion(ExpansionContext context)
-        throws OptionsParsingException {
-      return expansionData.getExpansion(context);
+    public ImmutableList<String> getExpansion() throws OptionsParsingException {
+      return evaluatedExpansion;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof OptionDescription) {
+        OptionDescription other = (OptionDescription) obj;
+        // Check that the option is the same, with the same expansion.
+        return other.optionDefinition.equals(optionDefinition)
+            && other.evaluatedExpansion.equals(evaluatedExpansion);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return optionDefinition.hashCode() + evaluatedExpansion.hashCode();
     }
   }
 
@@ -283,6 +286,78 @@
    * Returns a description of all the options this parser can digest. In addition to {@link Option}
    * annotations, this method also interprets {@link OptionsUsage} annotations which give an
    * intuitive short description for the options. Options of the same category (see {@link
+   * OptionDocumentationCategory}) will be grouped together.
+   *
+   * @param productName the name of this product (blaze, bazel)
+   * @param helpVerbosity if {@code long}, the options will be described verbosely, including their
+   *     types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if
+   *     {@code short}, the options are just enumerated.
+   */
+  public String describeOptions(String productName, HelpVerbosity helpVerbosity) {
+    StringBuilder desc = new StringBuilder();
+    LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory =
+        getOptionsSortedByCategory();
+    ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions =
+        OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName);
+    for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
+        optionsByCategory.entrySet()) {
+      String categoryDescription = optionCategoryDescriptions.get(e.getKey());
+      List<OptionDefinition> categorizedOptionList = e.getValue();
+
+      // Describe the category if we're going to end up using it at all.
+      if (!categorizedOptionList.isEmpty()) {
+        desc.append("\n").append(categoryDescription).append(":\n");
+      }
+      // Describe the options in this category.
+      for (OptionDefinition optionDef : categorizedOptionList) {
+        OptionsUsage.getUsage(optionDef, desc, helpVerbosity, impl.getOptionsData(), true);
+      }
+    }
+
+    return desc.toString().trim();
+  }
+
+  /**
+   * @return all documented options loaded in this parser, grouped by categories in display order.
+   */
+  private LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>>
+      getOptionsSortedByCategory() {
+    OptionsData data = impl.getOptionsData();
+    if (data.getOptionsClasses().isEmpty()) {
+      return new LinkedHashMap<>();
+    }
+
+    // Get the documented options grouped by category.
+    ListMultimap<OptionDocumentationCategory, OptionDefinition> optionsByCategories =
+        ArrayListMultimap.create();
+    for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) {
+      for (OptionDefinition optionDefinition :
+          OptionsData.getAllOptionDefinitionsForClass(optionsClass)) {
+        // Only track documented options.
+        if (optionDefinition.getDocumentationCategory()
+            != OptionDocumentationCategory.UNDOCUMENTED) {
+          optionsByCategories.put(optionDefinition.getDocumentationCategory(), optionDefinition);
+        }
+      }
+    }
+
+    // Put the categories into display order and sort the options in each category.
+    LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> sortedCategoriesToOptions =
+        new LinkedHashMap<>(OptionFilterDescriptions.documentationOrder.length, 1);
+    for (OptionDocumentationCategory category : OptionFilterDescriptions.documentationOrder) {
+      List<OptionDefinition> optionList = optionsByCategories.get(category);
+      if (optionList != null) {
+        optionList.sort(OptionDefinition.BY_OPTION_NAME);
+        sortedCategoriesToOptions.put(category, optionList);
+      }
+    }
+    return sortedCategoriesToOptions;
+  }
+
+  /**
+   * Returns a description of all the options this parser can digest. In addition to {@link Option}
+   * annotations, this method also interprets {@link OptionsUsage} annotations which give an
+   * intuitive short description for the options. Options of the same category (see {@link
    * Option#category}) will be grouped together.
    *
    * @param categoryDescriptions a mapping from category names to category descriptions.
@@ -291,7 +366,8 @@
    *     types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if
    *     {@code short}, the options are just enumerated.
    */
-  public String describeOptions(
+  @Deprecated
+  public String describeOptionsWithDeprecatedCategories(
       Map<String, String> categoryDescriptions, HelpVerbosity helpVerbosity) {
     OptionsData data = impl.getOptionsData();
     StringBuilder desc = new StringBuilder();
@@ -318,7 +394,8 @@
 
         if (optionDefinition.getDocumentationCategory()
             != OptionDocumentationCategory.UNDOCUMENTED) {
-          OptionsUsage.getUsage(optionDefinition, desc, helpVerbosity, impl.getOptionsData());
+          OptionsUsage.getUsage(
+              optionDefinition, desc, helpVerbosity, impl.getOptionsData(), false);
         }
       }
     }
@@ -326,17 +403,17 @@
   }
 
   /**
-   * Returns a description of all the options this parser can digest.
-   * In addition to {@link Option} annotations, this method also
-   * interprets {@link OptionsUsage} annotations which give an intuitive short
-   * description for the options.
+   * Returns a description of all the options this parser can digest. In addition to {@link Option}
+   * annotations, this method also interprets {@link OptionsUsage} annotations which give an
+   * intuitive short description for the options.
    *
-   * @param categoryDescriptions a mapping from category names to category
-   *   descriptions.  Options of the same category (see {@link
-   *   Option#category}) will be grouped together, preceded by the description
-   *   of the category.
+   * @param categoryDescriptions a mapping from category names to category descriptions. Options of
+   *     the same category (see {@link Option#category}) will be grouped together, preceded by the
+   *     description of the category.
    */
-  public String describeOptionsHtml(Map<String, String> categoryDescriptions, Escaper escaper) {
+  @Deprecated
+  public String describeOptionsHtmlWithDeprecatedCategories(
+      Map<String, String> categoryDescriptions, Escaper escaper) {
     OptionsData data = impl.getOptionsData();
     StringBuilder desc = new StringBuilder();
     if (!data.getOptionsClasses().isEmpty()) {
@@ -366,7 +443,7 @@
 
         if (optionDefinition.getDocumentationCategory()
             != OptionDocumentationCategory.UNDOCUMENTED) {
-          OptionsUsage.getUsageHtml(optionDefinition, desc, escaper, impl.getOptionsData());
+          OptionsUsage.getUsageHtml(optionDefinition, desc, escaper, impl.getOptionsData(), false);
         }
       }
       desc.append("</dl>\n");
@@ -375,6 +452,37 @@
   }
 
   /**
+   * Returns a description of all the options this parser can digest. In addition to {@link Option}
+   * annotations, this method also interprets {@link OptionsUsage} annotations which give an
+   * intuitive short description for the options.
+   */
+  public String describeOptionsHtml(Escaper escaper, String productName) {
+    StringBuilder desc = new StringBuilder();
+    LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory =
+        getOptionsSortedByCategory();
+    ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions =
+        OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName);
+
+    for (Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
+        optionsByCategory.entrySet()) {
+      desc.append("<dl>");
+      String categoryDescription = optionCategoryDescriptions.get(e.getKey());
+      List<OptionDefinition> categorizedOptionsList = e.getValue();
+
+      // Describe the category if we're going to end up using it at all.
+      if (!categorizedOptionsList.isEmpty()) {
+        desc.append(escaper.escape(categoryDescription)).append(":\n");
+      }
+      // Describe the options in this category.
+      for (OptionDefinition optionDef : categorizedOptionsList) {
+        OptionsUsage.getUsageHtml(optionDef, desc, escaper, impl.getOptionsData(), true);
+      }
+      desc.append("</dl>\n");
+    }
+    return desc.toString();
+  }
+
+  /**
    * Returns a string listing the possible flag completion for this command along with the command
    * completion if any. See {@link OptionsUsage#getCompletion(OptionDefinition, StringBuilder)} for
    * more details on the format for the flag completion.
@@ -412,29 +520,30 @@
    * @return The {@link OptionDescription} for the option, or null if there is no option by the
    *     given name.
    */
-  OptionDescription getOptionDescription(String name, OptionPriority priority, String source)
-      throws OptionsParsingException {
-    return impl.getOptionDescription(name, priority, source);
+  OptionDescription getOptionDescription(String name) throws OptionsParsingException {
+    return impl.getOptionDescription(name);
   }
 
   /**
-   * Returns a description of the options values that get expanded from this option with the given
-   * value.
+   * Returns the parsed options that get expanded from this option, whether it expands due to an
+   * implicit requirement or expansion.
    *
-   * @return The {@link com.google.devtools.common.options.OptionValueDescription>} for the option,
-   *     or null if there is no option by the given name.
+   * @param expansionOption the option that might need to be expanded. If this option does not
+   *     expand to other options, the empty list will be returned.
+   * @param originOfExpansionOption the origin of the option that's being expanded. This function
+   *     will take care of adjusting the source messages as necessary.
    */
-  ImmutableList<ParsedOptionDescription> getExpansionOptionValueDescriptions(
-      OptionDefinition option, @Nullable String optionValue, OptionPriority priority, String source)
+  ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions(
+      OptionDefinition expansionOption, OptionInstanceOrigin originOfExpansionOption)
       throws OptionsParsingException {
-    return impl.getExpansionOptionValueDescriptions(option, optionValue, priority, source);
+    return impl.getExpansionValueDescriptions(expansionOption, originOfExpansionOption);
   }
 
   /**
    * Returns a description of the option value set by the last previous call to {@link
-   * #parse(OptionPriority, String, List)} that successfully set the given option. If the option is
-   * of type {@link List}, the description will correspond to any one of the calls, but not
-   * necessarily the last.
+   * #parse(OptionPriority.PriorityCategory, String, List)} that successfully set the given option.
+   * If the option is of type {@link List}, the description will correspond to any one of the calls,
+   * but not necessarily the last.
    *
    * @return The {@link com.google.devtools.common.options.OptionValueDescription} for the option,
    *     or null if the value has not been set.
@@ -445,48 +554,64 @@
   }
 
   /**
-   * A convenience method, equivalent to
-   * {@code parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args))}.
+   * A convenience method, equivalent to {@code parse(PriorityCategory.COMMAND_LINE, null,
+   * Arrays.asList(args))}.
    */
   public void parse(String... args) throws OptionsParsingException {
-    parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args));
+    parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList(args));
   }
 
   /**
-   * A convenience method, equivalent to
-   * {@code parse(OptionPriority.COMMAND_LINE, null, args)}.
+   * A convenience method, equivalent to {@code parse(PriorityCategory.COMMAND_LINE, null, args)}.
    */
   public void parse(List<String> args) throws OptionsParsingException {
-    parse(OptionPriority.COMMAND_LINE, null, args);
+    parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, args);
   }
 
   /**
-   * Parses {@code args}, using the classes registered with this parser.
-   * {@link #getOptions(Class)} and {@link #getResidue()} return the results.
-   * May be called multiple times; later options override existing ones if they
-   * have equal or higher priority. The source of options is a free-form string
-   * that can be used for debugging. Strings that cannot be parsed as options
-   * accumulates as residue, if this parser allows it.
+   * Parses {@code args}, using the classes registered with this parser, at the given priority.
    *
-   * @see OptionPriority
+   * <p>May be called multiple times; later options override existing ones if they have equal or
+   * higher priority. Strings that cannot be parsed as options are accumulated as residue, if this
+   * parser allows it.
+   *
+   * <p>{@link #getOptions(Class)} and {@link #getResidue()} will return the results.
+   *
+   * @param priority the priority at which to parse these options. Within this priority category,
+   *     each option will be given an index to track its position. If parse() has already been
+   *     called at this priority, the indexing will continue where it left off, to keep ordering.
+   * @param source the source to track for each option parsed.
+   * @param args the arg list to parse. Each element might be an option, a value linked to an
+   *     option, or residue.
    */
-  public void parse(OptionPriority priority, String source,
-      List<String> args) throws OptionsParsingException {
+  public void parse(OptionPriority.PriorityCategory priority, String source, List<String> args)
+      throws OptionsParsingException {
     parseWithSourceFunction(priority, o -> source, args);
   }
 
   /**
-   * Parses {@code args}, using the classes registered with this parser. {@link #getOptions(Class)}
-   * and {@link #getResidue()} return the results. May be called multiple times; later options
-   * override existing ones if they have equal or higher priority. The source of options is given as
-   * a function that maps option names to the source of the option. Strings that cannot be parsed as
-   * options accumulates as* residue, if this parser allows it.
+   * Parses {@code args}, using the classes registered with this parser, at the given priority.
+   *
+   * <p>May be called multiple times; later options override existing ones if they have equal or
+   * higher priority. Strings that cannot be parsed as options are accumulated as residue, if this
+   * parser allows it.
+   *
+   * <p>{@link #getOptions(Class)} and {@link #getResidue()} will return the results.
+   *
+   * @param priority the priority at which to parse these options. Within this priority category,
+   *     each option will be given an index to track its position. If parse() has already been
+   *     called at this priority, the indexing will continue where it left off, to keep ordering.
+   * @param sourceFunction a function that maps option names to the source of the option.
+   * @param args the arg list to parse. Each element might be an option, a value linked to an
+   *     option, or residue.
    */
   public void parseWithSourceFunction(
-      OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args)
+      OptionPriority.PriorityCategory priority,
+      Function<OptionDefinition, String> sourceFunction,
+      List<String> args)
       throws OptionsParsingException {
     Preconditions.checkNotNull(priority);
-    Preconditions.checkArgument(priority != OptionPriority.DEFAULT);
+    Preconditions.checkArgument(priority != OptionPriority.PriorityCategory.DEFAULT);
     residue.addAll(impl.parse(priority, sourceFunction, args));
     if (!allowResidue && !residue.isEmpty()) {
       String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue);
@@ -495,6 +620,20 @@
   }
 
   /**
+   * @param origin the origin of this option instance, it includes the priority of the value. If
+   *     other values have already been or will be parsed at a higher priority, they might override
+   *     the provided value. If this option already has a value at this priority, this value will
+   *     have precedence, but this should be avoided, as it breaks order tracking.
+   * @param option the option to add the value for.
+   * @param value the value to add at the given priority.
+   */
+  void addOptionValueAtSpecificPriority(
+      OptionInstanceOrigin origin, OptionDefinition option, String value)
+      throws OptionsParsingException {
+    impl.addOptionValueAtSpecificPriority(origin, option, value);
+  }
+
+  /**
    * Clears the given option.
    *
    * <p>This will not affect options objects that have already been retrieved from this parser
@@ -693,4 +832,3 @@
             + "}");
   }
 }
-
diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java
index 176d51e..b543328 100644
--- a/java/com/google/devtools/common/options/OptionsParserImpl.java
+++ b/java/com/google/devtools/common/options/OptionsParserImpl.java
@@ -23,13 +23,13 @@
 import com.google.common.collect.Iterators;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Multimap;
+import com.google.devtools.common.options.OptionPriority.PriorityCategory;
+import com.google.devtools.common.options.OptionValueDescription.ExpansionBundle;
 import com.google.devtools.common.options.OptionsParser.OptionDescription;
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
@@ -54,7 +54,8 @@
    *   OptionDefinition("--port") -> 80
    * </pre>
    *
-   * This map is modified by repeated calls to {@link #parse(OptionPriority,Function,List)}.
+   * This map is modified by repeated calls to {@link #parse(OptionPriority.PriorityCategory,
+   * Function,List)}.
    */
   private final Map<OptionDefinition, OptionValueDescription> optionValues = new HashMap<>();
 
@@ -90,10 +91,9 @@
         }
       };
 
-  /**
-   * Create a new parser object
-   */
+  /** Create a new parser object. Do not accept a null OptionsData object. */
   OptionsParserImpl(OptionsData optionsData) {
+    Preconditions.checkNotNull(optionsData);
     this.optionsData = optionsData;
   }
 
@@ -158,12 +158,7 @@
             })
         // Ignore expansion options.
         .filter(value -> !value.getOptionDefinition().isExpansionOption())
-        .map(
-            value ->
-                "--"
-                    + value.getOptionDefinition().getOptionName()
-                    + "="
-                    + value.getUnconvertedValue())
+        .map(ParsedOptionDescription::getDeprecatedCanonicalForm)
         .collect(toCollection(ArrayList::new));
   }
 
@@ -193,8 +188,9 @@
   }
 
   private void addDeprecationWarning(String optionName, String warning) {
-    warnings.add("Option '" + optionName + "' is deprecated"
-        + (warning.isEmpty() ? "" : ": " + warning));
+    warnings.add(
+        String.format(
+            "Option '%s' is deprecated%s", optionName, (warning.isEmpty() ? "" : ": " + warning)));
   }
 
 
@@ -213,79 +209,58 @@
     return optionValues.get(optionDefinition);
   }
 
-  OptionDescription getOptionDescription(String name, OptionPriority priority, String source)
-      throws OptionsParsingException {
+  OptionDescription getOptionDescription(String name) throws OptionsParsingException {
     OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
     if (optionDefinition == null) {
       return null;
     }
-
-    return new OptionDescription(
-        optionDefinition,
-        optionsData.getExpansionDataForField(optionDefinition),
-        getImplicitDependentDescriptions(
-            ImmutableList.copyOf(optionDefinition.getImplicitRequirements()),
-            optionDefinition,
-            priority,
-            source));
-  }
-
-  /** @return A list of the descriptions corresponding to the implicit dependent flags passed in. */
-  private ImmutableList<ParsedOptionDescription> getImplicitDependentDescriptions(
-      ImmutableList<String> options,
-      OptionDefinition implicitDependent,
-      OptionPriority priority,
-      String source)
-      throws OptionsParsingException {
-    ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder();
-    Iterator<String> optionsIterator = options.iterator();
-
-    Function<OptionDefinition, String> sourceFunction =
-        o ->
-            String.format(
-                "implicitely required for option %s (source: %s)",
-                implicitDependent.getOptionName(), source);
-    while (optionsIterator.hasNext()) {
-      String unparsedFlagExpression = optionsIterator.next();
-      ParsedOptionDescription parsedOption =
-          identifyOptionAndPossibleArgument(
-              unparsedFlagExpression,
-              optionsIterator,
-              priority,
-              sourceFunction,
-              implicitDependent,
-              null);
-      builder.add(parsedOption);
-    }
-    return builder.build();
+    return new OptionDescription(optionDefinition, optionsData);
   }
 
   /**
-   * @return A list of the descriptions corresponding to options expanded from the flag for the
-   *     given value. The value itself is a string, no conversion has taken place.
+   * Implementation of {@link OptionsParser#getExpansionValueDescriptions(OptionDefinition,
+   * OptionInstanceOrigin)}
    */
-  ImmutableList<ParsedOptionDescription> getExpansionOptionValueDescriptions(
-      OptionDefinition expansionFlag,
-      @Nullable String flagValue,
-      OptionPriority priority,
-      String source)
+  ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions(
+      OptionDefinition expansionFlag, OptionInstanceOrigin originOfExpansionFlag)
       throws OptionsParsingException {
     ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder();
+    OptionInstanceOrigin originOfSubflags;
+    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);
+    } else {
+      return ImmutableList.of();
+    }
 
-    ImmutableList<String> options = optionsData.getEvaluatedExpansion(expansionFlag, flagValue);
     Iterator<String> optionsIterator = options.iterator();
-    Function<OptionDefinition, String> sourceFunction =
-        o -> String.format("expanded from %s (source: %s)", expansionFlag.getOptionName(), source);
     while (optionsIterator.hasNext()) {
       String unparsedFlagExpression = optionsIterator.next();
       ParsedOptionDescription parsedOption =
           identifyOptionAndPossibleArgument(
               unparsedFlagExpression,
               optionsIterator,
-              priority,
-              sourceFunction,
-              null,
-              expansionFlag);
+              originOfSubflags.getPriority(),
+              o -> originOfSubflags.getSource(),
+              originOfSubflags.getImplicitDependent(),
+              originOfSubflags.getExpandedFrom());
       builder.add(parsedOption);
     }
     return builder.build();
@@ -301,13 +276,18 @@
 
   /**
    * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be
-   * called recursively. In each call, there may be no duplicates, but separate calls may contain
-   * intersecting sets of options; in that case, the arg seen last takes precedence.
+   * called recursively. The option's definition dictates how it reacts to multiple settings. By
+   * default, the arg seen last at the highest priority takes precedence, overriding the early
+   * values. Options that accumulate multiple values will track them in priority and appearance
+   * order.
    */
   List<String> parse(
-      OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args)
+      OptionPriority.PriorityCategory priority,
+      Function<OptionDefinition, String> sourceFunction,
+      List<String> args)
       throws OptionsParsingException {
-    return parse(priority, sourceFunction, null, null, args);
+    return parse(
+        OptionPriority.lowestOptionPriorityAtCategory(priority), sourceFunction, null, null, args);
   }
 
   /**
@@ -315,8 +295,8 @@
    * called recursively. Calls may contain intersecting sets of options; in that case, the arg seen
    * last takes precedence.
    *
-   * <p>The method uses the invariant that if an option has neither an implicit dependent nor an
-   * expanded from value, then it must have been explicitly set.
+   * <p>The method treats options that have neither an implicitDependent nor an expandedFrom value
+   * as explicitly set.
    */
   private List<String> parse(
       OptionPriority priority,
@@ -326,7 +306,6 @@
       List<String> args)
       throws OptionsParsingException {
     List<String> unparsedArgs = new ArrayList<>();
-    LinkedHashMap<OptionDefinition, List<String>> implicitRequirements = new LinkedHashMap<>();
 
     Iterator<String> argsIterator = argsPreProcessor.preProcess(args).iterator();
     while (argsIterator.hasNext()) {
@@ -345,124 +324,7 @@
       ParsedOptionDescription parsedOption =
           identifyOptionAndPossibleArgument(
               arg, argsIterator, priority, sourceFunction, implicitDependent, expandedFrom);
-      OptionDefinition optionDefinition = parsedOption.getOptionDefinition();
-      // All options can be deprecated; check and warn before doing any option-type specific work.
-      maybeAddDeprecationWarning(optionDefinition);
-
-      // Track the value, before any remaining option-type specific work that is done outside of
-      // the OptionValueDescription.
-      OptionValueDescription entry =
-          optionValues.computeIfAbsent(
-              optionDefinition, OptionValueDescription::createOptionValueDescription);
-      entry.addOptionInstance(parsedOption, warnings);
-
-      @Nullable String unconvertedValue = parsedOption.getUnconvertedValue();
-      if (optionDefinition.isWrapperOption()) {
-        if (unconvertedValue.startsWith("-")) {
-          String sourceMessage =
-              "Unwrapped from wrapper option --" + optionDefinition.getOptionName();
-          List<String> unparsed =
-              parse(
-                  priority,
-                  o -> sourceMessage,
-                  null, // implicitDependent
-                  null, // expandedFrom
-                  ImmutableList.of(unconvertedValue));
-
-          if (!unparsed.isEmpty()) {
-            throw new OptionsParsingException(
-                "Unparsed options remain after unwrapping "
-                    + arg
-                    + ": "
-                    + Joiner.on(' ').join(unparsed));
-          }
-
-          // Don't process implicitRequirements or expansions for wrapper options. In particular,
-          // don't record this option in parsedOptions, so that only the wrapped option shows
-          // up in canonicalized options.
-          continue;
-
-        } else {
-          throw new OptionsParsingException(
-              "Invalid --"
-                  + optionDefinition.getOptionName()
-                  + " value format. "
-                  + "You may have meant --"
-                  + optionDefinition.getOptionName()
-                  + "=--"
-                  + unconvertedValue);
-        }
-      }
-
-      if (implicitDependent == null) {
-        // Log explicit options and expanded options in the order they are parsed (can be sorted
-        // later). Also remember whether they were expanded or not. This information is needed to
-        // correctly canonicalize flags.
-        parsedOptions.add(parsedOption);
-        if (optionDefinition.allowsMultiple()) {
-          canonicalizeValues.put(optionDefinition, parsedOption);
-        } else {
-          canonicalizeValues.replaceValues(optionDefinition, ImmutableList.of(parsedOption));
-        }
-      }
-
-      // Handle expansion options.
-      if (optionDefinition.isExpansionOption()) {
-        ImmutableList<String> expansion =
-            optionsData.getEvaluatedExpansion(optionDefinition, unconvertedValue);
-
-        String sourceFunctionApplication = sourceFunction.apply(optionDefinition);
-        String sourceMessage =
-            (sourceFunctionApplication == null)
-                ? String.format("expanded from option --%s", optionDefinition.getOptionName())
-                : String.format(
-                    "expanded from option --%s from %s",
-                    optionDefinition.getOptionName(), sourceFunctionApplication);
-        Function<OptionDefinition, String> expansionSourceFunction = o -> sourceMessage;
-        List<String> unparsed =
-            parse(priority, expansionSourceFunction, null, optionDefinition, expansion);
-        if (!unparsed.isEmpty()) {
-          // Throw an assertion, because this indicates an error in the definition of this
-          // option's expansion, not with the input as provided by the user.
-          throw new AssertionError(
-              "Unparsed options remain after parsing expansion of "
-                  + arg
-                  + ": "
-                  + Joiner.on(' ').join(unparsed));
-        }
-      }
-
-      // Collect any implicit requirements.
-      if (optionDefinition.hasImplicitRequirements()) {
-        implicitRequirements.put(
-            optionDefinition, Arrays.asList(optionDefinition.getImplicitRequirements()));
-      }
-    }
-
-    // Now parse any implicit requirements that were collected.
-    // TODO(bazel-team): this should happen when the option is encountered.
-    if (!implicitRequirements.isEmpty()) {
-      for (Map.Entry<OptionDefinition, List<String>> entry : implicitRequirements.entrySet()) {
-        OptionDefinition optionDefinition = entry.getKey();
-        String sourceFunctionApplication = sourceFunction.apply(optionDefinition);
-        String sourceMessage =
-            (sourceFunctionApplication == null)
-                ? String.format(
-                    "implicit requirement of option --%s", optionDefinition.getOptionName())
-                : String.format(
-                    "implicit requirement of option --%s from %s",
-                    optionDefinition.getOptionName(), sourceFunctionApplication);
-        Function<OptionDefinition, String> requirementSourceFunction = o -> sourceMessage;
-
-        List<String> unparsed = parse(priority, requirementSourceFunction, entry.getKey(), null,
-            entry.getValue());
-        if (!unparsed.isEmpty()) {
-          // Throw an assertion, because this indicates an error in the code that specified in the
-          // implicit requirements for the option(s).
-          throw new AssertionError("Unparsed options remain after parsing implicit options: "
-              + Joiner.on(' ').join(unparsed));
-        }
-      }
+      handleNewParsedOption(parsedOption);
     }
 
     // Go through the final values and make sure they are valid values for their option. Unlike any
@@ -475,6 +337,105 @@
     return unparsedArgs;
   }
 
+  /**
+   * Implementation of {@link OptionsParser#addOptionValueAtSpecificPriority(OptionInstanceOrigin,
+   * OptionDefinition, String)}
+   */
+  void addOptionValueAtSpecificPriority(
+      OptionInstanceOrigin origin, OptionDefinition option, String unconvertedValue)
+      throws OptionsParsingException {
+    Preconditions.checkNotNull(option);
+    Preconditions.checkNotNull(
+        unconvertedValue,
+        "Cannot set %s to a null value. Pass \"\" if an empty value is required.",
+        option);
+    Preconditions.checkNotNull(
+        origin,
+        "Cannot assign value \'%s\' to %s without a clear origin for this value.",
+        unconvertedValue,
+        option);
+    PriorityCategory priorityCategory = origin.getPriority().getPriorityCategory();
+    boolean isNotDefault = priorityCategory != OptionPriority.PriorityCategory.DEFAULT;
+    Preconditions.checkArgument(
+        isNotDefault,
+        "Attempt to assign value \'%s\' to %s at priority %s failed. Cannot set options at "
+            + "default priority - by definition, that means the option is unset.",
+        unconvertedValue,
+        option,
+        priorityCategory);
+
+    handleNewParsedOption(
+        new ParsedOptionDescription(
+            option,
+            String.format("--%s=%s", option.getOptionName(), unconvertedValue),
+            unconvertedValue,
+            origin));
+  }
+
+  /** Takes care of tracking the parsed option's value in relation to other options. */
+  private void handleNewParsedOption(ParsedOptionDescription parsedOption)
+      throws OptionsParsingException {
+    OptionDefinition optionDefinition = parsedOption.getOptionDefinition();
+    // All options can be deprecated; check and warn before doing any option-type specific work.
+    maybeAddDeprecationWarning(optionDefinition);
+    // Track the value, before any remaining option-type specific work that is done outside of
+    // the OptionValueDescription.
+    OptionValueDescription entry =
+        optionValues.computeIfAbsent(
+            optionDefinition,
+            def -> OptionValueDescription.createOptionValueDescription(def, optionsData));
+    ExpansionBundle expansionBundle = entry.addOptionInstance(parsedOption, warnings);
+    @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
+    // 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()) {
+      // 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);
+      if (optionDefinition.allowsMultiple()) {
+        canonicalizeValues.put(optionDefinition, parsedOption);
+      } else {
+        canonicalizeValues.replaceValues(optionDefinition, ImmutableList.of(parsedOption));
+      }
+    }
+
+    if (expansionBundle != null) {
+      List<String> unparsed =
+          parse(
+              parsedOption.getPriority(),
+              o -> expansionBundle.sourceOfExpansionArgs,
+              optionDefinition.hasImplicitRequirements() ? optionDefinition : null,
+              optionDefinition.isExpansionOption() ? optionDefinition : null,
+              expansionBundle.expansionArgs);
+      if (!unparsed.isEmpty()) {
+        if (optionDefinition.isWrapperOption()) {
+          throw new OptionsParsingException(
+              "Unparsed options remain after unwrapping "
+                  + unconvertedValue
+                  + ": "
+                  + Joiner.on(' ').join(unparsed));
+        } 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(
+              "Unparsed options remain after processing "
+                  + unconvertedValue
+                  + ": "
+                  + Joiner.on(' ').join(unparsed));
+        }
+      }
+    }
+  }
+
   private ParsedOptionDescription identifyOptionAndPossibleArgument(
       String arg,
       Iterator<String> nextArgs,
@@ -596,10 +557,7 @@
         optionDefinition.getField().set(optionsInstance, value);
       } catch (IllegalArgumentException e) {
         throw new IllegalStateException(
-            String.format(
-                "Unable to set option '%s' to value '%s'.",
-                optionDefinition.getOptionName(), value),
-            e);
+            String.format("Unable to set %s to value '%s'.", optionDefinition, value), e);
       } catch (IllegalAccessException e) {
         throw new IllegalStateException(
             "Could not set the field due to access issues. This is impossible, as the "
diff --git a/java/com/google/devtools/common/options/OptionsParsingException.java b/java/com/google/devtools/common/options/OptionsParsingException.java
index 73b48a0..6b82366 100644
--- a/java/com/google/devtools/common/options/OptionsParsingException.java
+++ b/java/com/google/devtools/common/options/OptionsParsingException.java
@@ -17,7 +17,7 @@
 /**
  * An exception that's thrown when the {@link OptionsParser} fails.
  *
- * @see OptionsParser#parse(OptionPriority,String,java.util.List)
+ * @see OptionsParser#parse(OptionPriority.PriorityCategory,String,java.util.List)
  */
 public class OptionsParsingException extends Exception {
   private final String invalidArgument;
diff --git a/java/com/google/devtools/common/options/OptionsProvider.java b/java/com/google/devtools/common/options/OptionsProvider.java
index 1c7737f..5fd8ac0 100644
--- a/java/com/google/devtools/common/options/OptionsProvider.java
+++ b/java/com/google/devtools/common/options/OptionsProvider.java
@@ -39,8 +39,10 @@
    * specified. If an option was specified multiple times, it is included in the result multiple
    * times. Does not include the residue.
    *
-   * <p>The returned list can be filtered if undocumented, hidden or implicit options should not be
-   * displayed.
+   * <p>The returned list includes undocumented, hidden or implicit options, and should be filtered
+   * as needed. Since it includes all options parsed, it will also include both an expansion option
+   * and the options it expanded to, and so blindly using this list for a new invocation will cause
+   * double-application of these options.
    */
   List<ParsedOptionDescription> asCompleteListOfParsedOptions();
 
diff --git a/java/com/google/devtools/common/options/OptionsUsage.java b/java/com/google/devtools/common/options/OptionsUsage.java
index 68a460e..6dee0eb 100644
--- a/java/com/google/devtools/common/options/OptionsUsage.java
+++ b/java/com/google/devtools/common/options/OptionsUsage.java
@@ -21,7 +21,11 @@
 import com.google.common.escape.Escaper;
 import java.text.BreakIterator;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.annotation.Nullable;
 
 /** A renderer for usage messages for any combination of options classes. */
@@ -42,7 +46,7 @@
         new ArrayList<>(OptionsData.getAllOptionDefinitionsForClass(optionsClass));
     optionDefinitions.sort(OptionDefinition.BY_OPTION_NAME);
     for (OptionDefinition optionDefinition : optionDefinitions) {
-      getUsage(optionDefinition, usage, OptionsParser.HelpVerbosity.LONG, data);
+      getUsage(optionDefinition, usage, OptionsParser.HelpVerbosity.LONG, data, false);
     }
   }
 
@@ -85,14 +89,18 @@
   private static @Nullable ImmutableList<String> getExpansionIfKnown(
       OptionDefinition optionDefinition, OptionsData optionsData) {
     Preconditions.checkNotNull(optionDefinition);
-    try {
-      return optionsData.getEvaluatedExpansion(optionDefinition, null);
-    } catch (ExpansionNeedsValueException e) {
-      return null;
-    } catch (OptionsParsingException e) {
-      throw new IllegalStateException("Error expanding void expansion function: ", e);
-    }
+    return optionsData.getEvaluatedExpansion(optionDefinition);
+  }
 
+  // Placeholder tag "UNKNOWN" is ignored.
+  private static boolean shouldEffectTagBeListed(OptionEffectTag effectTag) {
+    return !effectTag.equals(OptionEffectTag.UNKNOWN);
+  }
+
+  // Tags that only apply to undocumented options are excluded.
+  private static boolean shouldMetadataTagBeListed(OptionMetadataTag metadataTag) {
+    return !metadataTag.equals(OptionMetadataTag.HIDDEN)
+        && !metadataTag.equals(OptionMetadataTag.INTERNAL);
   }
 
   /** Appends the usage message for a single option-field message to 'usage'. */
@@ -100,14 +108,17 @@
       OptionDefinition optionDefinition,
       StringBuilder usage,
       OptionsParser.HelpVerbosity helpVerbosity,
-      OptionsData optionsData) {
+      OptionsData optionsData,
+      boolean includeTags) {
     String flagName = getFlagName(optionDefinition);
     String typeDescription = getTypeDescription(optionDefinition);
     usage.append("  --").append(flagName);
-    if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) { // just the name
+    if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) {
       usage.append('\n');
       return;
     }
+
+    // Add the option's type and default information. Stop there for "medium" verbosity.
     if (optionDefinition.getAbbreviation() != '\0') {
       usage.append(" [-").append(optionDefinition.getAbbreviation()).append(']');
     }
@@ -127,9 +138,12 @@
       usage.append(")");
     }
     usage.append("\n");
-    if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) { // just the name and type.
+    if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) {
       return;
     }
+
+    // For verbosity "long," add the full description and expansion, along with the tag
+    // information if requested.
     if (!optionDefinition.getHelpText().isEmpty()) {
       usage.append(paragraphFill(optionDefinition.getHelpText(), /*indent=*/ 4, /*width=*/ 80));
       usage.append('\n');
@@ -151,9 +165,28 @@
       for (String req : optionDefinition.getImplicitRequirements()) {
         requiredMsg.append(req).append(" ");
       }
-      usage.append(paragraphFill(requiredMsg.toString(), /*indent=*/ 6, /*width=*/ 80));
+      usage.append(paragraphFill(requiredMsg.toString(), 6, 80)); // (indent, width)
       usage.append('\n');
     }
+    if (!includeTags) {
+      return;
+    }
+
+    // If we are expected to include the tags, add them for high verbosity.
+    Stream<OptionEffectTag> effectTagStream =
+        Arrays.stream(optionDefinition.getOptionEffectTags())
+            .filter(OptionsUsage::shouldEffectTagBeListed);
+    Stream<OptionMetadataTag> metadataTagStream =
+        Arrays.stream(optionDefinition.getOptionMetadataTags())
+            .filter(OptionsUsage::shouldMetadataTagBeListed);
+    String tagList =
+        Stream.concat(effectTagStream, metadataTagStream)
+            .map(tag -> tag.toString().toLowerCase())
+            .collect(Collectors.joining(", "));
+    if (!tagList.isEmpty()) {
+      usage.append(paragraphFill("Tags: " + tagList, 6, 80)); // (indent, width)
+      usage.append("\n");
+    }
   }
 
   /** Append the usage message for a single option-field message to 'usage'. */
@@ -161,7 +194,8 @@
       OptionDefinition optionDefinition,
       StringBuilder usage,
       Escaper escaper,
-      OptionsData optionsData) {
+      OptionsData optionsData,
+      boolean includeTags) {
     String plainFlagName = optionDefinition.getOptionName();
     String flagName = getFlagName(optionDefinition);
     String valueDescription = optionDefinition.getValueTypeHelpText();
@@ -203,7 +237,7 @@
       usage.append('\n');
     }
 
-    if (!optionsData.getExpansionDataForField(optionDefinition).isEmpty()) {
+    if (!optionsData.getEvaluatedExpansion(optionDefinition).isEmpty()) {
       // If this is an expansion option, list the expansion if known, or at least specify that we
       // don't know.
       usage.append("<br/>\n");
@@ -215,7 +249,10 @@
         Preconditions.checkArgument(!expansion.isEmpty());
         expandsMsg = new StringBuilder("Expands to:<br/>\n");
         for (String exp : expansion) {
-          // TODO(ulfjack): Can we link to the expanded flags here?
+          // TODO(ulfjack): We should link to the expanded flags, but unfortunately we don't
+          // currently guarantee that all flags are only printed once. A flag in an OptionBase that
+          // is included by 2 different commands, but not inherited through a parent command, will
+          // be printed multiple times.
           expandsMsg
               .append("&nbsp;&nbsp;<code>")
               .append(escaper.escape(exp))
@@ -225,6 +262,32 @@
       usage.append(expandsMsg.toString());
     }
 
+    // Add effect tags, if not UNKNOWN, and metadata tags, if not empty.
+    if (includeTags) {
+      Stream<OptionEffectTag> effectTagStream =
+          Arrays.stream(optionDefinition.getOptionEffectTags())
+              .filter(OptionsUsage::shouldEffectTagBeListed);
+      Stream<OptionMetadataTag> metadataTagStream =
+          Arrays.stream(optionDefinition.getOptionMetadataTags())
+              .filter(OptionsUsage::shouldMetadataTagBeListed);
+      String tagList =
+          Stream.concat(
+                  effectTagStream.map(
+                      tag ->
+                          String.format(
+                              "<a href=\"#effect_tag_%s\"><code>%s</code></a>",
+                              tag, tag.name().toLowerCase())),
+                  metadataTagStream.map(
+                      tag ->
+                          String.format(
+                              "<a href=\"#metadata_tag_%s\"><code>%s</code></a>",
+                              tag, tag.name().toLowerCase())))
+              .collect(Collectors.joining(", "));
+      if (!tagList.isEmpty()) {
+        usage.append("<br>Tags: \n").append(tagList);
+      }
+    }
+
     usage.append("</dd>\n");
   }
 
@@ -263,8 +326,10 @@
       builder.append("={auto,yes,no}\n");
       builder.append("--no").append(flagName).append("\n");
     } else if (fieldType.isEnum()) {
-      builder.append("={")
-          .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase()).append("}\n");
+      builder
+          .append("={")
+          .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase(Locale.ENGLISH))
+          .append("}\n");
     } else if (fieldType.getSimpleName().equals("Label")) {
       // String comparison so we don't introduce a dependency to com.google.devtools.build.lib.
       builder.append("=label\n");
diff --git a/java/com/google/devtools/common/options/ParsedOptionDescription.java b/java/com/google/devtools/common/options/ParsedOptionDescription.java
index 0910579..f55f8ad 100644
--- a/java/com/google/devtools/common/options/ParsedOptionDescription.java
+++ b/java/com/google/devtools/common/options/ParsedOptionDescription.java
@@ -15,6 +15,7 @@
 package com.google.devtools.common.options;
 
 import com.google.common.collect.ImmutableList;
+import java.util.function.Function;
 import javax.annotation.Nullable;
 
 /**
@@ -49,6 +50,46 @@
     return commandLineForm;
   }
 
+  public String getCanonicalForm() {
+    return getCanonicalFormWithValueEscaper(s -> s);
+  }
+
+  public String getCanonicalFormWithValueEscaper(Function<String, String> escapingFunction) {
+    // For boolean flags (note that here we do not check for TriState flags, only flags with actual
+    // boolean values, so that we know the return type of getConvertedValue), use the --[no]flag
+    // form for the canonical value.
+    if (optionDefinition.getType().equals(boolean.class)) {
+      try {
+        return ((boolean) getConvertedValue() ? "--" : "--no") + optionDefinition.getOptionName();
+      } catch (OptionsParsingException e) {
+        throw new RuntimeException("Unexpected parsing exception", e);
+      }
+    } else {
+      String optionString = "--" + optionDefinition.getOptionName();
+      if (unconvertedValue != null) { // Can be null for Void options.
+        optionString += "=" + escapingFunction.apply(unconvertedValue);
+      }
+      return optionString;
+    }
+  }
+
+  @Deprecated
+  // TODO(b/65646296) Once external dependencies are cleaned up, use getCanonicalForm()
+  String getDeprecatedCanonicalForm() {
+    String value = unconvertedValue;
+    // For boolean flags (note that here we do not check for TriState flags, only flags with actual
+    // boolean values, so that we know the return type of getConvertedValue), set them all to 1 or
+    // 0, instead of keeping the wide variety of values we accept in their original form.
+    if (optionDefinition.getType().equals(boolean.class)) {
+      try {
+        value = (boolean) getConvertedValue() ? "1" : "0";
+      } catch (OptionsParsingException e) {
+        throw new RuntimeException("Unexpected parsing exception", e);
+      }
+    }
+    return String.format("--%s=%s", optionDefinition.getOptionName(), value);
+  }
+
   public boolean isBooleanOption() {
     return optionDefinition.getType().equals(boolean.class);
   }
@@ -74,7 +115,11 @@
     return unconvertedValue;
   }
 
-  OptionPriority getPriority() {
+  public OptionInstanceOrigin getOrigin() {
+    return origin;
+  }
+
+  public OptionPriority getPriority() {
     return origin.getPriority();
   }
 
@@ -108,7 +153,7 @@
   @Override
   public String toString() {
     StringBuilder result = new StringBuilder();
-    result.append("option '").append(optionDefinition.getOptionName()).append("' ");
+    result.append(optionDefinition);
     result.append("set to '").append(unconvertedValue).append("' ");
     result.append("with priority ").append(origin.getPriority());
     if (origin.getSource() != null) {
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 cdc3263..c74febb 100644
--- a/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixerTest.java
@@ -98,7 +98,12 @@
   private byte[] desugar(ClassReader reader) {
     ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
     DefaultMethodClassFixer fixer =
-        new DefaultMethodClassFixer(writer, classpathReader, bootclassPath, classLoader);
+        new DefaultMethodClassFixer(
+            writer,
+            classpathReader,
+            DependencyCollector.NoWriteCollectors.FAIL_ON_MISSING,
+            bootclassPath,
+            classLoader);
     reader.accept(fixer, 0);
     return writer.toByteArray();
   }
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 8321d75..20e6028 100644
--- a/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/DesugarJava8FunctionalTest.java
@@ -23,6 +23,9 @@
 import com.google.devtools.build.android.desugar.testdata.java8.ConcreteOverridesDefaultWithLambda;
 import com.google.devtools.build.android.desugar.testdata.java8.DefaultInterfaceMethodWithStaticInitializer;
 import com.google.devtools.build.android.desugar.testdata.java8.DefaultInterfaceWithBridges;
+import com.google.devtools.build.android.desugar.testdata.java8.DefaultMethodFromSeparateJava8Target;
+import com.google.devtools.build.android.desugar.testdata.java8.DefaultMethodFromSeparateJava8TargetOverridden;
+import com.google.devtools.build.android.desugar.testdata.java8.DefaultMethodTransitivelyFromSeparateJava8Target;
 import com.google.devtools.build.android.desugar.testdata.java8.FunctionWithDefaultMethod;
 import com.google.devtools.build.android.desugar.testdata.java8.FunctionalInterfaceWithInitializerAndDefaultMethods;
 import com.google.devtools.build.android.desugar.testdata.java8.GenericDefaultInterfaceWithLambda;
@@ -111,6 +114,12 @@
   }
 
   @Test
+  public void testBootclasspathMethodInvocations() {
+    InterfaceMethod concrete = new InterfaceMethod.Concrete();
+    assertThat(concrete.defaultInvokingBootclasspathMethods("Larry")).isEqualTo("Larry");
+  }
+
+  @Test
   public void testStaticMethodsInInterface_explicitAndLambdaBody() {
     List<Long> result = FunctionWithDefaultMethod.DoubleInts.add(ImmutableList.of(7, 39, 8), 3);
     assertThat(result).containsExactly(10L, 42L, 11L).inOrder();
@@ -394,4 +403,16 @@
                   .getExpectedInitializationOrder());
     }
   }
+
+  /**
+   * Tests that default methods on the classpath are correctly handled. We'll also verify the
+   * metadata that's emitted for this case to make sure the binary-wide double-check for correct
+   * desugaring of default and static interface methods keeps working (b/65645388).
+   */
+  @Test
+  public void testDefaultMethodsInSeparateTarget() {
+    assertThat(new DefaultMethodFromSeparateJava8Target().dflt()).isEqualTo("dflt");
+    assertThat(new DefaultMethodTransitivelyFromSeparateJava8Target().dflt()).isEqualTo("dflt");
+    assertThat(new DefaultMethodFromSeparateJava8TargetOverridden().dflt()).isEqualTo("override");
+  }
 }
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 b8c8b54..2eab943 100644
--- a/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/Java7CompatibilityTest.java
@@ -31,7 +31,7 @@
   public void testJava7CompatibleInterface() throws Exception {
     ClassReader reader = new ClassReader(ExtendsDefault.class.getName());
     ClassTester tester = new ClassTester();
-    reader.accept(new Java7Compatibility(tester, null), 0);
+    reader.accept(new Java7Compatibility(tester, null, null), 0);
     assertThat(tester.version).isEqualTo(Opcodes.V1_7);
     assertThat(tester.bridgeMethods).isEqualTo(0); // make sure we strip bridge methods
     assertThat(tester.clinitMethods).isEqualTo(1); // make sure we don't strip <clinit>
@@ -41,7 +41,7 @@
   public void testDefaultMethodFails() throws Exception {
     ClassReader reader = new ClassReader(WithDefault.class.getName());
     try {
-      reader.accept(new Java7Compatibility(null, null), 0);
+      reader.accept(new Java7Compatibility(null, null, null), 0);
       fail("IllegalArgumentException expected");
     } catch (IllegalArgumentException expected) {
       assertThat(expected).hasMessageThat().contains("getVersion()I");
@@ -56,7 +56,7 @@
   public void testConcreteClassRedeclaresBridges() throws Exception {
     ClassReader reader = new ClassReader(Impl.class.getName());
     ClassTester tester = new ClassTester();
-    reader.accept(new Java7Compatibility(tester, null), 0);
+    reader.accept(new Java7Compatibility(tester, null, null), 0);
     assertThat(tester.version).isEqualTo(Opcodes.V1_7);
     assertThat(tester.bridgeMethods).isEqualTo(2);
   }
diff --git a/test/java/com/google/devtools/build/android/desugar/class_with_lambdas_in_implemented_interface_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/class_with_lambdas_in_implemented_interface_disassembled_golden.txt
index 48a8632..73a563a 100644
--- a/test/java/com/google/devtools/build/android/desugar/class_with_lambdas_in_implemented_interface_disassembled_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/class_with_lambdas_in_implemented_interface_disassembled_golden.txt
@@ -2,6 +2,7 @@
 public class com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod$Concrete implements com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod {
   public com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod$Concrete();
   public java.util.List defaultMethodReference(java.util.List);
+  public java.lang.String defaultInvokingBootclasspathMethods(java.lang.String);
   public java.util.List staticMethodReference(java.util.List);
   public java.util.List lambdaCallsDefaultMethod(java.util.List);
   public boolean startsWithS(java.lang.String);
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
new file mode 100644
index 0000000..e99abc4
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/dependencies/MetadataCollectorTest.java
@@ -0,0 +1,113 @@
+// Copyright 2017 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.dependencies;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+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;
+
+/** Unit tests for {@link MetadataCollector}. */
+@RunWith(JUnit4.class)
+public class MetadataCollectorTest {
+
+  @Test
+  public void testEmptyAvoidsOutput() {
+    assertThat(new MetadataCollector(false).toByteArray()).isNull();
+  }
+
+  @Test
+  public void testAssumeCompanionClass() throws Exception {
+    MetadataCollector collector = new MetadataCollector(false);
+    collector.assumeCompanionClass("a", "b$$CC");
+    collector.assumeCompanionClass("b", "b$$CC");
+    collector.assumeCompanionClass("a", "a$$CC");
+
+    DesugarDepsInfo info = extractProto(collector);
+    assertThat(info.getAssumePresentList())
+        .containsExactly(
+            Dependency.newBuilder().setOrigin(wrapType("a")).setTarget(wrapType("b$$CC")).build(),
+            Dependency.newBuilder().setOrigin(wrapType("b")).setTarget(wrapType("b$$CC")).build(),
+            Dependency.newBuilder().setOrigin(wrapType("a")).setTarget(wrapType("a$$CC")).build());
+  }
+
+  @Test
+  public void testMissingImplementedInterface() throws Exception {
+    MetadataCollector collector = new MetadataCollector(true);
+    collector.missingImplementedInterface("a", "b");
+    collector.missingImplementedInterface("a", "c");
+    collector.missingImplementedInterface("c", "b");
+
+    DesugarDepsInfo info = extractProto(collector);
+    assertThat(info.getMissingInterfaceList())
+        .containsExactly(
+            Dependency.newBuilder().setOrigin(wrapType("a")).setTarget(wrapType("b")).build(),
+            Dependency.newBuilder().setOrigin(wrapType("a")).setTarget(wrapType("c")).build(),
+            Dependency.newBuilder().setOrigin(wrapType("c")).setTarget(wrapType("b")).build());
+  }
+
+  @Test
+  public void testRecordExtendedInterfaces() throws Exception {
+    MetadataCollector collector = new MetadataCollector(false);
+    collector.recordExtendedInterfaces("a", "b", "c");
+    collector.recordExtendedInterfaces("b");
+    collector.recordExtendedInterfaces("c", "d");
+
+    DesugarDepsInfo info = extractProto(collector);
+    assertThat(info.getInterfaceWithSupertypesList())
+        .containsExactly(
+            InterfaceDetails.newBuilder()
+                .setOrigin(wrapType("a"))
+                .addAllExtendedInterface(ImmutableList.of(wrapType("b"), wrapType("c")))
+                .build(),
+            InterfaceDetails.newBuilder()
+                .setOrigin(wrapType("c"))
+                .addAllExtendedInterface(ImmutableList.of(wrapType("d")))
+                .build());
+  }
+
+  @Test
+  public void testRecordDefaultMethods() throws Exception {
+    MetadataCollector collector = new MetadataCollector(false);
+    collector.recordDefaultMethods("a", 0);
+    collector.recordDefaultMethods("b", 1);
+
+    DesugarDepsInfo info = extractProto(collector);
+    assertThat(info.getInterfaceWithCompanionList())
+        .containsExactly(
+            InterfaceWithCompanion.newBuilder()
+                .setOrigin(wrapType("a"))
+                .setNumDefaultMethods(0)
+                .build(),
+            InterfaceWithCompanion.newBuilder()
+                .setOrigin(wrapType("b"))
+                .setNumDefaultMethods(1)
+                .build());
+  }
+
+  private static Type wrapType(String name) {
+    return Type.newBuilder().setBinaryName(name).build();
+  }
+
+  private DesugarDepsInfo extractProto(MetadataCollector collector) throws Exception {
+    return DesugarDepsInfo.parseFrom(collector.toByteArray());
+  }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/desugar_deps_consistency_test.sh b/test/java/com/google/devtools/build/android/desugar/desugar_deps_consistency_test.sh
new file mode 100755
index 0000000..e8e7e23
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/desugar_deps_consistency_test.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+#
+# 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.
+set -eu
+
+out="$(mktemp)"
+if ! "devtools/blaze/singlejar/singlejar" --output "${out}" --check_desugar_deps --sources "$@"; then
+  rm "${out}"
+  case "$0" in
+    *_fail_test) echo "Singlejar failed as expected!"; exit 0;;
+  esac
+  echo "Singlejar unexpectedly failed"
+  exit 1
+fi
+
+case "$0" in
+  *_fail_test) rm "${out}"; echo "Singlejar unexpectedly succeeded :("; exit 1;;
+esac
+
+if third_party/java/jdk/jar/jar tf "${out}" | grep 'desugar_deps'; then
+  rm "${out}"
+  echo "Singlejar output unexpectedly contains desugaring metadata"
+  exit 2
+fi  # else grep didn't find anything -> pass
+rm "${out}"
+exit 0
diff --git a/test/java/com/google/devtools/build/android/desugar/interface_with_desugared_method_bodies_disassembled_golden.txt b/test/java/com/google/devtools/build/android/desugar/interface_with_desugared_method_bodies_disassembled_golden.txt
index 828cee4..2d993a6 100644
--- a/test/java/com/google/devtools/build/android/desugar/interface_with_desugared_method_bodies_disassembled_golden.txt
+++ b/test/java/com/google/devtools/build/android/desugar/interface_with_desugared_method_bodies_disassembled_golden.txt
@@ -1,6 +1,7 @@
 Compiled from "InterfaceMethod.java"
 public interface com.google.devtools.build.android.desugar.testdata.java8.InterfaceMethod {
   public abstract java.util.List<java.lang.String> defaultMethodReference(java.util.List<java.lang.String>);
+  public abstract java.lang.String defaultInvokingBootclasspathMethods(java.lang.String);
   public abstract java.util.List<java.lang.String> staticMethodReference(java.util.List<java.lang.String>);
   public abstract java.util.List<java.lang.String> lambdaCallsDefaultMethod(java.util.List<java.lang.String>);
   public abstract boolean startsWithS(java.lang.String);
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.java
index 47d8ab6..420ac15 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/InterfaceWithLambda.java
@@ -14,12 +14,14 @@
 package com.google.devtools.build.android.desugar.testdata;
 
 import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.stream.Collectors;
 
 public interface InterfaceWithLambda {
   String ZERO = String.valueOf(0);
-  ImmutableList<String> DIGITS =
+  List<String> DIGITS =
       ImmutableList.of(0, 1)
           .stream()
           .map(i -> i == 0 ? ZERO : String.valueOf(i))
-          .collect(ImmutableList.toImmutableList());
+          .collect(Collectors.toList());
 }
diff --git a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java b/test/java/com/google/devtools/build/android/desugar/testdata/b68049457/StaticInterfaceMethod.java
similarity index 63%
rename from java/com/google/devtools/common/options/ExpansionNeedsValueException.java
rename to test/java/com/google/devtools/build/android/desugar/testdata/b68049457/StaticInterfaceMethod.java
index d63b988..7815a73 100644
--- a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/b68049457/StaticInterfaceMethod.java
@@ -11,15 +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.common.options;
+package com.google.devtools.build.android.desugar.testdata.b68049457;
 
-/**
- * Exception specific to evaluating {@link ExpansionFunction} objects. Used when expansion isn't
- * possible because of a missing input.
- */
-public final class ExpansionNeedsValueException extends OptionsParsingException {
-
-  public ExpansionNeedsValueException(String message) {
-    super(message);
+/** Interface declaring a static method for regression test for b/68049457. */
+public interface StaticInterfaceMethod {
+  static String never() {
+    throw new IllegalStateException("can't get here");
   }
 }
diff --git a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java b/test/java/com/google/devtools/build/android/desugar/testdata/b68049457/StaticInterfaceMethodCaller.java
similarity index 63%
copy from java/com/google/devtools/common/options/ExpansionNeedsValueException.java
copy to test/java/com/google/devtools/build/android/desugar/testdata/b68049457/StaticInterfaceMethodCaller.java
index d63b988..f961d96 100644
--- a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/b68049457/StaticInterfaceMethodCaller.java
@@ -11,15 +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.common.options;
+package com.google.devtools.build.android.desugar.testdata.b68049457;
 
-/**
- * Exception specific to evaluating {@link ExpansionFunction} objects. Used when expansion isn't
- * possible because of a missing input.
- */
-public final class ExpansionNeedsValueException extends OptionsParsingException {
-
-  public ExpansionNeedsValueException(String message) {
-    super(message);
+/** Class calling static interface method for regression test for b/68049457. */
+public class StaticInterfaceMethodCaller {
+  public String callIt() {
+    return StaticInterfaceMethod.never();
   }
 }
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.java
index 0d9f70a..30a5200 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteDefaultInterfaceWithLambda.java
@@ -14,15 +14,17 @@
 package com.google.devtools.build.android.desugar.testdata.java8;
 
 import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.stream.Collectors;
 
 public class ConcreteDefaultInterfaceWithLambda implements DefaultInterfaceWithLambda {
   static final String ONE = String.valueOf(1);
 
   @Override
-  public ImmutableList<String> digits() {
+  public List<String> digits() {
     return ImmutableList.of(0, 2)
         .stream()
         .map(i -> i == 0 ? ONE : String.valueOf(i))
-        .collect(ImmutableList.toImmutableList());
+        .collect(Collectors.toList());
   }
 }
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.java
index cdcc5e9..5998f41 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/ConcreteOverridesDefaultWithLambda.java
@@ -14,24 +14,26 @@
 package com.google.devtools.build.android.desugar.testdata.java8;
 
 import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.stream.Collectors;
 
 public class ConcreteOverridesDefaultWithLambda implements DefaultInterfaceWithLambda {
   static final String TWO = String.valueOf(2);
   static final String THREE = String.valueOf(3);
 
   @Override
-  public ImmutableList<String> defaultWithLambda() {
+  public List<String> defaultWithLambda() {
     return ImmutableList.of(0, 3)
         .stream()
         .map(i -> i == 0 ? TWO : String.valueOf(i))
-        .collect(ImmutableList.toImmutableList());
+        .collect(Collectors.toList());
   }
 
   @Override
-  public ImmutableList<String> digits() {
+  public List<String> digits() {
     return ImmutableList.of(0, 4)
         .stream()
         .map(i -> i == 0 ? THREE : String.valueOf(i))
-        .collect(ImmutableList.toImmutableList());
+        .collect(Collectors.toList());
   }
 }
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.java
index e97cae9..ce5fca7 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.java
@@ -14,20 +14,22 @@
 package com.google.devtools.build.android.desugar.testdata.java8;
 
 import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.stream.Collectors;
 
 public interface DefaultInterfaceWithLambda {
   String ZERO = String.valueOf(0);
 
-  public default ImmutableList<String> defaultWithLambda() {
+  public default List<String> defaultWithLambda() {
     return ImmutableList.of(0, 1)
         .stream()
         .map(i -> i == 0 ? ZERO : String.valueOf(i))
-        .collect(ImmutableList.toImmutableList());
+        .collect(Collectors.toList());
   }
 
-  public default ImmutableList<String> defaultCallsInterfaceMethod() {
+  public default List<String> defaultCallsInterfaceMethod() {
     return digits();
   }
 
-  public ImmutableList<String> digits();
+  public List<String> digits();
 }
diff --git a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodFromSeparateJava8Target.java
similarity index 61%
copy from java/com/google/devtools/common/options/ExpansionNeedsValueException.java
copy to test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodFromSeparateJava8Target.java
index d63b988..e0e8703 100644
--- a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodFromSeparateJava8Target.java
@@ -11,15 +11,10 @@
 // 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.common.options;
+package com.google.devtools.build.android.desugar.testdata.java8;
 
-/**
- * Exception specific to evaluating {@link ExpansionFunction} objects. Used when expansion isn't
- * possible because of a missing input.
- */
-public final class ExpansionNeedsValueException extends OptionsParsingException {
+import com.google.devtools.build.android.desugar.testdata.separate8.SeparateInterfaceWithDefaultMethod;
 
-  public ExpansionNeedsValueException(String message) {
-    super(message);
-  }
-}
+/** Test class that inherits default method defined in separate target for testing b/65645388. */
+public class DefaultMethodFromSeparateJava8Target
+    implements SeparateInterfaceWithDefaultMethod {}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodFromSeparateJava8TargetOverridden.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodFromSeparateJava8TargetOverridden.java
new file mode 100644
index 0000000..1613f8e
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodFromSeparateJava8TargetOverridden.java
@@ -0,0 +1,25 @@
+// Copyright 2017 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;
+
+import com.google.devtools.build.android.desugar.testdata.separate8.SeparateInterfaceThatInheritsDefaultMethod;
+
+/** Test class that overrides default method defined in separate target for testing b/65645388. */
+public class DefaultMethodFromSeparateJava8TargetOverridden
+    implements SeparateInterfaceThatInheritsDefaultMethod {
+  @Override
+  public String dflt() {
+    return "override";
+  }
+}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodTransitivelyFromSeparateJava8Target.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodTransitivelyFromSeparateJava8Target.java
new file mode 100644
index 0000000..693eaf7
--- /dev/null
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodTransitivelyFromSeparateJava8Target.java
@@ -0,0 +1,23 @@
+// Copyright 2017 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;
+
+import com.google.devtools.build.android.desugar.testdata.separate8.SeparateInterfaceThatInheritsDefaultMethod;
+
+/**
+ * Test class that transitively inherits default method defined in separate target for testing
+ * b/65645388.
+ */
+public class DefaultMethodTransitivelyFromSeparateJava8Target
+    implements SeparateInterfaceThatInheritsDefaultMethod {}
diff --git a/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.java b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.java
index 622e6e5..ae2b6e1 100644
--- a/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/java8/InterfaceMethod.java
@@ -15,6 +15,7 @@
 
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Desugar test input interface that declares lambdas and method references in default and static
@@ -25,6 +26,10 @@
     return names.stream().filter(this::startsWithS).collect(Collectors.toList());
   }
 
+  public default String defaultInvokingBootclasspathMethods(String expectedValue) {
+    return Stream.of(expectedValue).findFirst().orElse("unexpected");
+  }
+
   public default List<String> staticMethodReference(List<String> names) {
     return names.stream().filter(InterfaceMethod::startsWithA).collect(Collectors.toList());
   }
diff --git a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java b/test/java/com/google/devtools/build/android/desugar/testdata/separate8/SeparateInterfaceThatInheritsDefaultMethod.java
similarity index 62%
copy from java/com/google/devtools/common/options/ExpansionNeedsValueException.java
copy to test/java/com/google/devtools/build/android/desugar/testdata/separate8/SeparateInterfaceThatInheritsDefaultMethod.java
index d63b988..400a6f1 100644
--- a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/separate8/SeparateInterfaceThatInheritsDefaultMethod.java
@@ -11,15 +11,8 @@
 // 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.common.options;
+package com.google.devtools.build.android.desugar.testdata.separate8;
 
-/**
- * Exception specific to evaluating {@link ExpansionFunction} objects. Used when expansion isn't
- * possible because of a missing input.
- */
-public final class ExpansionNeedsValueException extends OptionsParsingException {
-
-  public ExpansionNeedsValueException(String message) {
-    super(message);
-  }
-}
+/** Interface that inherits default method in separate compilation target for testing b/65645388. */
+public interface SeparateInterfaceThatInheritsDefaultMethod
+    extends SeparateInterfaceWithDefaultMethod {}
diff --git a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java b/test/java/com/google/devtools/build/android/desugar/testdata/separate8/SeparateInterfaceWithDefaultMethod.java
similarity index 63%
copy from java/com/google/devtools/common/options/ExpansionNeedsValueException.java
copy to test/java/com/google/devtools/build/android/desugar/testdata/separate8/SeparateInterfaceWithDefaultMethod.java
index d63b988..b3c04c8 100644
--- a/java/com/google/devtools/common/options/ExpansionNeedsValueException.java
+++ b/test/java/com/google/devtools/build/android/desugar/testdata/separate8/SeparateInterfaceWithDefaultMethod.java
@@ -11,15 +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.common.options;
+package com.google.devtools.build.android.desugar.testdata.separate8;
 
-/**
- * Exception specific to evaluating {@link ExpansionFunction} objects. Used when expansion isn't
- * possible because of a missing input.
- */
-public final class ExpansionNeedsValueException extends OptionsParsingException {
-
-  public ExpansionNeedsValueException(String message) {
-    super(message);
+/** Interface with default method in separate compilation target for testing b/65645388. */
+public interface SeparateInterfaceWithDefaultMethod {
+  default String dflt() {
+    return "dflt";
   }
 }
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 8664932..907edd0 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
@@ -56,6 +56,9 @@
 com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceMethodWithStaticInitializer.class
 com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithBridges.class
 com/google/devtools/build/android/desugar/testdata/java8/DefaultInterfaceWithLambda.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodFromSeparateJava8Target.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodFromSeparateJava8TargetOverridden.class
+com/google/devtools/build/android/desugar/testdata/java8/DefaultMethodTransitivelyFromSeparateJava8Target.class
 com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$DoubleInts.class
 com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod$DoubleInts2.class
 com/google/devtools/build/android/desugar/testdata/java8/FunctionWithDefaultMethod.class