Reland "ASM-based reader/writer and Java preprocessor for keep annotations."

Bug: b/248408342

This reverts commit 5189431b2affc70400b7df1fae2dac363d54469e.

Change-Id: I204081bad423a746a071bbe75745c571e9f9ebb9
diff --git a/build.gradle b/build.gradle
index eca19a7..8dbce66 100644
--- a/build.gradle
+++ b/build.gradle
@@ -198,6 +198,11 @@
         }
         output.resourcesDir = 'build/classes/kotlinR8TestResources'
     }
+    keepanno {
+        java {
+            srcDirs = ['src/keepanno/main/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/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepCondition.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepCondition.java
new file mode 100644
index 0000000..6dd46b2
--- /dev/null
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/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.experimental.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/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepConstants.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepConstants.java
new file mode 100644
index 0000000..c12ba98
--- /dev/null
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepConstants.java
@@ -0,0 +1,34 @@
+// 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.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";
+  }
+}
diff --git a/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepEdge.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepEdge.java
new file mode 100644
index 0000000..d1ae40a
--- /dev/null
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/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.experimental.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/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepTarget.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/annotations/KeepTarget.java
new file mode 100644
index 0000000..f272568
--- /dev/null
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/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.experimental.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/main/java/com/android/tools/r8/experimental/keepanno/asm/KeepEdgeReader.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/asm/KeepEdgeReader.java
new file mode 100644
index 0000000..3c705c7
--- /dev/null
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/asm/KeepEdgeReader.java
@@ -0,0 +1,172 @@
+// 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.asm;
+
+import com.android.tools.r8.experimental.keepanno.annotations.KeepConstants.Edge;
+import com.android.tools.r8.experimental.keepanno.annotations.KeepConstants.Target;
+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.KeepEdgeException;
+import com.android.tools.r8.experimental.keepanno.ast.KeepItemPattern;
+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 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 String classConstant = 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) {
+        classConstant = ((Type) value).getClassName();
+        return;
+      }
+      super.visit(name, value);
+    }
+
+    @Override
+    public void visitEnd() {
+      if (classConstant != null) {
+        KeepTarget target =
+            KeepTarget.builder()
+                .setItem(
+                    KeepItemPattern.builder()
+                        .setClassPattern(KeepQualifiedClassNamePattern.exact(classConstant))
+                        .build())
+                .build();
+        parent.accept(target);
+        return;
+      }
+      throw new KeepEdgeException("Unexpected failure to build @KeepTarget");
+    }
+  }
+}
diff --git a/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/asm/KeepEdgeWriter.java
new file mode 100644
index 0000000..bda02d8
--- /dev/null
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/asm/KeepEdgeWriter.java
@@ -0,0 +1,75 @@
+// 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.asm;
+
+import com.android.tools.r8.experimental.keepanno.annotations.KeepConstants;
+import com.android.tools.r8.experimental.keepanno.annotations.KeepConstants.Edge;
+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.KeepPreconditions;
+import com.android.tools.r8.experimental.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.experimental.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();
+                    }
+                    return null;
+                  });
+          targetVisitor.visitEnd();
+        });
+    arrayVisitor.visitEnd();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
similarity index 99%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
index acd327d..ce3f502 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepCondition.java
@@ -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/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
similarity index 78%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
index 6a227fd..524192e 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepConsequences.java
@@ -40,6 +40,7 @@
   private final List<KeepTarget> targets;
 
   private KeepConsequences(List<KeepTarget> targets) {
+    assert targets != null;
     this.targets = targets;
   }
 
@@ -50,4 +51,21 @@
   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();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
similarity index 76%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
index f61483a..7cd6d9f 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdge.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.experimental.keepanno.ast;
 
