Merge commit '50f70c15b80b489373b7d6b9134062b546eb0b30' into dev-release
diff --git a/build.gradle b/build.gradle
index eca19a7..f775822 100644
--- a/build.gradle
+++ b/build.gradle
@@ -198,6 +198,11 @@
         }
         output.resourcesDir = 'build/classes/kotlinR8TestResources'
     }
+    keepanno {
+        java {
+            srcDirs = ['src/keepanno/java']
+        }
+    }
 }
 
 // Ensure importing into IntelliJ IDEA use the same output directories as Gradle. In tests we
@@ -207,7 +212,7 @@
 idea {
     sourceSets.all { SourceSet sources ->
         module {
-            if (sources.name == "main") {
+            if (sources.name == "main" || sources.name == "keepanno") {
                 sourceDirs += sources.java.srcDirs
                 outputDir sources.output.classesDirs[0]
             } else {
@@ -311,6 +316,11 @@
     apiUsageSampleCompile "com.google.guava:guava:$guavaVersion"
     kotlinR8TestResourcesCompileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
     errorprone("com.google.errorprone:error_prone_core:$errorproneVersion")
+
+    keepannoCompile group: 'org.ow2.asm', name: 'asm', version: asmVersion
+    keepannoCompile "com.google.guava:guava:$guavaVersion"
+    testCompile sourceSets.keepanno.output
+    testRuntime sourceSets.keepanno.output
 }
 
 def r8LibPath = "$buildDir/libs/r8lib.jar"
@@ -1093,6 +1103,7 @@
 task testJarSources(type: Jar, dependsOn: [testClasses, buildLibraryDesugarConversions]) {
     archiveFileName = "r8testsbase.jar"
     from sourceSets.test.output
+    from sourceSets.keepanno.output
     // We only want to include tests that use R8 when generating keep rules for applymapping.
     include "com/android/tools/r8/**"
     include "android/**"
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
new file mode 100644
index 0000000..9d0265e
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface KeepCondition {}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
new file mode 100644
index 0000000..24e49d1
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepConstants.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.annotations;
+
+/**
+ * Utility class for referencing the various keep annotations and their structure.
+ *
+ * <p>Use of these references avoids poluting the Java namespace with imports of the java
+ * annotations which overlap in name with the actual semantic AST types.
+ */
+public final class KeepConstants {
+
+  public static String getDescriptor(Class<?> clazz) {
+    return "L" + clazz.getTypeName().replace('.', '/') + ";";
+  }
+
+  public static String getBinaryNameFromClassTypeName(String classTypeName) {
+    return classTypeName.replace('.', '/');
+  }
+
+  public static final class Edge {
+    public static final Class<KeepEdge> CLASS = KeepEdge.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    public static final String preconditions = "preconditions";
+    public static final String consequences = "consequences";
+  }
+
+  public static final class Target {
+    public static final Class<KeepTarget> CLASS = KeepTarget.class;
+    public static final String DESCRIPTOR = getDescriptor(CLASS);
+    public static final String classConstant = "classConstant";
+    public static final String methodName = "methodName";
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java
new file mode 100644
index 0000000..19b1a4a
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepEdge.java
@@ -0,0 +1,17 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface KeepEdge {
+  KeepCondition[] preconditions() default {};
+
+  KeepTarget[] consequences();
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
new file mode 100644
index 0000000..e20f288
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.ANNOTATION_TYPE)
+@Retention(RetentionPolicy.CLASS)
+public @interface KeepTarget {
+  Class<?> classConstant() default Object.class;
+
+  String classTypeName() default "";
+
+  String methodName() default "";
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
new file mode 100644
index 0000000..3ba3b8b
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -0,0 +1,181 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.asm;
+
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Target;
+import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepEdgeException;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern.Builder;
+import com.android.tools.r8.keepanno.ast.KeepMembersPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepPreconditions;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepTarget;
+import java.util.HashSet;
+import java.util.Set;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+public class KeepEdgeReader implements Opcodes {
+
+  public static int ASM_VERSION = ASM9;
+
+  public static Set<KeepEdge> readKeepEdges(byte[] classFileBytes) {
+    ClassReader reader = new ClassReader(classFileBytes);
+    Set<KeepEdge> edges = new HashSet<>();
+    reader.accept(new KeepEdgeClassVisitor(edges::add), ClassReader.SKIP_CODE);
+    return edges;
+  }
+
+  private static class KeepEdgeClassVisitor extends ClassVisitor {
+    private final Parent<KeepEdge> parent;
+
+    KeepEdgeClassVisitor(Parent<KeepEdge> parent) {
+      super(ASM_VERSION);
+      this.parent = parent;
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
+      // Skip any visible annotations as @KeepEdge is not runtime visible.
+      if (!visible && descriptor.equals(Edge.DESCRIPTOR)) {
+        return new KeepEdgeVisitor(parent);
+      }
+      return null;
+    }
+  }
+
+  // Interface for providing AST result(s) for a sub-tree back up to its parent.
+  private interface Parent<T> {
+    void accept(T result);
+  }
+
+  private abstract static class AnnotationVisitorBase extends AnnotationVisitor {
+
+    AnnotationVisitorBase() {
+      super(ASM_VERSION);
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      throw new KeepEdgeException("Unexpected value in @KeepEdge: " + name + " = " + value);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      throw new KeepEdgeException("Unexpected annotation in @KeepEdge: " + name);
+    }
+
+    @Override
+    public void visitEnum(String name, String descriptor, String value) {
+      throw new KeepEdgeException("Unexpected enum in @KeepEdge: " + name);
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      throw new KeepEdgeException("Unexpected array in @KeepEdge: " + name);
+    }
+  }
+
+  private static class KeepEdgeVisitor extends AnnotationVisitorBase {
+    private final Parent<KeepEdge> parent;
+    private final KeepEdge.Builder builder = KeepEdge.builder();
+
+    KeepEdgeVisitor(Parent<KeepEdge> parent) {
+      this.parent = parent;
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      if (name.equals(Edge.preconditions)) {
+        return new KeepPreconditionsVisitor(builder::setPreconditions);
+      }
+      if (name.equals(Edge.consequences)) {
+        return new KeepConsequencesVisitor(builder::setConsequences);
+      }
+      return super.visitArray(name);
+    }
+
+    @Override
+    public void visitEnd() {
+      parent.accept(builder.build());
+    }
+  }
+
+  private static class KeepPreconditionsVisitor extends AnnotationVisitorBase {
+    private final Parent<KeepPreconditions> parent;
+
+    public KeepPreconditionsVisitor(Parent<KeepPreconditions> parent) {
+      this.parent = parent;
+    }
+  }
+
+  private static class KeepConsequencesVisitor extends AnnotationVisitorBase {
+    private final Parent<KeepConsequences> parent;
+    private final KeepConsequences.Builder builder = KeepConsequences.builder();
+
+    public KeepConsequencesVisitor(Parent<KeepConsequences> parent) {
+      this.parent = parent;
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+      if (descriptor.equals(Target.DESCRIPTOR)) {
+        return new KeepTargetVisitor(builder::addTarget);
+      }
+      return super.visitAnnotation(name, descriptor);
+    }
+
+    @Override
+    public void visitEnd() {
+      parent.accept(builder.build());
+    }
+  }
+
+  private static class KeepTargetVisitor extends AnnotationVisitorBase {
+    private final Parent<KeepTarget> parent;
+    private KeepQualifiedClassNamePattern classNamePattern = null;
+    private KeepMethodNamePattern methodName = null;
+
+    public KeepTargetVisitor(Parent<KeepTarget> parent) {
+      this.parent = parent;
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (name.equals(Target.classConstant) && value instanceof Type) {
+        classNamePattern = KeepQualifiedClassNamePattern.exact(((Type) value).getClassName());
+        return;
+      }
+      if (name.equals(Target.methodName) && value instanceof String) {
+        methodName = KeepMethodNamePattern.exact((String) value);
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public void visitEnd() {
+      Builder itemBuilder = KeepItemPattern.builder();
+      if (classNamePattern != null) {
+        itemBuilder.setClassPattern(classNamePattern);
+      }
+      if (methodName != null) {
+        itemBuilder.setMembersPattern(
+            KeepMembersPattern.builder()
+                .addMethodPattern(KeepMethodPattern.builder().setNamePattern(methodName).build())
+                .build());
+      }
+      KeepTarget target = KeepTarget.builder().setItem(itemBuilder.build()).build();
+      parent.accept(target);
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
new file mode 100644
index 0000000..1641d8d
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -0,0 +1,112 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.asm;
+
+import com.android.tools.r8.keepanno.annotations.KeepConstants;
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Target;
+import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepPreconditions;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.utils.Unimplemented;
+import java.util.function.BiFunction;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+public class KeepEdgeWriter implements Opcodes {
+
+  public static void writeEdge(KeepEdge edge, ClassVisitor visitor) {
+    writeEdge(edge, visitor::visitAnnotation);
+  }
+
+  public static void writeEdge(
+      KeepEdge edge, BiFunction<String, Boolean, AnnotationVisitor> getVisitor) {
+    new KeepEdgeWriter().writeEdge(edge, getVisitor.apply(Edge.DESCRIPTOR, false));
+  }
+
+  private void writeEdge(KeepEdge edge, AnnotationVisitor visitor) {
+    writePreconditions(visitor, edge.getPreconditions());
+    writeConsequences(visitor, edge.getConsequences());
+    visitor.visitEnd();
+  }
+
+  private void writePreconditions(AnnotationVisitor visitor, KeepPreconditions preconditions) {
+    if (preconditions.isAlways()) {
+      return;
+    }
+    throw new Unimplemented();
+  }
+
+  private void writeConsequences(AnnotationVisitor visitor, KeepConsequences consequences) {
+    assert !consequences.isEmpty();
+    String ignoredArrayValueName = null;
+    AnnotationVisitor arrayVisitor = visitor.visitArray(KeepConstants.Edge.consequences);
+    consequences.forEachTarget(
+        target -> {
+          AnnotationVisitor targetVisitor =
+              arrayVisitor.visitAnnotation(ignoredArrayValueName, KeepConstants.Target.DESCRIPTOR);
+          // No options imply keep all.
+          if (!target.getOptions().isKeepAll()) {
+            throw new Unimplemented();
+          }
+          target
+              .getItem()
+              .match(
+                  () -> {
+                    throw new Unimplemented();
+                  },
+                  clazz -> {
+                    KeepQualifiedClassNamePattern namePattern = clazz.getClassNamePattern();
+                    if (namePattern.isExact()) {
+                      Type typeConstant = Type.getType(namePattern.getExactDescriptor());
+                      targetVisitor.visit(KeepConstants.Target.classConstant, typeConstant);
+                    } else {
+                      throw new Unimplemented();
+                    }
+                    if (!clazz.getExtendsPattern().isAny()) {
+                      throw new Unimplemented();
+                    }
+                    if (clazz.getMembersPattern().isNone()) {
+                      // Default is "no methods".
+                    } else if (clazz.getMembersPattern().isAll()) {
+                      throw new Unimplemented();
+                    } else {
+                      clazz
+                          .getMembersPattern()
+                          .forEach(
+                              field -> {
+                                throw new Unimplemented();
+                              },
+                              method -> {
+                                KeepMethodNamePattern methodNamePattern = method.getNamePattern();
+                                methodNamePattern.match(
+                                    () -> {
+                                      throw new Unimplemented();
+                                    },
+                                    exactMethodName -> {
+                                      targetVisitor.visit(Target.methodName, exactMethodName);
+                                      return null;
+                                    });
+                                if (!method.getAccessPattern().isAny()) {
+                                  throw new Unimplemented();
+                                }
+                                if (!method.getReturnTypePattern().isAny()) {
+                                  throw new Unimplemented();
+                                }
+                                if (!method.getParametersPattern().isAny()) {
+                                  throw new Unimplemented();
+                                }
+                              });
+                    }
+                    return null;
+                  });
+          targetVisitor.visitEnd();
+        });
+    arrayVisitor.visitEnd();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
similarity index 95%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
index acd327d..43b4176 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepCondition.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 /**
  * A keep condition is the content of an item in the set of preconditions.
@@ -41,5 +41,5 @@
 
   public KeepItemPattern getItemPattern() {
     return itemPattern;
-    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConsequences.java
similarity index 64%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConsequences.java
index 6a227fd..1322746 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepConsequences.java
@@ -1,11 +1,12 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * Set of consequences of a keep edge.
@@ -29,6 +30,9 @@
     }
 
     public KeepConsequences build() {
+      if (targets.isEmpty()) {
+        throw new KeepEdgeException("Invalid empty consequent set");
+      }
       return new KeepConsequences(targets);
     }
   }
@@ -40,6 +44,8 @@
   private final List<KeepTarget> targets;
 
   private KeepConsequences(List<KeepTarget> targets) {
+    assert targets != null;
+    assert !targets.isEmpty();
     this.targets = targets;
   }
 
@@ -50,4 +56,26 @@
   public void forEachTarget(Consumer<KeepTarget> fn) {
     targets.forEach(fn);
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepConsequences that = (KeepConsequences) o;
+    return targets.equals(that.targets);
+  }
+
+  @Override
+  public int hashCode() {
+    return targets.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return targets.stream().map(Object::toString).collect(Collectors.joining(", "));
+  }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
new file mode 100644
index 0000000..4da44aa
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdge.java
@@ -0,0 +1,125 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.ast;
+
+import java.util.Objects;
+
+/**
+ * An edge in the keep graph.
+ *
+ * <p>An edge describes a set of preconditions and a set of consequences. If the preconditions are
+ * met, then the consequences are put into effect.
+ *
+ * <p>Below is a BNF of the keep edge AST for reference. The non-terminals are written in ALL_CAPS,
+ * possibly-empty repeatable subexpressions denoted with SUB* and non-empty with SUB+
+ *
+ * <p>In the Java AST, the non-terminals are prefixed with 'Keep' and in CamelCase.
+ *
+ * <p>TODO(b/248408342): Update the BNF and AST to be complete.
+ *
+ * <pre>
+ *   EDGE ::= PRECONDITIONS -> CONSEQUENCES
+ *
+ *   PRECONDITIONS ::= always | CONDITION+
+ *   CONDITION ::= ITEM_PATTERN
+ *
+ *   CONSEQUENCES ::= TARGET+
+ *   TARGET ::= any | OPTIONS ITEM_PATTERN
+ *   OPTIONS ::= keep-all | OPTION+
+ *   OPTION ::= shrinking | optimizing | obfuscating | access-modifying
+ *
+ *   ITEM_PATTERN ::= any | CLASS_PATTERN
+ *   CLASS_PATTERN ::= QUALIFIED_CLASS_NAME_PATTERN extends EXTENDS_PATTERN { MEMBERS_PATTERN }
+ *
+ *   TYPE_PATTERN ::= any
+ *   PACKAGE_PATTERN ::= any | exact package-name
+ *   QUALIFIED_CLASS_NAME_PATTERN ::= any | PACKAGE_PATTERN | UNQUALIFIED_CLASS_NAME_PATTERN
+ *   UNQUALIFIED_CLASS_NAME_PATTERN ::= any | exact simple-class-name
+ *   EXTENDS_PATTERN ::= any | QUALIFIED_CLASS_NAME_PATTERN
+ *
+ *   MEMBERS_PATTERN ::= none | all | METHOD_PATTERN*
+ *
+ *   METHOD_PATTERN
+ *     ::= METHOD_ACCESS_PATTERN
+ *           METHOD_RETURN_TYPE_PATTERN
+ *           METHOD_NAME_PATTERN
+ *           METHOD_PARAMETERS_PATTERN
+ *
+ *   METHOD_ACCESS_PATTERN ::= any
+ *   METHOD_NAME_PATTERN ::= any | exact method-name
+ *   METHOD_RETURN_TYPE_PATTERN ::= void | TYPE_PATTERN
+ *   METHOD_PARAMETERS_PATTERN ::= any | none | TYPE_PATTERN+
+ * </pre>
+ */
+public final class KeepEdge {
+
+  public static class Builder {
+    private KeepPreconditions preconditions = KeepPreconditions.always();
+    private KeepConsequences consequences;
+
+    private Builder() {}
+
+    public Builder setPreconditions(KeepPreconditions preconditions) {
+      this.preconditions = preconditions;
+      return this;
+    }
+
+    public Builder setConsequences(KeepConsequences consequences) {
+      this.consequences = consequences;
+      return this;
+    }
+
+    public KeepEdge build() {
+      if (consequences.isEmpty()) {
+        throw new KeepEdgeException("KeepEdge must have non-empty set of consequences.");
+      }
+      return new KeepEdge(preconditions, consequences);
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  private final KeepPreconditions preconditions;
+  private final KeepConsequences consequences;
+
+  private KeepEdge(KeepPreconditions preconditions, KeepConsequences consequences) {
+    assert preconditions != null;
+    assert consequences != null;
+    this.preconditions = preconditions;
+    this.consequences = consequences;
+  }
+
+  public KeepPreconditions getPreconditions() {
+    return preconditions;
+  }
+
+  public KeepConsequences getConsequences() {
+    return consequences;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepEdge keepEdge = (KeepEdge) o;
+    return preconditions.equals(keepEdge.preconditions)
+        && consequences.equals(keepEdge.consequences);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(preconditions, consequences);
+  }
+
+  @Override
+  public String toString() {
+    return "KeepEdge{" + "preconditions=" + preconditions + ", consequences=" + consequences + '}';
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeException.java
similarity index 85%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeException.java
index 3345c8f..5abe947 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepEdgeException.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 public class KeepEdgeException extends RuntimeException {
 
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java
similarity index 67%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java
index 17e543f..b6b0fd7 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepExtendsPattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 /** Pattern for matching the "extends" or "implements" clause of a class. */
 public abstract class KeepExtendsPattern {
@@ -42,6 +42,21 @@
     public boolean isAny() {
       return true;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
   }
 
   private static class KeepExtendsClassPattern extends KeepExtendsPattern {
@@ -49,6 +64,7 @@
     private final KeepQualifiedClassNamePattern pattern;
 
     public KeepExtendsClassPattern(KeepQualifiedClassNamePattern pattern) {
+      assert pattern != null;
       this.pattern = pattern;
     }
 
@@ -56,6 +72,28 @@
     public boolean isAny() {
       return pattern.isAny();
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      KeepExtendsClassPattern that = (KeepExtendsClassPattern) o;
+      return pattern.equals(that.pattern);
+    }
+
+    @Override
+    public int hashCode() {
+      return pattern.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return pattern.toString();
+    }
   }
 
   public static Builder builder() {
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java
similarity index 86%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java
index 04e2697..8de46e1 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldPattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 public class KeepFieldPattern extends KeepMemberPattern {
 
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
similarity index 75%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
index 3c6f947..e5b0987 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepItemPattern.java
@@ -1,8 +1,9 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
+import java.util.Objects;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -87,6 +88,21 @@
     public <T> T match(Supplier<T> onAny, Function<KeepClassPattern, T> onItem) {
       return onAny.get();
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
   }
 
   public static class KeepClassPattern extends KeepItemPattern {
@@ -100,6 +116,9 @@
         KeepQualifiedClassNamePattern qualifiedClassPattern,
         KeepExtendsPattern extendsPattern,
         KeepMembersPattern membersPattern) {
+      assert qualifiedClassPattern != null;
+      assert extendsPattern != null;
+      assert membersPattern != null;
       this.qualifiedClassPattern = qualifiedClassPattern;
       this.extendsPattern = extendsPattern;
       this.membersPattern = membersPattern;
@@ -130,6 +149,37 @@
     public KeepMembersPattern getMembersPattern() {
       return membersPattern;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (obj == null || getClass() != obj.getClass()) {
+        return false;
+      }
+      KeepClassPattern that = (KeepClassPattern) obj;
+      return qualifiedClassPattern.equals(that.qualifiedClassPattern)
+          && extendsPattern.equals(that.extendsPattern)
+          && membersPattern.equals(that.membersPattern);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(qualifiedClassPattern, extendsPattern, membersPattern);
+    }
+
+    @Override
+    public String toString() {
+      return "KeepClassPattern{"
+          + "qualifiedClassPattern="
+          + qualifiedClassPattern
+          + ", extendsPattern="
+          + extendsPattern
+          + ", membersPattern="
+          + membersPattern
+          + '}';
+    }
   }
 
   public abstract boolean isAny();
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
similarity index 93%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
index 73bae7e..bd716e9 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMemberPattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 public abstract class KeepMemberPattern {
 
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMembersPattern.java
similarity index 73%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMembersPattern.java
index 6d3f93e..be5af32 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMembersPattern.java
@@ -1,12 +1,14 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
-import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.keepanno.utils.Unimplemented;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 public abstract class KeepMembersPattern {
 
@@ -89,6 +91,21 @@
     public void forEach(Consumer<KeepFieldPattern> onField, Consumer<KeepMethodPattern> onMethod) {
       throw new Unimplemented("Should this include all and none?");
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
   }
 
   private static class KeepMembersNonePattern extends KeepMembersPattern {
@@ -116,6 +133,21 @@
     public void forEach(Consumer<KeepFieldPattern> onField, Consumer<KeepMethodPattern> onMethod) {
       throw new Unimplemented("Should this include all and none?");
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "<none>";
+    }
   }
 
   private static class KeepMembersSomePattern extends KeepMembersPattern {
@@ -146,6 +178,33 @@
       fields.forEach(onField);
       methods.forEach(onMethod);
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (obj == null || getClass() != obj.getClass()) {
+        return false;
+      }
+      KeepMembersSomePattern that = (KeepMembersSomePattern) obj;
+      return methods.equals(that.methods) && fields.equals(that.fields);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(methods, fields);
+    }
+
+    @Override
+    public String toString() {
+      return "KeepMembersSomePattern{"
+          + "methods={"
+          + methods.stream().map(Object::toString).collect(Collectors.joining(", "))
+          + "}, fields={"
+          + fields.stream().map(Object::toString).collect(Collectors.joining(", "))
+          + "}}";
+    }
   }
 
   private KeepMembersPattern() {}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodAccessPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodAccessPattern.java
new file mode 100644
index 0000000..fd0fd3e
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodAccessPattern.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.ast;
+
+// TODO: finish this.
+public abstract class KeepMethodAccessPattern {
+
+  public static KeepMethodAccessPattern any() {
+    return Any.getInstance();
+  }
+
+  public abstract boolean isAny();
+
+  private static class Any extends KeepMethodAccessPattern {
+
+    private static Any INSTANCE = null;
+
+    private static Any getInstance() {
+      if (INSTANCE == null) {
+        INSTANCE = new Any();
+      }
+      return INSTANCE;
+    }
+
+    @Override
+    public boolean isAny() {
+      return true;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
similarity index 68%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
index ada6701..6dc373d 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -47,12 +47,28 @@
     public <T> T match(Supplier<T> onAny, Function<String, T> onExact) {
       return onAny.get();
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
   }
 
   private static class KeepMethodNameExactPattern extends KeepMethodNamePattern {
     private final String name;
 
     public KeepMethodNameExactPattern(String name) {
+      assert name != null;
       this.name = name;
     }
 
@@ -60,5 +76,27 @@
     public <T> T match(Supplier<T> onAny, Function<String, T> onExact) {
       return onExact.apply(name);
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      KeepMethodNameExactPattern that = (KeepMethodNameExactPattern) o;
+      return name.equals(that.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return name.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
similarity index 70%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
index 4f42202..83b4606 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodParametersPattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 import java.util.Collections;
 import java.util.List;
@@ -22,6 +22,10 @@
 
   public abstract <T> T match(Supplier<T> onAny, Function<List<KeepTypePattern>, T> onList);
 
+  public boolean isAny() {
+    return match(() -> true, params -> false);
+  }
+
   private static class None extends KeepMethodParametersPattern {
     private static None INSTANCE = null;
 
@@ -36,6 +40,21 @@
     public <T> T match(Supplier<T> onAny, Function<List<KeepTypePattern>, T> onList) {
       return onList.apply(Collections.emptyList());
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "()";
+    }
   }
 
   private static class Any extends KeepMethodParametersPattern {
@@ -52,5 +71,20 @@
     public <T> T match(Supplier<T> onAny, Function<List<KeepTypePattern>, T> onList) {
       return onAny.get();
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "(...)";
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
similarity index 71%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
index 5db7d26..b3ec418 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
@@ -1,7 +1,9 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
+
+import java.util.Objects;
 
 public final class KeepMethodPattern extends KeepMemberPattern {
 
@@ -62,6 +64,10 @@
       KeepMethodNamePattern namePattern,
       KeepMethodReturnTypePattern returnTypePattern,
       KeepMethodParametersPattern parametersPattern) {
+    assert accessPattern != null;
+    assert namePattern != null;
+    assert returnTypePattern != null;
+    assert parametersPattern != null;
     this.accessPattern = accessPattern;
     this.namePattern = namePattern;
     this.returnTypePattern = returnTypePattern;
@@ -87,4 +93,38 @@
   public KeepMethodParametersPattern getParametersPattern() {
     return parametersPattern;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepMethodPattern that = (KeepMethodPattern) o;
+    return accessPattern.equals(that.accessPattern)
+        && namePattern.equals(that.namePattern)
+        && returnTypePattern.equals(that.returnTypePattern)
+        && parametersPattern.equals(that.parametersPattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(accessPattern, namePattern, returnTypePattern, parametersPattern);
+  }
+
+  @Override
+  public String toString() {
+    return "KeepMethodPattern{"
+        + "access="
+        + accessPattern
+        + ", name="
+        + namePattern
+        + ", returnType="
+        + returnTypePattern
+        + ", parameters="
+        + parametersPattern
+        + '}';
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
similarity index 63%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
index 4e7b5b9..f711bc0 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodReturnTypePattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -23,6 +23,10 @@
 
   public abstract <T> T match(Supplier<T> onVoid, Function<KeepTypePattern, T> onType);
 
+  public boolean isAny() {
+    return match(() -> false, KeepTypePattern::isAny);
+  }
+
   private static class VoidType extends KeepMethodReturnTypePattern {
     private static VoidType INSTANCE = null;
 
@@ -37,6 +41,21 @@
     public <T> T match(Supplier<T> onVoid, Function<KeepTypePattern, T> onType) {
       return onVoid.get();
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "void";
+    }
   }
 
   private static class SomeType extends KeepMethodReturnTypePattern {
@@ -44,6 +63,7 @@
     private final KeepTypePattern typePattern;
 
     private SomeType(KeepTypePattern typePattern) {
+      assert typePattern != null;
       this.typePattern = typePattern;
     }
 
@@ -51,5 +71,27 @@
     public <T> T match(Supplier<T> onVoid, Function<KeepTypePattern, T> onType) {
       return onType.apply(typePattern);
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      SomeType someType = (SomeType) o;
+      return typePattern.equals(someType.typePattern);
+    }
+
+    @Override
+    public int hashCode() {
+      return typePattern.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return typePattern.toString();
+    }
   }
 }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
new file mode 100644
index 0000000..21b7487
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepOptions.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.ast;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public final class KeepOptions {
+
+  public boolean isKeepAll() {
+    return allowedOptions.isEmpty();
+  }
+
+  public enum KeepOption {
+    SHRINKING,
+    OPTIMIZING,
+    OBFUSCATING,
+    ACCESS_MODIFYING,
+  }
+
+  public static KeepOptions keepAll() {
+    if (ALLOW_NONE_INSTANCE == null) {
+      ALLOW_NONE_INSTANCE = new KeepOptions(ImmutableSet.of());
+    }
+    return ALLOW_NONE_INSTANCE;
+  }
+
+  public static Builder allowBuilder() {
+    return new Builder(true);
+  }
+
+  public static Builder disallowBuilder() {
+    return new Builder(false);
+  }
+
+  public static KeepOptions allow(KeepOption... options) {
+    return allowBuilder().addAll(options).build();
+  }
+
+  public static KeepOptions disallow(KeepOption... options) {
+    return disallowBuilder().addAll(options).build();
+  }
+
+  public static class Builder {
+    public final boolean allowIfSet;
+    public Set<KeepOption> options = new HashSet<>();
+
+    private Builder(boolean allowIfSet) {
+      this.allowIfSet = allowIfSet;
+    }
+
+    public Builder add(KeepOption option) {
+      options.add(option);
+      return this;
+    }
+
+    public Builder addAll(KeepOption... options) {
+      return addAll(Arrays.asList(options));
+    }
+
+    public Builder addAll(Collection<KeepOption> options) {
+      this.options.addAll(options);
+      return this;
+    }
+
+    public KeepOptions build() {
+      // Fast path check for the two variants of "keep all".
+      if (options.isEmpty()) {
+        if (allowIfSet) {
+          return keepAll();
+        }
+        throw new KeepEdgeException("Invalid keep options that disallow nothing.");
+      }
+      if (options.size() == KeepOption.values().length) {
+        if (!allowIfSet) {
+          return keepAll();
+        }
+        throw new KeepEdgeException("Invalid keep options that allow everything.");
+      }
+      // The normalized options is the "allow variant", if not of that form invert it on build.
+      if (allowIfSet) {
+        return new KeepOptions(ImmutableSet.copyOf(options));
+      }
+      ImmutableSet.Builder<KeepOption> invertedOptions = ImmutableSet.builder();
+      for (KeepOption option : KeepOption.values()) {
+        if (!options.contains(option)) {
+          invertedOptions.add(option);
+        }
+      }
+      return new KeepOptions(invertedOptions.build());
+    }
+  }
+
+  private static KeepOptions ALLOW_NONE_INSTANCE = null;
+
+  private final ImmutableSet<KeepOption> allowedOptions;
+
+  private KeepOptions(ImmutableSet<KeepOption> options) {
+    this.allowedOptions = options;
+  }
+
+  public boolean isAllowed(KeepOption option) {
+    return allowedOptions.contains(option);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepOptions that = (KeepOptions) o;
+    return allowedOptions.equals(that.allowedOptions);
+  }
+
+  @Override
+  public int hashCode() {
+    return allowedOptions.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return "KeepOptions{"
+        + allowedOptions.stream().map(Objects::toString).collect(Collectors.joining(", "))
+        + '}';
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java
similarity index 93%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java
index 10db385..7d23490 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 public abstract class KeepPackagePattern {
 
@@ -88,6 +88,11 @@
     public int hashCode() {
       return System.identityHashCode(this);
     }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
   }
 
   private static final class KeepPackageTopPattern extends KeepPackageExactPattern {
@@ -114,6 +119,11 @@
     public boolean isTop() {
       return true;
     }
+
+    @Override
+    public String toString() {
+      return "";
+    }
   }
 
   public static class KeepPackageExactPattern extends KeepPackagePattern {
@@ -166,6 +176,11 @@
     public int hashCode() {
       return fullPackage.hashCode();
     }
+
+    @Override
+    public String toString() {
+      return fullPackage;
+    }
   }
 
   public abstract boolean isAny();
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPreconditions.java
similarity index 69%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPreconditions.java
index f3083df..f672013 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPreconditions.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -59,6 +59,21 @@
     public void forEach(Consumer<KeepCondition> fn) {
       // Empty.
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "true";
+    }
   }
 
   private static class KeepPreconditionsSome extends KeepPreconditions {
@@ -66,6 +81,8 @@
     private final List<KeepCondition> preconditions;
 
     private KeepPreconditionsSome(List<KeepCondition> preconditions) {
+      assert preconditions != null;
+      assert !preconditions.isEmpty();
       this.preconditions = preconditions;
     }
 
@@ -78,5 +95,27 @@
     public void forEach(Consumer<KeepCondition> fn) {
       preconditions.forEach(fn);
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      KeepPreconditionsSome that = (KeepPreconditionsSome) o;
+      return preconditions.equals(that.preconditions);
+    }
+
+    @Override
+    public int hashCode() {
+      return preconditions.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return preconditions.toString();
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java
similarity index 82%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java
index 9485580..97d01b9 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 import java.util.Objects;
 
@@ -74,6 +74,21 @@
     return packagePattern.isAny() && namePattern.isAny();
   }
 
+  public boolean isExact() {
+    return packagePattern.isExact() && namePattern.isExact();
+  }
+
+  public String getExactDescriptor() {
+    if (!isExact()) {
+      throw new KeepEdgeException("Attempt to obtain exact qualified type for inexact pattern");
+    }
+    return 'L'
+        + packagePattern.asExact().getExactPackageAsString().replace('.', '/')
+        + (packagePattern.isTop() ? "" : "/")
+        + namePattern.asExact().getExactNameAsString()
+        + ';';
+  }
+
   public KeepPackagePattern getPackagePattern() {
     return packagePattern;
   }
@@ -98,4 +113,9 @@
   public int hashCode() {
     return Objects.hash(packagePattern.hashCode(), namePattern.hashCode());
   }
+
+  @Override
+  public String toString() {
+    return packagePattern + (packagePattern.isTop() ? "" : ".") + namePattern;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
similarity index 67%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
index a63a34a..111022b 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTarget.java
@@ -1,7 +1,9 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
+
+import java.util.Objects;
 
 public class KeepTarget {
 
@@ -38,6 +40,8 @@
   private final KeepOptions options;
 
   private KeepTarget(KeepItemPattern item, KeepOptions options) {
+    assert item != null;
+    assert options != null;
     this.item = item;
     this.options = options;
   }
@@ -53,4 +57,26 @@
   public KeepOptions getOptions() {
     return options;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepTarget that = (KeepTarget) o;
+    return item.equals(that.item) && options.equals(that.options);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(item, options);
+  }
+
+  @Override
+  public String toString() {
+    return "KeepTarget{" + "item=" + item + ", options=" + options + '}';
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTypePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
similarity index 69%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTypePattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
index eff9e9c..a790094 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTypePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepTypePattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 public abstract class KeepTypePattern {
 
@@ -23,6 +23,21 @@
     public boolean isAny() {
       return true;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
   }
 
   public abstract boolean isAny();
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepUnqualfiedClassNamePattern.java
similarity index 93%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepUnqualfiedClassNamePattern.java
index ab38ed2..c9ac380 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepUnqualfiedClassNamePattern.java
@@ -1,7 +1,7 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 public abstract class KeepUnqualfiedClassNamePattern {
 
@@ -71,6 +71,11 @@
     public int hashCode() {
       return System.identityHashCode(this);
     }
+
+    @Override
+    public String toString() {
+      return "*";
+    }
   }
 
   public static class KeepClassNameExactPattern extends KeepUnqualfiedClassNamePattern {
@@ -117,6 +122,11 @@
     public int hashCode() {
       return className.hashCode();
     }
+
+    @Override
+    public String toString() {
+      return className;
+    }
   }
 
   public abstract boolean isAny();
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
similarity index 83%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java
rename to src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index 6c24ec1..d9b75ae 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -1,32 +1,31 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.keeprules;
+package com.android.tools.r8.keepanno.keeprules;
 
-import com.android.tools.r8.errors.Unimplemented;
-import com.android.tools.r8.experimental.keepanno.ast.KeepConsequences;
-import com.android.tools.r8.experimental.keepanno.ast.KeepEdge;
-import com.android.tools.r8.experimental.keepanno.ast.KeepFieldPattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepItemPattern.KeepClassPattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepMembersPattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepMethodAccessPattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepMethodNamePattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepMethodParametersPattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepMethodPattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepMethodReturnTypePattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepOptions;
-import com.android.tools.r8.experimental.keepanno.ast.KeepOptions.KeepOption;
-import com.android.tools.r8.experimental.keepanno.ast.KeepPackagePattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepPreconditions;
-import com.android.tools.r8.experimental.keepanno.ast.KeepQualifiedClassNamePattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepTarget;
-import com.android.tools.r8.experimental.keepanno.ast.KeepTypePattern;
-import com.android.tools.r8.experimental.keepanno.ast.KeepUnqualfiedClassNamePattern;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern.KeepClassPattern;
+import com.android.tools.r8.keepanno.ast.KeepMembersPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodAccessPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodParametersPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodReturnTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepOptions;
+import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
+import com.android.tools.r8.keepanno.ast.KeepPackagePattern;
+import com.android.tools.r8.keepanno.ast.KeepPreconditions;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepTarget;
+import com.android.tools.r8.keepanno.ast.KeepTypePattern;
+import com.android.tools.r8.keepanno.ast.KeepUnqualfiedClassNamePattern;
+import com.android.tools.r8.keepanno.utils.Unimplemented;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 public class KeepRuleExtractor {
 
@@ -151,7 +150,11 @@
       StringBuilder builder, KeepMethodParametersPattern parametersPattern) {
     return parametersPattern.match(
         () -> builder.append("(***)"),
-        list -> StringUtils.append(builder, list, ", ", BraceType.PARENS));
+        list ->
+            builder
+                .append('(')
+                .append(list.stream().map(Object::toString).collect(Collectors.joining(", ")))
+                .append(')'));
   }
 
   private static StringBuilder printMethodName(
@@ -220,7 +223,7 @@
         return "optimization";
       case OBFUSCATING:
         return "obfuscation";
-      case ACCESS_MODIFING:
+      case ACCESS_MODIFYING:
         return "accessmodification";
       default:
         throw new Unimplemented();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
new file mode 100644
index 0000000..36bbaeb
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
@@ -0,0 +1,279 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.processor;
+
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+
+import com.android.tools.r8.keepanno.annotations.KeepConstants;
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Target;
+import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.keepanno.asm.KeepEdgeWriter;
+import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepEdge.Builder;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMembersPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepTarget;
+import com.android.tools.r8.keepanno.utils.Unimplemented;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.SimpleAnnotationValueVisitor7;
+import javax.lang.model.util.SimpleTypeVisitor7;
+import javax.tools.Diagnostic.Kind;
+import javax.tools.JavaFileObject;
+import org.objectweb.asm.ClassWriter;
+
+@SupportedAnnotationTypes("com.android.tools.r8.keepanno.annotations.*")
+@SupportedSourceVersion(SourceVersion.RELEASE_7)
+public class KeepEdgeProcessor extends AbstractProcessor {
+
+  public static String getClassTypeNameForSynthesizedEdges(String classTypeName) {
+    return classTypeName + "$$KeepEdges";
+  }
+
+  @Override
+  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+    for (Element rootElement : roundEnv.getRootElements()) {
+      TypeElement typeElement = getEnclosingTypeElement(rootElement);
+      KeepEdge edge = processKeepEdge(typeElement, roundEnv);
+      if (edge != null) {
+        String edgeTargetClass =
+            getClassTypeNameForSynthesizedEdges(typeElement.getQualifiedName().toString());
+        byte[] writtenEdge = writeEdge(edge, edgeTargetClass);
+        Filer filer = processingEnv.getFiler();
+        try {
+          JavaFileObject classFile = filer.createClassFile(edgeTargetClass);
+          classFile.openOutputStream().write(writtenEdge);
+        } catch (IOException e) {
+          error(e.getMessage());
+        }
+      }
+    }
+    return true;
+  }
+
+  private static byte[] writeEdge(KeepEdge edge, String classTypeName) {
+    String classBinaryName = KeepConstants.getBinaryNameFromClassTypeName(classTypeName);
+    ClassWriter classWriter = new ClassWriter(0);
+    classWriter.visit(
+        KeepEdgeReader.ASM_VERSION,
+        ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
+        classBinaryName,
+        null,
+        "java/lang/Object",
+        null);
+    classWriter.visitSource("SynthesizedKeepEdge", null);
+    KeepEdgeWriter.writeEdge(edge, classWriter);
+    classWriter.visitEnd();
+    return classWriter.toByteArray();
+  }
+
+  private KeepEdge processKeepEdge(TypeElement keepEdge, RoundEnvironment roundEnv) {
+    AnnotationMirror mirror = getAnnotationMirror(keepEdge, KeepConstants.Edge.CLASS);
+    if (mirror == null) {
+      return null;
+    }
+    Builder edgeBuilder = KeepEdge.builder();
+    processPreconditions(edgeBuilder, mirror);
+    processConsequences(edgeBuilder, mirror);
+    return edgeBuilder.build();
+  }
+
+  private void processPreconditions(Builder edgeBuilder, AnnotationMirror mirror) {
+    AnnotationValue preconditions = getAnnotationValue(mirror, Edge.preconditions);
+    if (preconditions == null) {
+      return;
+    }
+    throw new Unimplemented();
+  }
+
+  private void processConsequences(Builder edgeBuilder, AnnotationMirror mirror) {
+    AnnotationValue consequences = getAnnotationValue(mirror, Edge.consequences);
+    if (consequences == null) {
+      return;
+    }
+    KeepConsequences.Builder consequencesBuilder = KeepConsequences.builder();
+    new AnnotationListValueVisitor(
+            value -> {
+              KeepTarget.Builder targetBuilder = KeepTarget.builder();
+              processTarget(targetBuilder, AnnotationMirrorValueVisitor.getMirror(value));
+              consequencesBuilder.addTarget(targetBuilder.build());
+            })
+        .onValue(consequences);
+    edgeBuilder.setConsequences(consequencesBuilder.build());
+  }
+
+  private void processTarget(KeepTarget.Builder builder, AnnotationMirror mirror) {
+    KeepItemPattern.Builder itemBuilder = KeepItemPattern.builder();
+    AnnotationValue classConstantValue = getAnnotationValue(mirror, Target.classConstant);
+    if (classConstantValue != null) {
+      DeclaredType type = AnnotationClassValueVisitor.getType(classConstantValue);
+      itemBuilder.setClassPattern(KeepQualifiedClassNamePattern.exact(type.toString()));
+    }
+    AnnotationValue methodNameValue = getAnnotationValue(mirror, Target.methodName);
+    if (methodNameValue != null) {
+      String methodName = AnnotationStringValueVisitor.getString(methodNameValue);
+      itemBuilder.setMembersPattern(
+          KeepMembersPattern.builder()
+              .addMethodPattern(
+                  KeepMethodPattern.builder()
+                      .setNamePattern(KeepMethodNamePattern.exact(methodName))
+                      .build())
+              .build());
+    }
+
+    builder.setItem(itemBuilder.build());
+  }
+
+  private void error(String message) {
+    processingEnv.getMessager().printMessage(Kind.ERROR, message);
+  }
+
+  private static TypeElement getEnclosingTypeElement(Element element) {
+    while (true) {
+      if (element == null || element instanceof TypeElement) {
+        return (TypeElement) element;
+      }
+      element = element.getEnclosingElement();
+    }
+  }
+
+  private static AnnotationMirror getAnnotationMirror(TypeElement typeElement, Class<?> clazz) {
+    String clazzName = clazz.getName();
+    for (AnnotationMirror m : typeElement.getAnnotationMirrors()) {
+      if (m.getAnnotationType().toString().equals(clazzName)) {
+        return m;
+      }
+    }
+    return null;
+  }
+
+  private static AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String key) {
+    for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry :
+        annotationMirror.getElementValues().entrySet()) {
+      if (entry.getKey().getSimpleName().toString().equals(key)) {
+        return entry.getValue();
+      }
+    }
+    return null;
+  }
+
+  /// Annotation Visitors
+
+  private abstract static class AnnotationValueVisitorBase<T>
+      extends SimpleAnnotationValueVisitor7<T, Object> {
+    @Override
+    protected T defaultAction(Object o1, Object o2) {
+      throw new IllegalStateException();
+    }
+
+    public T onValue(AnnotationValue value) {
+      return value.accept(this, null);
+    }
+  }
+
+  private static class AnnotationListValueVisitor
+      extends AnnotationValueVisitorBase<AnnotationListValueVisitor> {
+
+    private final Consumer<AnnotationValue> fn;
+
+    public AnnotationListValueVisitor(Consumer<AnnotationValue> fn) {
+      this.fn = fn;
+    }
+
+    @Override
+    public AnnotationListValueVisitor visitArray(
+        List<? extends AnnotationValue> values, Object ignore) {
+      values.forEach(fn);
+      return this;
+    }
+  }
+
+  private static class AnnotationMirrorValueVisitor
+      extends AnnotationValueVisitorBase<AnnotationMirrorValueVisitor> {
+
+    private AnnotationMirror mirror = null;
+
+    public static AnnotationMirror getMirror(AnnotationValue value) {
+      return new AnnotationMirrorValueVisitor().onValue(value).mirror;
+    }
+
+    @Override
+    public AnnotationMirrorValueVisitor visitAnnotation(AnnotationMirror mirror, Object o) {
+      this.mirror = mirror;
+      return this;
+    }
+  }
+
+  private static class AnnotationStringValueVisitor
+      extends AnnotationValueVisitorBase<AnnotationStringValueVisitor> {
+    private String string;
+
+    public static String getString(AnnotationValue value) {
+      return new AnnotationStringValueVisitor().onValue(value).string;
+    }
+
+    @Override
+    public AnnotationStringValueVisitor visitString(String string, Object ignore) {
+      this.string = string;
+      return this;
+    }
+  }
+
+  private static class AnnotationClassValueVisitor
+      extends AnnotationValueVisitorBase<AnnotationClassValueVisitor> {
+    private DeclaredType type = null;
+
+    public static DeclaredType getType(AnnotationValue value) {
+      return new AnnotationClassValueVisitor().onValue(value).type;
+    }
+
+    @Override
+    public AnnotationClassValueVisitor visitType(TypeMirror t, Object ignore) {
+      ClassTypeVisitor classTypeVisitor = new ClassTypeVisitor();
+      t.accept(classTypeVisitor, null);
+      type = classTypeVisitor.type;
+      return this;
+    }
+  }
+
+  private static class TypeVisitorBase<T> extends SimpleTypeVisitor7<T, Object> {
+    @Override
+    protected T defaultAction(TypeMirror typeMirror, Object ignore) {
+      throw new IllegalStateException();
+    }
+  }
+
+  private static class ClassTypeVisitor extends TypeVisitorBase<ClassTypeVisitor> {
+    private DeclaredType type = null;
+
+    @Override
+    public ClassTypeVisitor visitDeclared(DeclaredType t, Object ignore) {
+      this.type = t;
+      return this;
+    }
+  }
+}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/utils/Unimplemented.java b/src/keepanno/java/com/android/tools/r8/keepanno/utils/Unimplemented.java
new file mode 100644
index 0000000..8293126
--- /dev/null
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/utils/Unimplemented.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.utils;
+
+public class Unimplemented extends RuntimeException {
+  public Unimplemented() {}
+
+  public Unimplemented(String msg) {
+    super(msg);
+  }
+}
diff --git a/src/library_desugar/java/java/nio/file/FileApiFlips.java b/src/library_desugar/java/java/nio/file/FileApiFlips.java
index b68aeb2..9c8a46a 100644
--- a/src/library_desugar/java/java/nio/file/FileApiFlips.java
+++ b/src/library_desugar/java/java/nio/file/FileApiFlips.java
@@ -101,32 +101,35 @@
     throw exception("java.nio.file.attribute.FileTime", suffix);
   }
 
+  public static Object flipMaybeFileTime(Object val) {
+    if (val instanceof j$.nio.file.attribute.FileTime) {
+      j$.nio.file.attribute.FileTime fileTime;
+      try {
+        fileTime = (j$.nio.file.attribute.FileTime) val;
+      } catch (ClassCastException cce) {
+        throw exceptionFileTime(cce);
+      }
+      return FileAttributeConversions.convert(fileTime);
+    }
+    if (val instanceof java.nio.file.attribute.FileTime) {
+      java.nio.file.attribute.FileTime fileTime;
+      try {
+        fileTime = (java.nio.file.attribute.FileTime) val;
+      } catch (ClassCastException cce) {
+        throw exceptionFileTime(cce);
+      }
+      return FileAttributeConversions.convert(fileTime);
+    }
+    return val;
+  }
+
   public static Map<String, Object> flipMapWithMaybeFileTimeValues(Map<String, Object> in) {
     if (in == null || in.isEmpty()) {
       return in;
     }
     HashMap<String, Object> newMap = new HashMap<>();
     for (String key : in.keySet()) {
-      Object val = in.get(key);
-      if (val instanceof j$.nio.file.attribute.FileTime) {
-        j$.nio.file.attribute.FileTime fileTime;
-        try {
-          fileTime = (j$.nio.file.attribute.FileTime) val;
-        } catch (ClassCastException cce) {
-          throw exceptionFileTime(cce);
-        }
-        newMap.put(key, FileAttributeConversions.convert(fileTime));
-      } else if (val instanceof java.nio.file.attribute.FileTime) {
-        java.nio.file.attribute.FileTime fileTime;
-        try {
-          fileTime = (java.nio.file.attribute.FileTime) val;
-        } catch (ClassCastException cce) {
-          throw exceptionFileTime(cce);
-        }
-        newMap.put(key, FileAttributeConversions.convert(fileTime));
-      } else {
-        newMap.put(key, val);
-      }
+      newMap.put(key, flipMaybeFileTime(in.get(key)));
     }
     return newMap;
   }
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
index ff05a80..c97bd79 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
@@ -7,6 +7,13 @@
   "common_flags": [
     {
       "api_level_below_or_equal": 10000,
+      "amend_library_method": [
+        "public static java.nio.file.Path java.nio.file.Path#of(java.lang.String, java.lang.String[])",
+        "public static java.nio.file.Path java.nio.file.Path#of(java.net.URI)"
+      ]
+    },
+    {
+      "api_level_below_or_equal": 10000,
       "api_level_greater_or_equal": 24,
       "rewrite_prefix": {
         "java.util.stream.DesugarDoubleStream": "j$.util.stream.DesugarDoubleStream",
@@ -35,13 +42,15 @@
         "long java.util.concurrent.TimeUnit#convert(java.time.Duration)": "java.util.concurrent.DesugarTimeUnit"
       },
       "retarget_method_with_emulated_dispatch": {
-        "long java.io.InputStream#transferTo(java.io.OutputStream)": "java.io.DesugarInputStream"
+        "long java.io.InputStream#transferTo(java.io.OutputStream)": "java.io.DesugarInputStream",
+        "long java.io.ByteArrayInputStream#transferTo(java.io.OutputStream)": "long java.io.DesugarInputStream#transferTo(java.io.InputStream, java.io.OutputStream)"
       },
       "amend_library_method": [
         "public static java.util.concurrent.TimeUnit java.util.concurrent.TimeUnit#of(java.time.temporal.ChronoUnit)",
         "public java.time.temporal.ChronoUnit java.util.concurrent.TimeUnit#toChronoUnit()",
         "public long java.util.concurrent.TimeUnit#convert(java.time.Duration)",
-        "public long java.io.InputStream#transferTo(java.io.OutputStream)"
+        "public long java.io.InputStream#transferTo(java.io.OutputStream)",
+        "public long java.io.ByteArrayInputStream#transferTo(java.io.OutputStream)"
       ]
     },
     {
@@ -220,7 +229,8 @@
         "java.util.Map java.nio.file.spi.FileSystemProvider#readAttributes(java.nio.file.Path, java.lang.String, java.nio.file.LinkOption[])" : [-1, "java.util.Map java.nio.file.FileApiFlips#flipMapWithMaybeFileTimeValues(java.util.Map)"],
         "java.lang.Iterable java.nio.file.FileSystem#getRootDirectories()": [-1, "java.lang.Iterable java.nio.file.PathApiFlips#flipIterablePath(java.lang.Iterable)"],
         "java.util.Iterator java.nio.file.Path#iterator()": [-1, "java.util.Iterator java.nio.file.PathApiFlips#flipIteratorPath(java.util.Iterator)"],
-        "java.nio.file.DirectoryStream java.nio.file.spi.FileSystemProvider#newDirectoryStream(java.nio.file.Path, java.nio.file.DirectoryStream$Filter)": [-1, "java.nio.file.DirectoryStream java.nio.file.PathApiFlips#flipDirectoryStreamPath(java.nio.file.DirectoryStream)", 1, "java.nio.file.DirectoryStream$Filter java.nio.file.PathApiFlips#flipDirectoryStreamFilterPath(java.nio.file.DirectoryStream$Filter)"]
+        "java.nio.file.DirectoryStream java.nio.file.spi.FileSystemProvider#newDirectoryStream(java.nio.file.Path, java.nio.file.DirectoryStream$Filter)": [-1, "java.nio.file.DirectoryStream java.nio.file.PathApiFlips#flipDirectoryStreamPath(java.nio.file.DirectoryStream)", 1, "java.nio.file.DirectoryStream$Filter java.nio.file.PathApiFlips#flipDirectoryStreamFilterPath(java.nio.file.DirectoryStream$Filter)"],
+        "void java.nio.file.spi.FileSystemProvider#setAttribute(java.nio.file.Path, java.lang.String, java.lang.Object, java.nio.file.LinkOption[])": [2, "java.lang.Object java.nio.file.FileApiFlips#flipMaybeFileTime(java.lang.Object)"]
       },
       "wrapper_conversion": [
         "java.nio.channels.CompletionHandler",
@@ -403,6 +413,14 @@
       ]
     },
     {
+      "api_level_below_or_equal": 10000,
+      "api_level_greater_or_equal": 26,
+      "retarget_method": {
+        "java.nio.file.Path java.nio.file.Path#of(java.lang.String, java.lang.String[])": "java.nio.file.Path java.nio.file.Paths#get(java.lang.String, java.lang.String[])",
+        "java.nio.file.Path java.nio.file.Path#of(java.net.URI)": "java.nio.file.Path java.nio.file.Paths#get(java.net.URI)"
+      }
+    },
+    {
       "api_level_below_or_equal": 33,
       "amend_library_method": [
         "public java.lang.Object[] java.util.Collection#toArray(java.util.function.IntFunction)"
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
index 249de08..84334f5 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
@@ -131,7 +131,8 @@
       String owner = namingLens.lookupInternalName(rewrittenMethod.holder);
       String name = namingLens.lookupName(rewrittenMethod).toString();
       String desc = rewrittenMethod.proto.toDescriptorString(namingLens);
-      visitor.visitMethodInsn(rewrittenType.getCfOpcode(), owner, name, desc, itf);
+      visitor.visitMethodInsn(
+          rewrittenType.getCfOpcode(), owner, name, desc, rewrittenType.isInterface() || itf);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
deleted file mode 100644
index f61483a..0000000
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
-
-/**
- * An edge in the keep graph.
- *
- * <p>An edge describes a set of preconditions and a set of consequences. If the preconditions are
- * met, then the consequences are put into effect.
- */
-public final class KeepEdge {
-
-  public static class Builder {
-    private KeepPreconditions preconditions = KeepPreconditions.always();
-    private KeepConsequences consequences;
-
-    private Builder() {}
-
-    public Builder setPreconditions(KeepPreconditions preconditions) {
-      this.preconditions = preconditions;
-      return this;
-    }
-
-    public Builder setConsequences(KeepConsequences consequences) {
-      this.consequences = consequences;
-      return this;
-    }
-
-    public KeepEdge build() {
-      if (consequences.isEmpty()) {
-        throw new KeepEdgeException("KeepEdge must have non-empty set of consequences.");
-      }
-      return new KeepEdge(preconditions, consequences);
-    }
-  }
-
-  public static Builder builder() {
-    return new Builder();
-  }
-
-  private final KeepPreconditions preconditions;
-  private final KeepConsequences consequences;
-
-  private KeepEdge(KeepPreconditions preconditions, KeepConsequences consequences) {
-    this.preconditions = preconditions;
-    this.consequences = consequences;
-  }
-
-  public KeepPreconditions getPreconditions() {
-    return preconditions;
-  }
-
-  public KeepConsequences getConsequences() {
-    return consequences;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodAccessPattern.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodAccessPattern.java
deleted file mode 100644
index 7f83937..0000000
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodAccessPattern.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
-
-// TODO: finish this.
-public class KeepMethodAccessPattern {
-
-  public static KeepMethodAccessPattern any() {
-    return new KeepMethodAccessPattern();
-  }
-
-  public boolean isAny() {
-    return true;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java b/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
deleted file mode 100644
index 9fb521c..0000000
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
-
-import com.android.tools.r8.utils.SetUtils;
-import com.google.common.collect.ImmutableSet;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-public final class KeepOptions {
-
-  public enum KeepOption {
-    SHRINKING,
-    OPTIMIZING,
-    OBFUSCATING,
-    ACCESS_MODIFING,
-  }
-
-  public static KeepOptions keepAll() {
-    if (ALLOW_NONE_INSTANCE == null) {
-      ALLOW_NONE_INSTANCE = new KeepOptions(true, Collections.emptySet());
-    }
-    return ALLOW_NONE_INSTANCE;
-  }
-
-  public static Builder allowBuilder() {
-    return new Builder(true);
-  }
-
-  public static Builder disallowBuilder() {
-    return new Builder(false);
-  }
-
-  public static KeepOptions allow(KeepOption... options) {
-    return allowBuilder().addAll(options).build();
-  }
-
-  public static KeepOptions disallow(KeepOption... options) {
-    return disallowBuilder().addAll(options).build();
-  }
-
-  public static class Builder {
-    public final boolean allowIfSet;
-    public Set<KeepOption> options = SetUtils.newIdentityHashSet();
-
-    private Builder(boolean allowIfSet) {
-      this.allowIfSet = allowIfSet;
-    }
-
-    public Builder add(KeepOption option) {
-      options.add(option);
-      return this;
-    }
-
-    public Builder addAll(KeepOption... options) {
-      return addAll(Arrays.asList(options));
-    }
-
-    public Builder addAll(Collection<KeepOption> options) {
-      this.options.addAll(options);
-      return this;
-    }
-
-    public KeepOptions build() {
-      if (options.isEmpty()) {
-        if (allowIfSet) {
-          return keepAll();
-        }
-        throw new KeepEdgeException("Invalid keep options that disallow nothing.");
-      }
-      if (options.size() == KeepOption.values().length) {
-        if (!allowIfSet) {
-          return keepAll();
-        }
-        throw new KeepEdgeException("Invalid keep options that allow everything.");
-      }
-      return new KeepOptions(allowIfSet, ImmutableSet.copyOf(options));
-    }
-  }
-
-  private static KeepOptions ALLOW_NONE_INSTANCE = null;
-
-  private final boolean allowIfSet;
-  private final Set<KeepOption> options;
-
-  private KeepOptions(boolean allowIfSet, Set<KeepOption> options) {
-    this.allowIfSet = allowIfSet;
-    this.options = options;
-  }
-
-  public boolean isAllowed(KeepOption option) {
-    return options.contains(option) == allowIfSet;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index 10620fc6..ee86680 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -109,25 +109,14 @@
   }
 
   public boolean isMoreVisibleThan(
-      AccessFlags other, String packageNameThis, String packageNameOther) {
+      AccessFlags<?> other, String packageNameThis, String packageNameOther) {
     int visibilityOrdinal = getVisibilityOrdinal();
     if (visibilityOrdinal > other.getVisibilityOrdinal()) {
       return true;
     }
-    if (visibilityOrdinal == other.getVisibilityOrdinal()
+    return visibilityOrdinal == other.getVisibilityOrdinal()
         && isVisibilityDependingOnPackage()
-        && !packageNameThis.equals(packageNameOther)) {
-      return true;
-    }
-    return false;
-  }
-
-  public boolean isAtLeastAsVisibleAs(AccessFlags other) {
-    return getVisibilityOrdinal() >= other.getVisibilityOrdinal();
-  }
-
-  public boolean isSameVisibility(AccessFlags other) {
-    return getVisibilityOrdinal() == other.getVisibilityOrdinal();
+        && !packageNameThis.equals(packageNameOther);
   }
 
   public int getVisibilityOrdinal() {
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index 42f5d96..369e60c 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -536,7 +536,7 @@
     assert potentialHolder.isInterface();
     for (DexEncodedMethod virtualMethod : potentialHolder.virtualMethods()) {
       if (virtualMethod.getReference().match(method.getReference())
-          && virtualMethod.accessFlags.isSameVisibility(method.accessFlags)) {
+          && virtualMethod.isSameVisibility(method)) {
         return true;
       }
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index d68d906..985e215 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -569,6 +569,40 @@
     return isStatic();
   }
 
+  public boolean isAtLeastAsVisibleAsOtherInSameHierarchy(
+      DexEncodedMethod other, AppView<? extends AppInfoWithClassHierarchy> appView) {
+    assert getReference().getProto() == other.getReference().getProto();
+    assert appView.isSubtype(getHolderType(), other.getHolderType()).isTrue()
+        || appView.isSubtype(other.getHolderType(), getHolderType()).isTrue();
+    AccessFlags<MethodAccessFlags> accessFlags = getAccessFlags();
+    AccessFlags<?> otherAccessFlags = other.getAccessFlags();
+    if (accessFlags.getVisibilityOrdinal() < otherAccessFlags.getVisibilityOrdinal()) {
+      return false;
+    } else if (accessFlags.isPrivate()) {
+      return getHolderType() == other.getHolderType();
+    } else if (accessFlags.isPublic() || accessFlags.isProtected()) {
+      return true;
+    } else {
+      assert accessFlags.isPackagePrivate();
+      return getHolderType().getPackageName().equals(other.getHolderType().getPackageName());
+    }
+  }
+
+  public boolean isSameVisibility(DexEncodedMethod other) {
+    AccessFlags<MethodAccessFlags> accessFlags = getAccessFlags();
+    if (accessFlags.getVisibilityOrdinal() != other.getAccessFlags().getVisibilityOrdinal()) {
+      return false;
+    }
+    if (accessFlags.isPublic()) {
+      return true;
+    }
+    if (accessFlags.isPrivate()) {
+      return getHolderType() == other.getHolderType();
+    }
+    assert accessFlags.isVisibilityDependingOnPackage();
+    return getHolderType().getPackageName().equals(other.getHolderType().getPackageName());
+  }
+
   /**
    * Returns true if this method is synthetic.
    */
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java
index 16c578b..8f7f2b6 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanDesugaredLibrarySpecificationParser.java
@@ -302,9 +302,26 @@
     if (jsonFlagSet.has(RETARGET_METHOD_KEY)) {
       for (Map.Entry<String, JsonElement> retarget :
           jsonFlagSet.get(RETARGET_METHOD_KEY).getAsJsonObject().entrySet()) {
-        builder.retargetMethod(
-            parseMethod(retarget.getKey()),
-            stringDescriptorToDexType(retarget.getValue().getAsString()));
+        String key = retarget.getKey();
+        String value = retarget.getValue().getAsString();
+        if (value.contains("#")) {
+          builder.retargetMethodToMethod(parseMethod(key), parseMethod(value));
+        } else {
+          builder.retargetMethodToType(parseMethod(key), stringDescriptorToDexType(value));
+        }
+      }
+    }
+    if (jsonFlagSet.has(RETARGET_METHOD_EMULATED_DISPATCH_KEY)) {
+      for (Map.Entry<String, JsonElement> retarget :
+          jsonFlagSet.get(RETARGET_METHOD_EMULATED_DISPATCH_KEY).getAsJsonObject().entrySet()) {
+        String key = retarget.getKey();
+        String value = retarget.getValue().getAsString();
+        if (value.contains("#")) {
+          builder.retargetMethodEmulatedDispatchToMethod(parseMethod(key), parseMethod(value));
+        } else {
+          builder.retargetMethodEmulatedDispatchToType(
+              parseMethod(key), stringDescriptorToDexType(value));
+        }
       }
     }
     if (jsonFlagSet.has(COVARIANT_RETARGET_METHOD_KEY)) {
@@ -315,14 +332,6 @@
             stringDescriptorToDexType(retarget.getValue().getAsString()));
       }
     }
-    if (jsonFlagSet.has(RETARGET_METHOD_EMULATED_DISPATCH_KEY)) {
-      for (Map.Entry<String, JsonElement> retarget :
-          jsonFlagSet.get(RETARGET_METHOD_EMULATED_DISPATCH_KEY).getAsJsonObject().entrySet()) {
-        builder.retargetMethodEmulatedDispatch(
-            parseMethod(retarget.getKey()),
-            stringDescriptorToDexType(retarget.getValue().getAsString()));
-      }
-    }
     if (jsonFlagSet.has(BACKPORT_KEY)) {
       for (Map.Entry<String, JsonElement> backport :
           jsonFlagSet.get(BACKPORT_KEY).getAsJsonObject().entrySet()) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java
index 8c4316f..314d657 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/HumanRewritingFlags.java
@@ -32,8 +32,10 @@
   private final Map<DexType, DexType> emulatedInterfaces;
   private final Map<DexField, DexField> retargetStaticField;
   private final Map<DexMethod, DexType> covariantRetarget;
-  private final Map<DexMethod, DexType> retargetMethod;
-  private final Map<DexMethod, DexType> retargetMethodEmulatedDispatch;
+  private final Map<DexMethod, DexType> retargetMethodToType;
+  private final Map<DexMethod, DexType> retargetMethodEmulatedDispatchToType;
+  private final Map<DexMethod, DexMethod> retargetMethodToMethod;
+  private final Map<DexMethod, DexMethod> retargetMethodEmulatedDispatchToMethod;
   private final Map<DexMethod, DexMethod[]> apiGenericTypesConversion;
   private final Map<DexType, DexType> legacyBackport;
   private final Map<DexType, DexType> customConversions;
@@ -52,8 +54,10 @@
       Map<DexType, DexType> emulateLibraryInterface,
       Map<DexField, DexField> retargetStaticField,
       Map<DexMethod, DexType> covariantRetarget,
-      Map<DexMethod, DexType> retargetMethod,
-      Map<DexMethod, DexType> retargetMethodEmulatedDispatch,
+      Map<DexMethod, DexType> retargetMethodToType,
+      Map<DexMethod, DexType> retargetMethodEmulatedDispatchToType,
+      Map<DexMethod, DexMethod> retargetMethodToMethod,
+      Map<DexMethod, DexMethod> retargetMethodEmulatedDispatchToMethod,
       Map<DexMethod, DexMethod[]> apiGenericTypesConversion,
       Map<DexType, DexType> legacyBackport,
       Map<DexType, DexType> customConversion,
@@ -70,8 +74,10 @@
     this.emulatedInterfaces = emulateLibraryInterface;
     this.retargetStaticField = retargetStaticField;
     this.covariantRetarget = covariantRetarget;
-    this.retargetMethod = retargetMethod;
-    this.retargetMethodEmulatedDispatch = retargetMethodEmulatedDispatch;
+    this.retargetMethodToType = retargetMethodToType;
+    this.retargetMethodEmulatedDispatchToType = retargetMethodEmulatedDispatchToType;
+    this.retargetMethodToMethod = retargetMethodToMethod;
+    this.retargetMethodEmulatedDispatchToMethod = retargetMethodEmulatedDispatchToMethod;
     this.apiGenericTypesConversion = apiGenericTypesConversion;
     this.legacyBackport = legacyBackport;
     this.customConversions = customConversion;
@@ -97,6 +103,8 @@
         ImmutableMap.of(),
         ImmutableMap.of(),
         ImmutableMap.of(),
+        ImmutableMap.of(),
+        ImmutableMap.of(),
         ImmutableSet.of(),
         ImmutableSet.of(),
         ImmutableMap.of(),
@@ -120,8 +128,10 @@
         emulatedInterfaces,
         retargetStaticField,
         covariantRetarget,
-        retargetMethod,
-        retargetMethodEmulatedDispatch,
+        retargetMethodToType,
+        retargetMethodEmulatedDispatchToType,
+        retargetMethodToMethod,
+        retargetMethodEmulatedDispatchToMethod,
         apiGenericTypesConversion,
         legacyBackport,
         customConversions,
@@ -161,12 +171,20 @@
     return covariantRetarget;
   }
 
-  public Map<DexMethod, DexType> getRetargetMethod() {
-    return retargetMethod;
+  public Map<DexMethod, DexType> getRetargetMethodToType() {
+    return retargetMethodToType;
   }
 
-  public Map<DexMethod, DexType> getRetargetMethodEmulatedDispatch() {
-    return retargetMethodEmulatedDispatch;
+  public Map<DexMethod, DexType> getRetargetMethodEmulatedDispatchToType() {
+    return retargetMethodEmulatedDispatchToType;
+  }
+
+  public Map<DexMethod, DexMethod> getRetargetMethodToMethod() {
+    return retargetMethodToMethod;
+  }
+
+  public Map<DexMethod, DexMethod> getRetargetMethodEmulatedDispatchToMethod() {
+    return retargetMethodEmulatedDispatchToMethod;
   }
 
   public Set<DexMethod> getNeverOutlineApi() {
@@ -211,8 +229,8 @@
         && maintainPrefix.isEmpty()
         && emulatedInterfaces.isEmpty()
         && covariantRetarget.isEmpty()
-        && retargetMethod.isEmpty()
-        && retargetMethodEmulatedDispatch.isEmpty()
+        && retargetMethodToType.isEmpty()
+        && retargetMethodEmulatedDispatchToType.isEmpty()
         && retargetStaticField.isEmpty();
   }
 
@@ -228,8 +246,10 @@
     private final Map<DexType, DexType> emulatedInterfaces;
     private final Map<DexField, DexField> retargetStaticField;
     private final Map<DexMethod, DexType> covariantRetarget;
-    private final Map<DexMethod, DexType> retargetMethod;
-    private final Map<DexMethod, DexType> retargetMethodEmulatedDispatch;
+    private final Map<DexMethod, DexType> retargetMethodToType;
+    private final Map<DexMethod, DexType> retargetMethodEmulatedDispatchToType;
+    private final Map<DexMethod, DexMethod> retargetMethodToMethod;
+    private final Map<DexMethod, DexMethod> retargetMethodEmulatedDispatchToMethod;
     private final Map<DexMethod, DexMethod[]> apiGenericTypesConversion;
     private final Map<DexType, DexType> legacyBackport;
     private final Map<DexType, DexType> customConversions;
@@ -256,6 +276,8 @@
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
           new IdentityHashMap<>(),
+          new IdentityHashMap<>(),
+          new IdentityHashMap<>(),
           Sets.newIdentityHashSet(),
           Sets.newIdentityHashSet(),
           new IdentityHashMap<>(),
@@ -274,8 +296,10 @@
         Map<DexType, DexType> emulateLibraryInterface,
         Map<DexField, DexField> retargetStaticField,
         Map<DexMethod, DexType> covariantRetarget,
-        Map<DexMethod, DexType> retargetMethod,
-        Map<DexMethod, DexType> retargetMethodEmulatedDispatch,
+        Map<DexMethod, DexType> retargetMethodToType,
+        Map<DexMethod, DexType> retargetMethodEmulatedDispatchToType,
+        Map<DexMethod, DexMethod> retargetMethodToMethod,
+        Map<DexMethod, DexMethod> retargetMethodEmulatedDispatchToMethod,
         Map<DexMethod, DexMethod[]> apiConversionCollection,
         Map<DexType, DexType> backportCoreLibraryMember,
         Map<DexType, DexType> customConversions,
@@ -294,8 +318,12 @@
       this.emulatedInterfaces = new IdentityHashMap<>(emulateLibraryInterface);
       this.retargetStaticField = new IdentityHashMap<>(retargetStaticField);
       this.covariantRetarget = new IdentityHashMap<>(covariantRetarget);
-      this.retargetMethod = new IdentityHashMap<>(retargetMethod);
-      this.retargetMethodEmulatedDispatch = new IdentityHashMap<>(retargetMethodEmulatedDispatch);
+      this.retargetMethodToType = new IdentityHashMap<>(retargetMethodToType);
+      this.retargetMethodEmulatedDispatchToType =
+          new IdentityHashMap<>(retargetMethodEmulatedDispatchToType);
+      this.retargetMethodToMethod = new IdentityHashMap<>(retargetMethodToMethod);
+      this.retargetMethodEmulatedDispatchToMethod =
+          new IdentityHashMap<>(retargetMethodEmulatedDispatchToMethod);
       this.apiGenericTypesConversion = new IdentityHashMap<>(apiConversionCollection);
       this.legacyBackport = new IdentityHashMap<>(backportCoreLibraryMember);
       this.customConversions = new IdentityHashMap<>(customConversions);
@@ -384,15 +412,42 @@
       return this;
     }
 
-    public Builder retargetMethod(DexMethod key, DexType rewrittenType) {
+    public Builder retargetMethodToType(DexMethod key, DexType rewrittenType) {
       put(
-          retargetMethod,
+          retargetMethodToType,
           key,
           rewrittenType,
           HumanDesugaredLibrarySpecificationParser.RETARGET_METHOD_KEY);
       return this;
     }
 
+    public Builder retargetMethodEmulatedDispatchToType(DexMethod key, DexType rewrittenType) {
+      put(
+          retargetMethodEmulatedDispatchToType,
+          key,
+          rewrittenType,
+          HumanDesugaredLibrarySpecificationParser.RETARGET_METHOD_EMULATED_DISPATCH_KEY);
+      return this;
+    }
+
+    public Builder retargetMethodToMethod(DexMethod key, DexMethod retarget) {
+      put(
+          retargetMethodToMethod,
+          key,
+          retarget,
+          HumanDesugaredLibrarySpecificationParser.RETARGET_METHOD_KEY);
+      return this;
+    }
+
+    public Builder retargetMethodEmulatedDispatchToMethod(DexMethod key, DexMethod retarget) {
+      put(
+          retargetMethodEmulatedDispatchToMethod,
+          key,
+          retarget,
+          HumanDesugaredLibrarySpecificationParser.RETARGET_METHOD_EMULATED_DISPATCH_KEY);
+      return this;
+    }
+
     public Builder covariantRetargetMethod(DexMethod key, DexType rewrittenType) {
       put(
           covariantRetarget,
@@ -411,15 +466,6 @@
       return this;
     }
 
-    public Builder retargetMethodEmulatedDispatch(DexMethod key, DexType rewrittenType) {
-      put(
-          retargetMethodEmulatedDispatch,
-          key,
-          rewrittenType,
-          HumanDesugaredLibrarySpecificationParser.RETARGET_METHOD_EMULATED_DISPATCH_KEY);
-      return this;
-    }
-
     public void addApiGenericTypesConversion(DexMethod method, int index, DexMethod conversion) {
       DexMethod[] types =
           apiGenericTypesConversion.computeIfAbsent(
@@ -473,8 +519,10 @@
           ImmutableMap.copyOf(emulatedInterfaces),
           ImmutableMap.copyOf(retargetStaticField),
           ImmutableMap.copyOf(covariantRetarget),
-          ImmutableMap.copyOf(retargetMethod),
-          ImmutableMap.copyOf(retargetMethodEmulatedDispatch),
+          ImmutableMap.copyOf(retargetMethodToType),
+          ImmutableMap.copyOf(retargetMethodEmulatedDispatchToType),
+          ImmutableMap.copyOf(retargetMethodToMethod),
+          ImmutableMap.copyOf(retargetMethodEmulatedDispatchToMethod),
           ImmutableMap.copyOf(apiGenericTypesConversion),
           ImmutableMap.copyOf(legacyBackport),
           ImmutableMap.copyOf(customConversions),
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java
index 6283419..ad9735c 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationFlagDeduplicator.java
@@ -93,15 +93,15 @@
         commonBuilder::putEmulatedInterface,
         builder::putEmulatedInterface);
     deduplicateFlags(
-        flags.getRetargetMethod(),
-        otherFlags.getRetargetMethod(),
-        commonBuilder::retargetMethod,
-        builder::retargetMethod);
+        flags.getRetargetMethodToType(),
+        otherFlags.getRetargetMethodToType(),
+        commonBuilder::retargetMethodToType,
+        builder::retargetMethodToType);
     deduplicateFlags(
-        flags.getRetargetMethodEmulatedDispatch(),
-        otherFlags.getRetargetMethodEmulatedDispatch(),
-        commonBuilder::retargetMethodEmulatedDispatch,
-        builder::retargetMethodEmulatedDispatch);
+        flags.getRetargetMethodEmulatedDispatchToType(),
+        otherFlags.getRetargetMethodEmulatedDispatchToType(),
+        commonBuilder::retargetMethodEmulatedDispatchToType,
+        builder::retargetMethodEmulatedDispatchToType);
     deduplicateFlags(
         flags.getLegacyBackport(),
         otherFlags.getLegacyBackport(),
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java
index 736632a..2096b33 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/humanspecification/MultiAPILevelHumanDesugaredLibrarySpecificationJsonExporter.java
@@ -124,13 +124,22 @@
       if (!flags.getRetargetStaticField().isEmpty()) {
         toJson.put(RETARGET_STATIC_FIELD_KEY, mapToString(flags.getRetargetStaticField()));
       }
-      if (!flags.getRetargetMethod().isEmpty()) {
-        toJson.put(RETARGET_METHOD_KEY, mapToString(flags.getRetargetMethod()));
+      if (!flags.getRetargetMethodToType().isEmpty()) {
+        toJson.put(RETARGET_METHOD_KEY, mapToString(flags.getRetargetMethodToType()));
       }
-      if (!flags.getRetargetMethodEmulatedDispatch().isEmpty()) {
+      if (!flags.getRetargetMethodEmulatedDispatchToMethod().isEmpty()) {
+        toJson.put(
+            RETARGET_METHOD_KEY, mapToString(flags.getRetargetMethodEmulatedDispatchToMethod()));
+      }
+      if (!flags.getRetargetMethodEmulatedDispatchToType().isEmpty()) {
         toJson.put(
             RETARGET_METHOD_EMULATED_DISPATCH_KEY,
-            mapToString(flags.getRetargetMethodEmulatedDispatch()));
+            mapToString(flags.getRetargetMethodEmulatedDispatchToType()));
+      }
+      if (!flags.getRetargetMethodEmulatedDispatchToMethod().isEmpty()) {
+        toJson.put(
+            RETARGET_METHOD_EMULATED_DISPATCH_KEY,
+            mapToString(flags.getRetargetMethodEmulatedDispatchToMethod()));
       }
       if (!flags.getDontRetarget().isEmpty()) {
         toJson.put(DONT_RETARGET_KEY, setToString(flags.getDontRetarget()));
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineEmulatedInterfaceConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineEmulatedInterfaceConverter.java
index 5f385c5..00c7da1 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineEmulatedInterfaceConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineEmulatedInterfaceConverter.java
@@ -95,7 +95,7 @@
     LinkedHashMap<DexType, DerivedMethod> extraDispatchCases = new LinkedHashMap<>();
     // Retarget core lib emulated dispatch handled as part of emulated interface dispatch.
     Map<DexMethod, DexType> retargetCoreLibMember =
-        rewritingFlags.getRetargetMethodEmulatedDispatch();
+        rewritingFlags.getRetargetMethodEmulatedDispatchToType();
     for (DexMethod retarget : retargetCoreLibMember.keySet()) {
       if (retarget.match(method)) {
         DexClass inClass = appInfo.definitionFor(retarget.getHolderType());
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachinePrefixConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachinePrefixConverter.java
index a1a8d29..7a669c2 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachinePrefixConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachinePrefixConverter.java
@@ -50,11 +50,14 @@
   public void convertPrefixFlags(
       HumanRewritingFlags rewritingFlags, BiConsumer<String, Set<DexString>> warnConsumer) {
     rewriteClasses();
-    rewriteValues(rewritingFlags.getRetargetMethod());
-    rewriteValues(rewritingFlags.getRetargetMethodEmulatedDispatch());
+    rewriteValues(rewritingFlags.getRetargetMethodToType());
+    rewriteValues(rewritingFlags.getRetargetMethodEmulatedDispatchToType());
     rewriteValues(rewritingFlags.getCustomConversions());
+    rewriteMethodValues(rewritingFlags.getRetargetMethodToMethod());
+    rewriteMethodValues(rewritingFlags.getRetargetMethodEmulatedDispatchToMethod());
     rewriteEmulatedInterface(rewritingFlags.getEmulatedInterfaces());
-    rewriteRetargetKeys(rewritingFlags.getRetargetMethodEmulatedDispatch());
+    rewriteRetargetKeys(rewritingFlags.getRetargetMethodEmulatedDispatchToType());
+    rewriteRetargetKeys(rewritingFlags.getRetargetMethodEmulatedDispatchToMethod());
     rewriteApiConversions(rewritingFlags.getApiGenericConversion());
     warnIfUnusedPrefix(warnConsumer);
   }
@@ -79,7 +82,7 @@
                 convertedPrefix + interfaceType.substring(firstPackage + 1)));
   }
 
-  private void rewriteRetargetKeys(Map<DexMethod, DexType> retarget) {
+  private void rewriteRetargetKeys(Map<DexMethod, ?> retarget) {
     for (DexMethod dexMethod : retarget.keySet()) {
       DexType type = convertJavaNameToDesugaredLibrary(dexMethod.holder);
       builder.rewriteDerivedTypeOnly(dexMethod.holder, type);
@@ -108,6 +111,12 @@
     }
   }
 
+  private void rewriteMethodValues(Map<?, DexMethod> flags) {
+    for (DexMethod method : flags.values()) {
+      registerType(method.getHolderType());
+    }
+  }
+
   private void rewriteClasses() {
     appInfo.app().forEachLibraryType(this::registerClassType);
     if (libraryCompilation) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineRetargetConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineRetargetConverter.java
index 6078e9d..004c35f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineRetargetConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/HumanToMachineRetargetConverter.java
@@ -24,6 +24,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 public class HumanToMachineRetargetConverter {
 
@@ -45,13 +46,22 @@
         .getCovariantRetarget()
         .forEach((method, type) -> convertCovariantRetarget(builder, method, type));
     rewritingFlags
-        .getRetargetMethod()
-        .forEach((method, type) -> convertRetargetMethod(builder, method, type));
+        .getRetargetMethodToType()
+        .forEach((method, type) -> convertRetargetMethodToType(builder, method, type));
     rewritingFlags
-        .getRetargetMethodEmulatedDispatch()
+        .getRetargetMethodEmulatedDispatchToType()
         .forEach(
             (method, type) ->
-                convertRetargetMethodEmulatedDispatch(builder, rewritingFlags, method, type));
+                convertRetargetMethodEmulatedDispatchToType(builder, rewritingFlags, method, type));
+    rewritingFlags
+        .getRetargetMethodToMethod()
+        .forEach((method, retarget) -> convertRetargetMethodToMethod(builder, method, retarget));
+    rewritingFlags
+        .getRetargetMethodEmulatedDispatchToMethod()
+        .forEach(
+            (method, retarget) ->
+                convertRetargetMethodEmulatedDispatchToMethod(
+                    builder, rewritingFlags, method, retarget));
     warnConsumer.accept("Cannot retarget missing references: ", missingReferences);
   }
 
@@ -66,37 +76,6 @@
     builder.putStaticFieldRetarget(field, rewrittenField);
   }
 
-  private void convertRetargetMethodEmulatedDispatch(
-      MachineRewritingFlags.Builder builder,
-      HumanRewritingFlags rewritingFlags,
-      DexMethod method,
-      DexType type) {
-    DexClass holder = appInfo.definitionFor(method.holder);
-    DexEncodedMethod foundMethod = holder.lookupMethod(method);
-    if (foundMethod == null) {
-      missingReferences.add(method);
-      return;
-    }
-    if (foundMethod.isStatic()) {
-      appInfo
-          .app()
-          .options
-          .reporter
-          .error("Cannot generate emulated dispatch for static method " + foundMethod);
-      return;
-    }
-    if (!seemsToNeedEmulatedDispatch(holder, foundMethod)) {
-      appInfo
-          .app()
-          .options
-          .reporter
-          .warning(
-              "Generating (seemingly unnecessary) emulated dispatch for final method "
-                  + foundMethod);
-    }
-    convertEmulatedVirtualRetarget(builder, rewritingFlags, foundMethod, type);
-  }
-
   private void convertCovariantRetarget(
       MachineRewritingFlags.Builder builder, DexMethod method, DexType type) {
     DexClass holder = appInfo.definitionFor(method.holder);
@@ -133,7 +112,9 @@
   }
 
   private void convertRetargetMethod(
-      MachineRewritingFlags.Builder builder, DexMethod method, DexType type) {
+      DexMethod method,
+      Consumer<DexEncodedMethod> staticRetarget,
+      Consumer<DexEncodedMethod> nonEmulatedVirtualRetarget) {
     DexClass holder = appInfo.definitionFor(method.holder);
     DexEncodedMethod foundMethod = holder.lookupMethod(method);
     if (foundMethod == null) {
@@ -141,7 +122,7 @@
       return;
     }
     if (foundMethod.isStatic()) {
-      convertStaticRetarget(builder, foundMethod, type);
+      staticRetarget.accept(foundMethod);
       return;
     }
     if (seemsToNeedEmulatedDispatch(holder, foundMethod)) {
@@ -154,7 +135,72 @@
                   + foundMethod
                   + " which could lead to invalid runtime execution in overrides.");
     }
-    convertNonEmulatedVirtualRetarget(builder, foundMethod, type);
+    nonEmulatedVirtualRetarget.accept(foundMethod);
+  }
+
+  private void convertRetargetMethodToType(
+      MachineRewritingFlags.Builder builder, DexMethod method, DexType type) {
+    convertRetargetMethod(
+        method,
+        foundMethod -> convertStaticRetarget(builder, foundMethod, type),
+        foundMethod -> convertNonEmulatedVirtualRetarget(builder, foundMethod, type));
+  }
+
+  private void convertRetargetMethodToMethod(
+      MachineRewritingFlags.Builder builder, DexMethod method, DexMethod retarget) {
+    convertRetargetMethod(
+        method,
+        foundMethod -> builder.putStaticRetarget(method, retarget),
+        foundMethod -> builder.putNonEmulatedVirtualRetarget(method, retarget));
+  }
+
+  private void convertRetargetMethodEmulatedDispatch(
+      DexMethod method, Consumer<DexEncodedMethod> emulatedRetarget) {
+    DexClass holder = appInfo.definitionFor(method.holder);
+    DexEncodedMethod foundMethod = holder.lookupMethod(method);
+    if (foundMethod == null) {
+      missingReferences.add(method);
+      return;
+    }
+    if (foundMethod.isStatic()) {
+      appInfo
+          .app()
+          .options
+          .reporter
+          .error("Cannot generate emulated dispatch for static method " + foundMethod);
+      return;
+    }
+    if (!seemsToNeedEmulatedDispatch(holder, foundMethod)) {
+      appInfo
+          .app()
+          .options
+          .reporter
+          .warning(
+              "Generating (seemingly unnecessary) emulated dispatch for final method "
+                  + foundMethod);
+    }
+    emulatedRetarget.accept(foundMethod);
+  }
+
+  private void convertRetargetMethodEmulatedDispatchToType(
+      MachineRewritingFlags.Builder builder,
+      HumanRewritingFlags rewritingFlags,
+      DexMethod method,
+      DexType type) {
+    convertRetargetMethodEmulatedDispatch(
+        method,
+        foundMethod -> convertEmulatedVirtualRetarget(builder, rewritingFlags, foundMethod, type));
+  }
+
+  private void convertRetargetMethodEmulatedDispatchToMethod(
+      MachineRewritingFlags.Builder builder,
+      HumanRewritingFlags rewritingFlags,
+      DexMethod method,
+      DexMethod retarget) {
+    convertRetargetMethodEmulatedDispatch(
+        method,
+        foundMethod ->
+            convertEmulatedVirtualRetarget(builder, rewritingFlags, foundMethod, retarget));
   }
 
   private boolean seemsToNeedEmulatedDispatch(DexClass holder, DexEncodedMethod method) {
@@ -166,10 +212,7 @@
       MachineRewritingFlags.Builder builder,
       HumanRewritingFlags rewritingFlags,
       DexEncodedMethod src,
-      DexType type) {
-    DexProto newProto = appInfo.dexItemFactory().prependHolderToProto(src.getReference());
-    DexMethod forwardingDexMethod =
-        appInfo.dexItemFactory().createMethod(type, newProto, src.getName());
+      DexMethod forwardingDexMethod) {
     if (isEmulatedInterfaceDispatch(src, appInfo, rewritingFlags)) {
       // Handled by emulated interface dispatch.
       builder.putEmulatedVirtualRetargetThroughEmulatedInterface(
@@ -190,6 +233,17 @@
             interfaceMethod, dispatchMethod, forwardingMethod, dispatchCases));
   }
 
+  private void convertEmulatedVirtualRetarget(
+      MachineRewritingFlags.Builder builder,
+      HumanRewritingFlags rewritingFlags,
+      DexEncodedMethod src,
+      DexType type) {
+    DexProto newProto = appInfo.dexItemFactory().prependHolderToProto(src.getReference());
+    DexMethod forwardingDexMethod =
+        appInfo.dexItemFactory().createMethod(type, newProto, src.getName());
+    convertEmulatedVirtualRetarget(builder, rewritingFlags, src, forwardingDexMethod);
+  }
+
   private boolean isEmulatedInterfaceDispatch(
       DexEncodedMethod method,
       AppInfoWithClassHierarchy appInfo,
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LegacyToHumanSpecificationConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LegacyToHumanSpecificationConverter.java
index b251add..1f7fe89 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LegacyToHumanSpecificationConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/specificationconversion/LegacyToHumanSpecificationConverter.java
@@ -161,7 +161,7 @@
           itemFactory.createMethod(
               itemFactory.createType(itemFactory.arraysDescriptor), proto, name);
       DexType target = itemFactory.createType("Ljava/util/DesugarArrays;");
-      builder.retargetMethod(source, target);
+      builder.retargetMethodToType(source, target);
 
       builder.amendLibraryMethod(
           source,
@@ -178,7 +178,7 @@
       source =
           itemFactory.createMethod(itemFactory.createType("Ljava/util/TimeZone;"), proto, name);
       target = itemFactory.createType("Ljava/util/DesugarTimeZone;");
-      builder.retargetMethod(source, target);
+      builder.retargetMethodToType(source, target);
     }
     // Required by
     // https://github.com/google/desugar_jdk_libs/commit/485071cd09a3691549d065ba9e323d07edccf085.
@@ -272,9 +272,9 @@
             if (definition.isStatic()
                 || definition.isFinal()
                 || dexClassAndMethod.getHolder().isFinal()) {
-              builder.retargetMethod(dexClassAndMethod.getReference(), rewrittenType);
+              builder.retargetMethodToType(dexClassAndMethod.getReference(), rewrittenType);
             } else {
-              builder.retargetMethodEmulatedDispatch(
+              builder.retargetMethodEmulatedDispatchToType(
                   dexClassAndMethod.getReference(), rewrittenType);
             }
           }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 6099074..3d9b8d2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -2220,8 +2220,7 @@
       InstructionListIterator it = block.listIterator(code);
       while (it.hasNext()) {
         Instruction instruction = it.next();
-        if (instruction.getLocalInfo() != null
-            || !allowNewFilledArrayConstruction(instruction)) {
+        if (instruction.getLocalInfo() != null || !allowNewFilledArrayConstruction(instruction)) {
           continue;
         }
         NewArrayEmpty newArray = instruction.asNewArrayEmpty();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index 8a023db..08f3912 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -395,8 +395,7 @@
             .isPossiblyFalse()
         || !newResolutionResult
             .getResolvedMethod()
-            .getAccessFlags()
-            .isAtLeastAsVisibleAs(resolutionResult.getResolvedMethod().getAccessFlags())
+            .isAtLeastAsVisibleAsOtherInSameHierarchy(resolutionResult.getResolvedMethod(), appView)
         // isOverriding expects both arguments to be not private.
         || (!newResolutionResult.getResolvedMethod().isPrivateMethod()
             && !isOverriding(
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
index 0cc861f..0dbea46 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
@@ -86,7 +86,13 @@
   }
 
   private DexMethod getReboundMethodReference(DexMethod method) {
-    return nonReboundMethodReferenceToDefinitionMap.getOrDefault(method, method);
+    DexMethod rebound = nonReboundMethodReferenceToDefinitionMap.get(method);
+    while (rebound != null) {
+      method = rebound;
+      rebound = nonReboundMethodReferenceToDefinitionMap.get(method);
+    }
+    assert method != rebound;
+    return method;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/RedundantBridgeRemover.java b/src/main/java/com/android/tools/r8/optimize/RedundantBridgeRemover.java
index d0066e0..5cbb1b6 100644
--- a/src/main/java/com/android/tools/r8/optimize/RedundantBridgeRemover.java
+++ b/src/main/java/com/android/tools/r8/optimize/RedundantBridgeRemover.java
@@ -10,12 +10,17 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.FailedResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
 import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
 import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemovalLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.KeepMethodInfo;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -31,12 +36,7 @@
   }
 
   private DexClassAndMethod getTargetForRedundantBridge(ProgramMethod method) {
-    // Clean-up the predicate check.
-    if (appView.appInfo().isPinned(method.getReference())) {
-      return null;
-    }
     DexEncodedMethod definition = method.getDefinition();
-    // TODO(b/197490164): Remove if method is abstract.
     BridgeInfo bridgeInfo = definition.getOptimizationInfo().getBridgeInfo();
     boolean isBridge = definition.isBridge() || bridgeInfo != null;
     if (!isBridge || definition.isAbstract()) {
@@ -59,18 +59,10 @@
     if (targetMethod == null) {
       return null;
     }
-    if (method.getAccessFlags().isPublic()) {
-      if (!targetMethod.getAccessFlags().isPublic()) {
-        return null;
-      }
-    } else {
-      if (targetMethod.getAccessFlags().isProtected()
-          && !targetMethod.getHolderType().isSamePackage(method.getHolderType())) {
-        return null;
-      }
-      if (targetMethod.getAccessFlags().isPrivate()) {
-        return null;
-      }
+    if (!targetMethod
+        .getDefinition()
+        .isAtLeastAsVisibleAsOtherInSameHierarchy(method.getDefinition(), appView)) {
+      return null;
     }
     if (definition.isStatic()
         && method.getHolder().hasClassInitializer()
@@ -148,6 +140,16 @@
           ProgramMethodSet bridgesToRemoveForClass = ProgramMethodSet.create();
           clazz.forEachProgramMethod(
               method -> {
+                KeepMethodInfo keepInfo = appView.getKeepInfo(method);
+                if (!keepInfo.isShrinkingAllowed(appView.options())
+                    || !keepInfo.isOptimizationAllowed(appView.options())) {
+                  return;
+                }
+                if (isRedundantAbstractBridge(method)) {
+                  // Record that the redundant bridge should be removed.
+                  bridgesToRemoveForClass.add(method);
+                  return;
+                }
                 DexClassAndMethod target = getTargetForRedundantBridge(method);
                 if (target != null) {
                   // Record that the redundant bridge should be removed.
@@ -175,6 +177,55 @@
     return bridgesToRemove;
   }
 
+  private boolean isRedundantAbstractBridge(ProgramMethod method) {
+    if (!method.getAccessFlags().isAbstract() || method.getDefinition().getCode() != null) {
+      return false;
+    }
+    DexProgramClass holder = method.getHolder();
+    if (holder.getSuperType() == null) {
+      assert holder.getType() == appView.dexItemFactory().objectType;
+      return false;
+    }
+    MethodResolutionResult superTypeResolution =
+        appView.appInfo().resolveMethodOn(holder.getSuperType(), method.getReference(), false);
+    if (superTypeResolution.isMultiMethodResolutionResult()) {
+      return false;
+    }
+    // Check if there is a definition in the super type hieararchy that is also abstract and has the
+    // same visibility.
+    if (superTypeResolution.isSingleResolution()) {
+      DexClassAndMethod resolutionPair =
+          superTypeResolution.asSingleResolution().getResolutionPair();
+      return resolutionPair.getDefinition().isAbstract()
+          && resolutionPair
+              .getDefinition()
+              .isAtLeastAsVisibleAsOtherInSameHierarchy(method.getDefinition(), appView)
+          && (!resolutionPair.getHolder().isInterface() || holder.getInterfaces().isEmpty());
+    }
+    // Only check for interfaces if resolving the method on super type causes NoSuchMethodError.
+    FailedResolutionResult failedResolutionResult = superTypeResolution.asFailedResolution();
+    if (failedResolutionResult == null
+        || !failedResolutionResult.isNoSuchMethodErrorResult(holder, appView.appInfo())
+        || holder.getInterfaces().isEmpty()) {
+      return false;
+    }
+    for (DexType iface : holder.getInterfaces()) {
+      SingleResolutionResult<?> singleIfaceResult =
+          appView
+              .appInfo()
+              .resolveMethodOn(iface, method.getReference(), true)
+              .asSingleResolution();
+      if (singleIfaceResult == null
+          || !singleIfaceResult.getResolvedMethod().isAbstract()
+          || !singleIfaceResult
+              .getResolvedMethod()
+              .isAtLeastAsVisibleAsOtherInSameHierarchy(method.getDefinition(), appView)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   private void pruneApp(
       Map<DexProgramClass, ProgramMethodSet> bridgesToRemove, ExecutorService executorService)
       throws ExecutionException {
diff --git a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
index a680b36..042cc6a 100644
--- a/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/redundantbridgeremoval/RedundantBridgeRemovalLens.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
+import com.android.tools.r8.ir.code.Invoke;
 import com.google.common.collect.Sets;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -80,14 +81,18 @@
       do {
         newReference = methodMap.get(newReference);
       } while (methodMap.containsKey(newReference));
-      if (previous.getType().isSuper() && interfaces.contains(newReference.getHolderType())) {
+      boolean holderTypeIsInterface = interfaces.contains(newReference.getHolderType());
+      if (previous.getType().isSuper() && holderTypeIsInterface) {
         return previous;
       }
       return MethodLookupResult.builder(this)
           .setReference(newReference)
           .setReboundReference(newReference)
           .setPrototypeChanges(previous.getPrototypeChanges())
-          .setType(previous.getType())
+          .setType(
+              holderTypeIsInterface && previous.getType().isVirtual()
+                  ? Invoke.Type.INTERFACE
+                  : previous.getType())
           .build();
     }
     return previous;
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 9af4f06..e653a7a 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1548,7 +1548,8 @@
     private boolean enableInterfaceMerging =
         System.getProperty("com.android.tools.r8.enableHorizontalInterfaceMerging") != null;
     private boolean enableInterfaceMergingInInitial = false;
-    private boolean enableSameFilePolicy = false;
+    private boolean enableSameFilePolicy =
+        System.getProperty("com.android.tools.r8.enableSameFilePolicy") != null;
     private boolean enableSyntheticMerging = true;
     private boolean ignoreRuntimeTypeChecksForTesting = false;
     private boolean restrictToSynthetics = false;
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index fc9157d..43b1b02 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.androidapi.AndroidApiDataAccess;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.references.ClassReference;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
 import java.io.BufferedOutputStream;
@@ -304,6 +305,10 @@
     return DescriptorUtils.getClassBinaryName(clazz) + CLASS_EXTENSION;
   }
 
+  public static String zipEntryNameForClass(ClassReference clazz) {
+    return clazz.getBinaryName() + CLASS_EXTENSION;
+  }
+
   public static long getOffsetOfResourceInZip(File file, String entry) throws IOException {
     // Look into the jar file to see find the offset.
     ZipFile zipFile = new ZipFile(file);
diff --git a/src/test/examplesJava11/path/PathExample.java b/src/test/examplesJava11/path/PathExample.java
new file mode 100644
index 0000000..8b17da2
--- /dev/null
+++ b/src/test/examplesJava11/path/PathExample.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package path;
+
+import java.nio.file.Path;
+
+public class PathExample {
+
+  public static void main(String[] args) {
+    Path thePath = Path.of("foo", "bar");
+    System.out.println(thePath);
+    System.out.println(Path.of(thePath.toUri()).getFileName());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/JavaCompilerTool.java b/src/test/java/com/android/tools/r8/JavaCompilerTool.java
index b961367..ba23dd5 100644
--- a/src/test/java/com/android/tools/r8/JavaCompilerTool.java
+++ b/src/test/java/com/android/tools/r8/JavaCompilerTool.java
@@ -102,10 +102,10 @@
   }
 
   public JavaCompilerTool addAnnotationProcessors(String... processors) {
-    return addAnnotationProcessor(Arrays.asList(processors));
+    return addAnnotationProcessors(Arrays.asList(processors));
   }
 
-  public JavaCompilerTool addAnnotationProcessor(Collection<String> processors) {
+  public JavaCompilerTool addAnnotationProcessors(Collection<String> processors) {
     annotationProcessors.addAll(processors);
     return this;
   }
@@ -118,6 +118,11 @@
     return this;
   }
 
+  public JavaCompilerTool addDebugAgent() {
+    return addOptions(
+        "-J-debug", "-J-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005");
+  }
+
   private Path getOrCreateOutputPath() throws IOException {
     return output != null ? output : state.getNewTempFolder().resolve("out.jar");
   }
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/BridgeWithInvokeSuperOnInterfaceTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/BridgeWithInvokeSuperOnInterfaceTest.java
new file mode 100644
index 0000000..cc24115
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/BridgeWithInvokeSuperOnInterfaceTest.java
@@ -0,0 +1,107 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.bridgeremoval;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class BridgeWithInvokeSuperOnInterfaceTest extends TestBase {
+
+  private static final String[] EXPECTED = new String[] {"I::foo"};
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, I.class)
+        .addProgramClassFileData(getJWithBridgeAccessFlag())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, I.class)
+        .addProgramClassFileData(getJWithBridgeAccessFlag())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableNoVerticalClassMergingAnnotations()
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .compile()
+        .inspect(
+            inspector -> {
+              // Check that we are removing the bridge if we support default methods.
+              if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                ClassSubject J = inspector.clazz(J.class);
+                assertThat(J, isPresent());
+                MethodSubject fooMethod = J.uniqueMethodWithOriginalName("foo");
+                assertThat(fooMethod, not(isPresent()));
+              }
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  private byte[] getJWithBridgeAccessFlag() throws Exception {
+    return transformer(J.class)
+        .setAccessFlags(MethodPredicate.onName("foo"), MethodAccessFlags::setBridge)
+        .transform();
+  }
+
+  @NoVerticalClassMerging
+  public interface I {
+
+    @NeverInline
+    default void foo() {
+      System.out.println("I::foo");
+    }
+  }
+
+  @NoVerticalClassMerging
+  public interface J extends I {
+
+    @Override
+    default void foo() {
+      I.super.foo();
+    }
+  }
+
+  public static class Main implements J {
+
+    public static void main(String[] args) {
+      new Main().callSuper();
+    }
+
+    @NeverInline
+    public void callSuper() {
+      J.super.foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/BridgeWithInvokeSuperTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/BridgeWithInvokeSuperTest.java
new file mode 100644
index 0000000..b965fea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/BridgeWithInvokeSuperTest.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.bridgeremoval;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class BridgeWithInvokeSuperTest extends TestBase {
+
+  private static final String[] EXPECTED = new String[] {"I::foo"};
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, I.class)
+        .addProgramClassFileData(getAWithBridgeAccessFlag())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, I.class)
+        .addProgramClassFileData(getAWithBridgeAccessFlag())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableNoVerticalClassMergingAnnotations()
+        .enableInliningAnnotations()
+        .compile()
+        .inspect(
+            inspector -> {
+              // Check that we are removing the bridge if we support default methods.
+              if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                ClassSubject A = inspector.clazz(A.class);
+                assertThat(A, isPresent());
+                MethodSubject fooMethod = A.uniqueMethodWithOriginalName("foo");
+                assertThat(fooMethod, not(isPresent()));
+              }
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  private byte[] getAWithBridgeAccessFlag() throws Exception {
+    return transformer(A.class)
+        .setAccessFlags(MethodPredicate.onName("foo"), MethodAccessFlags::setBridge)
+        .transform();
+  }
+
+  @NoVerticalClassMerging
+  public interface I {
+
+    @NeverInline
+    default void foo() {
+      System.out.println("I::foo");
+    }
+  }
+
+  public static class A implements I {
+
+    @Override
+    @NeverInline
+    public void foo() {
+      I.super.foo();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesTest.java
index cef3785..e6015ad 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesTest.java
@@ -25,9 +25,11 @@
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
 import java.nio.file.attribute.PosixFileAttributeView;
 import java.nio.file.attribute.PosixFileAttributes;
 import java.nio.file.attribute.PosixFilePermission;
+import java.time.Instant;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -157,6 +159,7 @@
       readWriteThroughFilesAPI(path);
       readThroughFileChannelAPI(path);
       attributeAccess(path);
+      Files.setAttribute(path, "basic:lastModifiedTime", FileTime.from(Instant.EPOCH));
       fspMethodsWithGeneric(path);
       pathGeneric();
     }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/InputStreamTransferToTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/InputStreamTransferToTest.java
index 70d445d..130cce2 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/InputStreamTransferToTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/InputStreamTransferToTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
 import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
@@ -67,4 +68,23 @@
         .run(parameters.getRuntime(), MAIN_CLASS)
         .assertSuccessWithOutput(EXPECTED_OUTPUT);
   }
+
+  /**
+   * See b/248200357, in T an override or transferTo was introduced in android.jar changing
+   * resolution.
+   */
+  @Test
+  public void testWithAndroidJarFromT() throws Exception {
+    // The method is not present on JDK8 so if we don't desugar that won't work.
+    Assume.assumeFalse(
+        parameters.isCfRuntime(CfVm.JDK8)
+            && !libraryDesugaringSpecification.hasNioFileDesugaring(parameters)
+            && compilationSpecification.isCfToCf());
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addProgramFiles(INPUT_JAR)
+        .addKeepMainRule(MAIN_CLASS)
+        .overrideLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.T))
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/PathOfTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/PathOfTest.java
new file mode 100644
index 0000000..33fa6ab
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/PathOfTest.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.jdk11;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PathOfTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+  private final CompilationSpecification compilationSpecification;
+
+  private static final Path INPUT_JAR = Paths.get(ToolHelper.EXAMPLES_JAVA11_JAR_DIR + "path.jar");
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("foo/bar", "bar");
+  private static final String MAIN_CLASS = "path.PathExample";
+
+  @Parameters(name = "{0}, spec: {1}, {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+        ImmutableList.of(JDK11_PATH),
+        DEFAULT_SPECIFICATIONS);
+  }
+
+  public PathOfTest(
+      TestParameters parameters,
+      LibraryDesugaringSpecification libraryDesugaringSpecification,
+      CompilationSpecification compilationSpecification) {
+    this.parameters = parameters;
+    this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+    this.compilationSpecification = compilationSpecification;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+        .addProgramFiles(INPUT_JAR)
+        .addKeepMainRule(MAIN_CLASS)
+        .run(parameters.getRuntime(), MAIN_CLASS)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java
index 2a07cfb..a66e6b4 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/specification/ConvertExportReadTest.java
@@ -293,7 +293,8 @@
     assertEquals(
         humanRewritingFlags1.getEmulatedInterfaces(), humanRewritingFlags2.getEmulatedInterfaces());
     assertEquals(
-        humanRewritingFlags1.getRetargetMethod(), humanRewritingFlags2.getRetargetMethod());
+        humanRewritingFlags1.getRetargetMethodToType(),
+        humanRewritingFlags2.getRetargetMethodToType());
 
     assertEquals(humanRewritingFlags1.getDontRetarget(), humanRewritingFlags2.getDontRetarget());
     assertEquals(
diff --git a/src/test/java/com/android/tools/r8/keepanno/asm/KeepEdgeAsmTest.java b/src/test/java/com/android/tools/r8/keepanno/asm/KeepEdgeAsmTest.java
new file mode 100644
index 0000000..b9ebfa2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/asm/KeepEdgeAsmTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.asm;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.testsource.KeepClassAndDefaultConstructorSource;
+import com.android.tools.r8.keepanno.testsource.KeepSourceEdges;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.transformers.ClassTransformer;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.AnnotationVisitor;
+
+@RunWith(Parameterized.class)
+public class KeepEdgeAsmTest extends TestBase {
+
+  private static final Class<?> SOURCE = KeepClassAndDefaultConstructorSource.class;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public KeepEdgeAsmTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  @Test
+  public void testAsmReader() throws Exception {
+    Set<KeepEdge> expectedEdges = KeepSourceEdges.getExpectedEdges(SOURCE);
+    ClassReference clazz = Reference.classFromClass(SOURCE);
+    // Original bytes of the test class.
+    byte[] original = ToolHelper.getClassAsBytes(SOURCE);
+    // Strip out all the annotations to ensure they are actually added again.
+    byte[] stripped =
+        transformer(SOURCE)
+            .addClassTransformer(
+                new ClassTransformer() {
+                  @Override
+                  public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
+                    // Ignore all input annotations.
+                    return null;
+                  }
+                })
+            .transform();
+    // Manually add in the expected edges again.
+    byte[] readded =
+        transformer(stripped, clazz)
+            .addClassTransformer(
+                new ClassTransformer() {
+
+                  @Override
+                  public void visitEnd() {
+                    for (KeepEdge edge : expectedEdges) {
+                      KeepEdgeWriter.writeEdge(edge, super::visitAnnotation);
+                    }
+                    super.visitEnd();
+                  }
+                })
+            .transform();
+
+    // Read the edges from each version.
+    Set<KeepEdge> originalEdges = KeepEdgeReader.readKeepEdges(original);
+    Set<KeepEdge> strippedEdges = KeepEdgeReader.readKeepEdges(stripped);
+    Set<KeepEdge> readdedEdges = KeepEdgeReader.readKeepEdges(readded);
+
+    // The edges are compared to the "expected" ast to ensure we don't hide failures in reading or
+    // writing.
+    assertEquals(Collections.emptySet(), strippedEdges);
+    assertEquals(expectedEdges, originalEdges);
+    assertEquals(expectedEdges, readdedEdges);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeApiTest.java b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
similarity index 87%
rename from src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeApiTest.java
rename to src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
index d69d055..bcdb8ed 100644
--- a/src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeApiTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
@@ -1,22 +1,22 @@
 // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.experimental.keepanno.ast;
+package com.android.tools.r8.keepanno.ast;
 
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.experimental.keepanno.ast.KeepOptions.KeepOption;
-import com.android.tools.r8.experimental.keepanno.keeprules.KeepRuleExtractor;
+import com.android.tools.r8.keepanno.ast.KeepOptions.KeepOption;
+import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
 import com.android.tools.r8.utils.StringUtils;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
-public class KeepEdgeApiTest extends TestBase {
+public class KeepEdgeAstTest extends TestBase {
 
   private static String CLASS = "com.example.Foo";
 
@@ -25,7 +25,7 @@
     return getTestParameters().withNoneRuntime().build();
   }
 
-  public KeepEdgeApiTest(TestParameters parameters) {
+  public KeepEdgeAstTest(TestParameters parameters) {
     parameters.assertNoneRuntime();
   }
 
@@ -102,10 +102,7 @@
         KeepEdge.builder()
             .setPreconditions(
                 KeepPreconditions.builder()
-                    .addCondition(
-                        KeepCondition.builder()
-                            .setItem(classItem(CLASS))
-                            .build())
+                    .addCondition(KeepCondition.builder().setItem(classItem(CLASS)).build())
                     .build())
             .setConsequences(
                 KeepConsequences.builder()
@@ -127,10 +124,7 @@
         KeepEdge.builder()
             .setPreconditions(
                 KeepPreconditions.builder()
-                    .addCondition(
-                        KeepCondition.builder()
-                            .setItem(classItem(CLASS))
-                            .build())
+                    .addCondition(KeepCondition.builder().setItem(classItem(CLASS)).build())
                     .build())
             .setConsequences(KeepConsequences.builder().addTarget(target(classItem(CLASS))).build())
             .build();
@@ -144,10 +138,7 @@
         KeepEdge.builder()
             .setPreconditions(
                 KeepPreconditions.builder()
-                    .addCondition(
-                        KeepCondition.builder()
-                            .setItem(classItem(CLASS))
-                            .build())
+                    .addCondition(KeepCondition.builder().setItem(classItem(CLASS)).build())
                     .build())
             .setConsequences(
                 KeepConsequences.builder()
diff --git a/src/test/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessorTest.java b/src/test/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessorTest.java
new file mode 100644
index 0000000..eb7b616
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessorTest.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.processor;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.JavaCompilerTool;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.keepanno.annotations.KeepConstants.Edge;
+import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.testsource.KeepClassAndDefaultConstructorSource;
+import com.android.tools.r8.keepanno.testsource.KeepSourceEdges;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepEdgeProcessorTest extends TestBase {
+
+  private static final Path KEEP_ANNO_PATH =
+      Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "keepanno");
+  private static final Class<?> SOURCE = KeepClassAndDefaultConstructorSource.class;
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultCfRuntime().build();
+  }
+
+  public KeepEdgeProcessorTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testClassfile() throws Exception {
+    Path out =
+        JavaCompilerTool.create(parameters.getRuntime().asCf(), temp)
+            .addAnnotationProcessors(typeName(KeepEdgeProcessor.class))
+            .addClasspathFiles(KEEP_ANNO_PATH)
+            .addClassNames(Collections.singletonList(typeName(SOURCE)))
+            .addClasspathFiles(Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "test"))
+            .addClasspathFiles(ToolHelper.DEPS)
+            .compile();
+
+    CodeInspector inspector = new CodeInspector(out);
+    checkSynthesizedKeepEdgeClass(inspector, out);
+    // The source is added as a classpath name but not part of the compilation unit output.
+    assertThat(inspector.clazz(SOURCE), isAbsent());
+  }
+
+  @Test
+  public void testJavaSource() throws Exception {
+    Path out =
+        JavaCompilerTool.create(parameters.getRuntime().asCf(), temp)
+            .addSourceFiles(ToolHelper.getSourceFileForTestClass(SOURCE))
+            .addAnnotationProcessors(typeName(KeepEdgeProcessor.class))
+            .addClasspathFiles(KEEP_ANNO_PATH)
+            .addClasspathFiles(ToolHelper.DEPS)
+            .compile();
+
+    testForJvm()
+        .addProgramFiles(out)
+        .run(parameters.getRuntime(), SOURCE)
+        .assertSuccessWithOutputLines("A is alive!")
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(SOURCE), isPresent());
+              checkSynthesizedKeepEdgeClass(inspector, out);
+            });
+  }
+
+  private void checkSynthesizedKeepEdgeClass(CodeInspector inspector, Path data)
+      throws IOException {
+    String synthesizedEdgesClassName =
+        KeepEdgeProcessor.getClassTypeNameForSynthesizedEdges(SOURCE.getTypeName());
+    ClassSubject synthesizedEdgesClass = inspector.clazz(synthesizedEdgesClassName);
+    assertThat(synthesizedEdgesClass, isPresent());
+    assertThat(synthesizedEdgesClass.annotation(Edge.CLASS.getTypeName()), isPresent());
+    String entry = ZipUtils.zipEntryNameForClass(synthesizedEdgesClass.getFinalReference());
+    byte[] bytes = ZipUtils.readSingleEntry(data, entry);
+    Set<KeepEdge> keepEdges = KeepEdgeReader.readKeepEdges(bytes);
+    assertEquals(KeepSourceEdges.getExpectedEdges(SOURCE), keepEdges);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepClassAndDefaultConstructorSource.java b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepClassAndDefaultConstructorSource.java
new file mode 100644
index 0000000..ca83a46
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepClassAndDefaultConstructorSource.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.testsource;
+
+import com.android.tools.r8.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+
+@KeepEdge(
+    consequences = {
+      // Keep the class to allow lookup of it.
+      @KeepTarget(classConstant = KeepClassAndDefaultConstructorSource.class),
+      // Keep the default constructor.
+      @KeepTarget(classConstant = KeepClassAndDefaultConstructorSource.class, methodName = "<init>")
+    })
+public class KeepClassAndDefaultConstructorSource {
+
+  public static class A {
+    public A() {
+      System.out.println("A is alive!");
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    Class<?> aClass =
+        Class.forName(
+            KeepClassAndDefaultConstructorSource.class.getPackage().getName()
+                + (args.length > 0 ? ".." : ".")
+                + "KeepClassAndDefaultConstructorSource$A");
+    aClass.getDeclaredConstructor().newInstance();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
new file mode 100644
index 0000000..187a019
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno.testsource;
+
+import com.android.tools.r8.keepanno.ast.KeepConsequences;
+import com.android.tools.r8.keepanno.ast.KeepEdge;
+import com.android.tools.r8.keepanno.ast.KeepItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepMembersPattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepMethodPattern;
+import com.android.tools.r8.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.keepanno.ast.KeepTarget;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Utility to get the AST edges for the various annotation test sources.
+ *
+ * <p>This makes it easier to share the test sources among tests (e.g., processor and asm tests).
+ */
+public class KeepSourceEdges {
+
+  public static Set<KeepEdge> getExpectedEdges(Class<?> clazz) {
+    if (clazz.equals(KeepClassAndDefaultConstructorSource.class)) {
+      return getKeepClassAndDefaultConstructorSourceEdges();
+    }
+    throw new RuntimeException();
+  }
+
+  public static Set<KeepEdge> getKeepClassAndDefaultConstructorSourceEdges() {
+    Class<?> clazz = KeepClassAndDefaultConstructorSource.class;
+    // Build the class target.
+    KeepQualifiedClassNamePattern name = KeepQualifiedClassNamePattern.exact(clazz.getTypeName());
+    KeepItemPattern classItem = KeepItemPattern.builder().setClassPattern(name).build();
+    KeepTarget classTarget = KeepTarget.builder().setItem(classItem).build();
+    // Build the constructor target.
+    KeepMethodPattern constructorMethod =
+        KeepMethodPattern.builder().setNamePattern(KeepMethodNamePattern.exact("<init>")).build();
+    KeepMembersPattern constructorMembers =
+        KeepMembersPattern.builder().addMethodPattern(constructorMethod).build();
+    KeepItemPattern constructorItem =
+        KeepItemPattern.builder()
+            .setClassPattern(name)
+            .setMembersPattern(constructorMembers)
+            .build();
+    KeepTarget constructorTarget = KeepTarget.builder().setItem(constructorItem).build();
+    // The consequet set is the class an its constructor.
+    KeepConsequences consequences =
+        KeepConsequences.builder().addTarget(classTarget).addTarget(constructorTarget).build();
+    KeepEdge edge = KeepEdge.builder().setConsequences(consequences).build();
+    return Collections.singleton(edge);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveInterfaceBridgeTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveInterfaceBridgeTest.java
index d569899..6d96680 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveInterfaceBridgeTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveInterfaceBridgeTest.java
@@ -5,9 +5,8 @@
 package com.android.tools.r8.memberrebinding;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isSynthetic;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
@@ -16,7 +15,6 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -58,10 +56,7 @@
             inspector -> {
               ClassSubject clazz = inspector.clazz(J.class);
               assertThat(clazz, isPresent());
-              // TODO(b/197490164): We should remove the bridge inserted here.
-              assertEquals(1, clazz.allMethods().size());
-              FoundMethodSubject foundMethodSubject = clazz.allMethods().get(0);
-              assertThat(foundMethodSubject, isSynthetic());
+              assertTrue(clazz.allMethods().isEmpty());
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveInterfaceBridgeWithSubTypeTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveInterfaceBridgeWithSubTypeTest.java
index 4551dbe..dd2f0c1 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveInterfaceBridgeWithSubTypeTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingRemoveInterfaceBridgeWithSubTypeTest.java
@@ -5,9 +5,8 @@
 package com.android.tools.r8.memberrebinding;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isSynthetic;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoHorizontalClassMerging;
@@ -18,7 +17,6 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,10 +61,7 @@
             inspector -> {
               ClassSubject clazz = inspector.clazz(A.class);
               assertThat(clazz, isPresent());
-              // TODO(b/197490164): We should remove the bridge inserted here.
-              assertEquals(1, clazz.virtualMethods().size());
-              FoundMethodSubject foundMethodSubject = clazz.virtualMethods().get(0);
-              assertThat(foundMethodSubject, isSynthetic());
+              assertTrue(clazz.virtualMethods().isEmpty());
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/b136195382/AbstractBridgeInheritTest.java b/src/test/java/com/android/tools/r8/shaking/b136195382/AbstractBridgeInheritTest.java
index f1fc012..58faf90 100644
--- a/src/test/java/com/android/tools/r8/shaking/b136195382/AbstractBridgeInheritTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/b136195382/AbstractBridgeInheritTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.shaking.b136195382;
 
-import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -13,8 +12,6 @@
 import com.android.tools.r8.shaking.b136195382.package2.Main;
 import com.android.tools.r8.shaking.b136195382.package2.SubFactory;
 import com.android.tools.r8.shaking.b136195382.package2.SubService;
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -35,8 +32,7 @@
   }
 
   @Test
-  public void testRemovingBridge()
-      throws ExecutionException, CompilationFailedException, IOException {
+  public void testRemovingBridge() throws Exception {
     testForR8(parameters.getBackend())
         .addProgramClasses(
             Service.class, Factory.class, SubService.class, SubFactory.class, Main.class)
@@ -44,7 +40,6 @@
         .enableInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("Hello World!")
-        .inspector();
+        .assertSuccessWithOutputLines("Hello World!");
   }
 }