+import java.util.Objects;
+
 /**
  * An edge in the keep graph.
  *
@@ -43,6 +45,8 @@
   private final KeepConsequences consequences;
 
   private KeepEdge(KeepPreconditions preconditions, KeepConsequences consequences) {
+    assert preconditions != null;
+    assert consequences != null;
     this.preconditions = preconditions;
     this.consequences = consequences;
   }
@@ -54,4 +58,22 @@
   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);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeException.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
similarity index 74%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
index 17e543f..1f6ef9d 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepExtendsPattern.java
@@ -42,6 +42,16 @@
     public boolean isAny() {
       return true;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
   }
 
   private static class KeepExtendsClassPattern extends KeepExtendsPattern {
@@ -49,6 +59,7 @@
     private final KeepQualifiedClassNamePattern pattern;
 
     public KeepExtendsClassPattern(KeepQualifiedClassNamePattern pattern) {
+      assert pattern != null;
       this.pattern = pattern;
     }
 
@@ -56,6 +67,23 @@
     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();
+    }
   }
 
   public static Builder builder() {
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepFieldPattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
similarity index 82%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
index 3c6f947..443d63b 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepItemPattern.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.experimental.keepanno.ast;
 
+import java.util.Objects;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -87,6 +88,16 @@
     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);
+    }
   }
 
   public static class KeepClassPattern extends KeepItemPattern {
@@ -100,6 +111,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 +144,25 @@
     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);
+    }
   }
 
   public abstract boolean isAny();
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMemberPattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
similarity index 82%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
index 6d3f93e..f8b3515 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMembersPattern.java
@@ -3,9 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.experimental.keepanno.ast;
 
-import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.experimental.keepanno.utils.Unimplemented;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 public abstract class KeepMembersPattern {
@@ -89,6 +90,16 @@
     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);
+    }
   }
 
   private static class KeepMembersNonePattern extends KeepMembersPattern {
@@ -116,6 +127,16 @@
     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);
+    }
   }
 
   private static class KeepMembersSomePattern extends KeepMembersPattern {
@@ -146,6 +167,23 @@
       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);
+    }
   }
 
   private KeepMembersPattern() {}
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodAccessPattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodAccessPattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodAccessPattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodAccessPattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodNamePattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodParametersPattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodPattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepMethodReturnTypePattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
similarity index 74%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
index 9fb521c..573a482 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepOptions.java
@@ -3,15 +3,19 @@
 // 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.HashSet;
+import java.util.Objects;
 import java.util.Set;
 
 public final class KeepOptions {
 
+  public boolean isKeepAll() {
+    return allowIfSet ? options.isEmpty() : options.size() == KeepOption.values().length;
+  }
+
   public enum KeepOption {
     SHRINKING,
     OPTIMIZING,
@@ -44,7 +48,7 @@
 
   public static class Builder {
     public final boolean allowIfSet;
-    public Set<KeepOption> options = SetUtils.newIdentityHashSet();
+    public Set<KeepOption> options = new HashSet<>();
 
     private Builder(boolean allowIfSet) {
       this.allowIfSet = allowIfSet;
@@ -77,7 +81,7 @@
         }
         throw new KeepEdgeException("Invalid keep options that allow everything.");
       }
-      return new KeepOptions(allowIfSet, ImmutableSet.copyOf(options));
+      return new KeepOptions(allowIfSet, Collections.unmodifiableSet(options));
     }
   }
 
@@ -94,4 +98,23 @@
   public boolean isAllowed(KeepOption option) {
     return options.contains(option) == allowIfSet;
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    // This does not actually capture equivalence. We should normalize the builder the 'allow'
+    // variant always.
+    KeepOptions that = (KeepOptions) o;
+    return allowIfSet == that.allowIfSet && options.equals(that.options);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(allowIfSet, options);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPackagePattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
similarity index 75%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
index f3083df..2a2c3b2 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepPreconditions.java
@@ -59,6 +59,16 @@
     public void forEach(Consumer<KeepCondition> fn) {
       // Empty.
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
   }
 
   private static class KeepPreconditionsSome extends KeepPreconditions {
@@ -66,6 +76,8 @@
     private final List<KeepCondition> preconditions;
 
     private KeepPreconditionsSome(List<KeepCondition> preconditions) {
+      assert preconditions != null;
+      assert !preconditions.isEmpty();
       this.preconditions = preconditions;
     }
 
@@ -78,5 +90,22 @@
     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();
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
similarity index 86%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
index 9485580..8aae535 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepQualifiedClassNamePattern.java
@@ -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;
   }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
similarity index 75%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
index a63a34a..059668a 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTarget.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.experimental.keepanno.ast;
 
+import java.util.Objects;
+
 public class KeepTarget {
 
   public static KeepTarget any() {
@@ -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,21 @@
   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);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTypePattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTypePattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTypePattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepTypePattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java
similarity index 100%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/ast/KeepUnqualfiedClassNamePattern.java
diff --git a/src/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java
similarity index 96%
rename from src/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java
rename to src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java
index 6c24ec1..f5d09fe 100644
--- a/src/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/keeprules/KeepRuleExtractor.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.experimental.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;
@@ -22,11 +21,11 @@
 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.experimental.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,12 @@
       StringBuilder builder, KeepMethodParametersPattern parametersPattern) {
     return parametersPattern.match(
         () -> builder.append("(***)"),
-        list -> StringUtils.append(builder, list, ", ", BraceType.PARENS));
+        list -> {
+          return builder
+              .append('{')
+              .append(list.stream().map(Object::toString).collect(Collectors.joining(", ")))
+              .append('}');
+        });
   }
 
   private static StringBuilder printMethodName(
diff --git a/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/processor/KeepEdgeProcessor.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/processor/KeepEdgeProcessor.java
new file mode 100644
index 0000000..a195fd4
--- /dev/null
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/processor/KeepEdgeProcessor.java
@@ -0,0 +1,247 @@
+// 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.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.experimental.keepanno.annotations.KeepConstants;
+import com.android.tools.r8.experimental.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.experimental.keepanno.asm.KeepEdgeWriter;
+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.KeepEdge.Builder;
+import com.android.tools.r8.experimental.keepanno.ast.KeepItemPattern;
+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.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.experimental.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 = findEnclosingTypeElement(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 TypeElement findEnclosingTypeElement(Element element) {
+    while (true) {
+      if (element == null || element instanceof TypeElement) {
+        return (TypeElement) element;
+      }
+      element = element.getEnclosingElement();
+    }
+  }
+
+  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, "preconditions");
+    if (preconditions == null) {
+      return;
+    }
+    throw new Unimplemented();
+  }
+
+  private void processConsequences(Builder edgeBuilder, AnnotationMirror mirror) {
+    AnnotationValue consequences = getAnnotationValue(mirror, "consequences");
+    if (consequences == null) {
+      return;
+    }
+    KeepConsequences.Builder consequencesBuilder = KeepConsequences.builder();
+    AnnotationListValueVisitor v =
+        new AnnotationListValueVisitor(
+            value -> {
+              KeepTarget.Builder targetBuilder = KeepTarget.builder();
+              processTarget(targetBuilder, getAnnotationMirror(value, KeepConstants.Target.CLASS));
+              consequencesBuilder.addTarget(targetBuilder.build());
+            });
+    consequences.accept(v, null);
+    edgeBuilder.setConsequences(consequencesBuilder.build());
+  }
+
+  private void processTarget(KeepTarget.Builder builder, AnnotationMirror mirror) {
+    KeepItemPattern.Builder itemBuilder = KeepItemPattern.builder();
+    AnnotationValue classConstantValue = getAnnotationValue(mirror, "classConstant");
+    if (classConstantValue != null) {
+      AnnotationClassValueVisitor v = new AnnotationClassValueVisitor();
+      classConstantValue.accept(v, null);
+      itemBuilder.setClassPattern(KeepQualifiedClassNamePattern.exact(v.type.toString()));
+    }
+    builder.setItem(itemBuilder.build());
+  }
+
+  public static class AnnotationListValueVisitor
+      extends SimpleAnnotationValueVisitor7<AnnotationListValueVisitor, Object> {
+
+    private final Consumer<AnnotationValue> fn;
+
+    public AnnotationListValueVisitor(Consumer<AnnotationValue> fn) {
+      this.fn = fn;
+    }
+
+    @Override
+    protected AnnotationListValueVisitor defaultAction(Object o, Object o2) {
+      throw new IllegalStateException();
+    }
+
+    @Override
+    public AnnotationListValueVisitor visitArray(List<? extends AnnotationValue> vals, Object o) {
+      vals.forEach(fn::accept);
+      return this;
+    }
+  }
+
+  public static class AnnotationMirrorValueVisitor
+      extends SimpleAnnotationValueVisitor7<AnnotationMirrorValueVisitor, Object> {
+
+    private AnnotationMirror mirror = null;
+
+    @Override
+    protected AnnotationMirrorValueVisitor defaultAction(Object o, Object o2) {
+      throw new IllegalStateException();
+    }
+
+    @Override
+    public AnnotationMirrorValueVisitor visitAnnotation(AnnotationMirror a, Object o) {
+      mirror = a;
+      return this;
+    }
+  }
+
+  public static class AnnotationClassValueVisitor
+      extends SimpleAnnotationValueVisitor7<AnnotationClassValueVisitor, Object> {
+
+    private DeclaredType type = null;
+
+    @Override
+    protected AnnotationClassValueVisitor defaultAction(Object o, Object o2) {
+      throw new IllegalStateException();
+    }
+
+    @Override
+    public AnnotationClassValueVisitor visitType(TypeMirror t, Object o) {
+      ClassTypeVisitor classTypeVisitor = new ClassTypeVisitor();
+      t.accept(classTypeVisitor, null);
+      type = classTypeVisitor.t;
+      return this;
+    }
+  }
+
+  public static class ClassTypeVisitor extends SimpleTypeVisitor7<ClassTypeVisitor, Object> {
+
+    DeclaredType t = null;
+
+    @Override
+    protected ClassTypeVisitor defaultAction(TypeMirror e, Object o) {
+      throw new IllegalStateException();
+    }
+
+    @Override
+    public ClassTypeVisitor visitDeclared(DeclaredType t, Object o) {
+      this.t = t;
+      return this;
+    }
+  }
+
+  private void error(String message) {
+    processingEnv.getMessager().printMessage(Kind.ERROR, message);
+  }
+
+  private static AnnotationMirror getAnnotationMirror(AnnotationValue value, Class<?> annoClass) {
+    AnnotationMirrorValueVisitor v = new AnnotationMirrorValueVisitor();
+    value.accept(v, null);
+    return v.mirror;
+  }
+
+  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;
+  }
+}
diff --git a/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/utils/Unimplemented.java b/src/keepanno/main/java/com/android/tools/r8/experimental/keepanno/utils/Unimplemented.java
new file mode 100644
index 0000000..0112a09
--- /dev/null
+++ b/src/keepanno/main/java/com/android/tools/r8/experimental/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.experimental.keepanno.utils;
+
+public class Unimplemented extends RuntimeException {
+  public Unimplemented() {}
+
+  public Unimplemented(String msg) {
+    super(msg);
+  }
+}
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/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/experimental/keepanno/asm/KeepEdgeAsmTest.java b/src/test/java/com/android/tools/r8/experimental/keepanno/asm/KeepEdgeAsmTest.java
new file mode 100644
index 0000000..832d7bc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/experimental/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.experimental.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.experimental.keepanno.ast.KeepEdge;
+import com.android.tools.r8.experimental.keepanno.testsource.KeepClassAndDefaultConstructorSource;
+import com.android.tools.r8.experimental.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/experimental/keepanno/ast/KeepEdgeAstTest.java
similarity index 90%
rename from src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeApiTest.java
rename to src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeAstTest.java
index d69d055..34a1f32 100644
--- a/src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeApiTest.java
+++ b/src/test/java/com/android/tools/r8/experimental/keepanno/ast/KeepEdgeAstTest.java
@@ -16,7 +16,7 @@
 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/experimental/keepanno/processor/KeepEdgeProcessorTest.java b/src/test/java/com/android/tools/r8/experimental/keepanno/processor/KeepEdgeProcessorTest.java
new file mode 100644
index 0000000..9ce665e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/experimental/keepanno/processor/KeepEdgeProcessorTest.java
@@ -0,0 +1,99 @@
+// 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.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.experimental.keepanno.annotations.KeepConstants.Edge;
+import com.android.tools.r8.experimental.keepanno.asm.KeepEdgeReader;
+import com.android.tools.r8.experimental.keepanno.ast.KeepEdge;
+import com.android.tools.r8.experimental.keepanno.testsource.KeepClassAndDefaultConstructorSource;
+import com.android.tools.r8.experimental.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 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(Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "main"))
+            .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(Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "main"))
+            .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/experimental/keepanno/testsource/KeepClassAndDefaultConstructorSource.java b/src/test/java/com/android/tools/r8/experimental/keepanno/testsource/KeepClassAndDefaultConstructorSource.java
new file mode 100644
index 0000000..0550498
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/experimental/keepanno/testsource/KeepClassAndDefaultConstructorSource.java
@@ -0,0 +1,36 @@
+// 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.testsource;
+
+import com.android.tools.r8.experimental.keepanno.annotations.KeepEdge;
+import com.android.tools.r8.experimental.keepanno.annotations.KeepTarget;
+
+@KeepEdge(
+    consequences = {
+      // Keep the class to allow lookup of it.
+      @KeepTarget(classConstant = KeepClassAndDefaultConstructorSource.class)
+      // TODO(b/248408342): Add default constructor target.
+      // // 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/experimental/keepanno/testsource/KeepSourceEdges.java b/src/test/java/com/android/tools/r8/experimental/keepanno/testsource/KeepSourceEdges.java
new file mode 100644
index 0000000..6c4f8fa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/experimental/keepanno/testsource/KeepSourceEdges.java
@@ -0,0 +1,37 @@
+// 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.testsource;
+
+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.KeepItemPattern;
+import com.android.tools.r8.experimental.keepanno.ast.KeepQualifiedClassNamePattern;
+import com.android.tools.r8.experimental.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;
+    KeepQualifiedClassNamePattern name = KeepQualifiedClassNamePattern.exact(clazz.getTypeName());
+    KeepItemPattern item = KeepItemPattern.builder().setClassPattern(name).build();
+    KeepTarget target = KeepTarget.builder().setItem(item).build();
+    KeepConsequences consequences = KeepConsequences.builder().addTarget(target).build();
+    KeepEdge edge = KeepEdge.builder().setConsequences(consequences).build();
+    return Collections.singleton(edge);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 63efc58..b0d3664 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -66,7 +66,7 @@
       List<MethodTransformer> methodTransformers,
       int flags) {
     ClassReader reader = new ClassReader(bytes);
-    ClassWriter writer = new ClassWriter(reader, flags);
+    ClassWriter writer = new ClassWriter(flags);
     ClassVisitor subvisitor = new InnerMostClassTransformer(writer, methodTransformers);
     for (int i = classTransformers.size() - 1; i >= 0; i--) {
       classTransformers.get(i).setSubVisitor(subvisitor);