[KeepAnno] Create test using extracted keep rules.

Bug: b/248408342
Change-Id: I8f55cc627a6ee280c3737b0fe71752c8c6b2ca5a
diff --git a/build.gradle b/build.gradle
index f775822..dce4a28 100644
--- a/build.gradle
+++ b/build.gradle
@@ -319,6 +319,7 @@
 
     keepannoCompile group: 'org.ow2.asm', name: 'asm', version: asmVersion
     keepannoCompile "com.google.guava:guava:$guavaVersion"
+    keepannoCompile files("third_party/openjdk/jdk8/linux-x86/lib/tools.jar")
     testCompile sourceSets.keepanno.output
     testRuntime sourceSets.keepanno.output
 }
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
index 15af2fa..8307265 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -95,7 +95,14 @@
       throw new Unimplemented();
     }
     if (!method.getReturnTypePattern().isAny()) {
-      throw new Unimplemented();
+      if (exactMethodName != null
+          && (exactMethodName.getName().equals("<init>")
+              || exactMethodName.getName().equals("<clinit>"))
+          && method.getReturnTypePattern().isVoid()) {
+        // constructors have implicit void return.
+      } else {
+        throw new Unimplemented();
+      }
     }
     if (!method.getParametersPattern().isAny()) {
       throw new Unimplemented();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
index ffcf3bd..e00fedd 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodPattern.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.keepanno.ast;
 
+import com.android.tools.r8.keepanno.ast.KeepMethodNamePattern.KeepMethodNameExactPattern;
 import java.util.Objects;
 
 public final class KeepMethodPattern extends KeepMemberPattern {
@@ -48,6 +49,15 @@
       if (namePattern == null) {
         throw new KeepEdgeException("Method pattern must declar a name pattern");
       }
+      KeepMethodReturnTypePattern returnTypePattern = this.returnTypePattern;
+      KeepMethodNameExactPattern exactName = namePattern.asExact();
+      if (exactName != null
+          && (exactName.getName().equals("<init>") || exactName.getName().equals("<clinit>"))) {
+        if (!this.returnTypePattern.isAny() && !this.returnTypePattern.isVoid()) {
+          throw new KeepEdgeException("Method constructor pattern must match 'void' type.");
+        }
+        returnTypePattern = KeepMethodReturnTypePattern.voidType();
+      }
       return new KeepMethodPattern(
           accessPattern, namePattern, returnTypePattern, parametersPattern);
     }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
index 1a9fc52..7c8c032 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractor.java
@@ -132,7 +132,7 @@
   private static StringBuilder printParameters(
       StringBuilder builder, KeepMethodParametersPattern parametersPattern) {
     if (parametersPattern.isAny()) {
-      return builder.append("(***)");
+      return builder.append("(...)");
     }
     return builder
         .append('(')
@@ -160,7 +160,7 @@
 
   private static StringBuilder printType(StringBuilder builder, KeepTypePattern typePattern) {
     if (typePattern.isAny()) {
-      return builder.append("*");
+      return builder.append("***");
     }
     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
index 3455ac8..c950191 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessor.java
@@ -21,6 +21,7 @@
 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 com.sun.tools.javac.code.Type;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
@@ -130,7 +131,11 @@
     AnnotationValue classConstantValue = getAnnotationValue(mirror, Target.classConstant);
     if (classConstantValue != null) {
       DeclaredType type = AnnotationClassValueVisitor.getType(classConstantValue);
-      itemBuilder.setClassPattern(KeepQualifiedClassNamePattern.exact(type.toString()));
+      // The processor API does not expose the descriptor or typename, so we need to depend on the
+      // sun.tools package and cast to its internal type to extract it. If not, this code will not
+      // work for inner classes as we cannot recover the $ separator.
+      String typeName = ((Type) type).tsym.flatName().toString();
+      itemBuilder.setClassPattern(KeepQualifiedClassNamePattern.exact(typeName));
     }
     AnnotationValue methodNameValue = getAnnotationValue(mirror, Target.methodName);
     if (methodNameValue != null) {
diff --git a/src/test/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorTest.java b/src/test/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorTest.java
new file mode 100644
index 0000000..94001ec
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/keeprules/KeepRuleExtractorTest.java
@@ -0,0 +1,63 @@
+// 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.keeprules;
+
+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.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 java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepRuleExtractorTest extends TestBase {
+
+  private static final Class<?> SOURCE = KeepClassAndDefaultConstructorSource.class;
+  private static final String EXPECTED = KeepSourceEdges.getExpected(SOURCE);
+  private static final Path KEEP_ANNO_PATH =
+      Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "keepanno");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public KeepRuleExtractorTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    List<String> rules = getKeepRulesForClass(SOURCE);
+    testForR8(parameters.getBackend())
+        .addClasspathFiles(KEEP_ANNO_PATH)
+        .addProgramClassesAndInnerClasses(SOURCE)
+        .addKeepRules(rules)
+        .addKeepMainRule(SOURCE)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), SOURCE)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  private List<String> getKeepRulesForClass(Class<?> clazz) throws IOException {
+    Set<KeepEdge> keepEdges = KeepEdgeReader.readKeepEdges(ToolHelper.getClassAsBytes(clazz));
+    List<String> rules = new ArrayList<>();
+    KeepRuleExtractor extractor = new KeepRuleExtractor(rules::add);
+    keepEdges.forEach(extractor::extract);
+    return rules;
+  }
+}
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
index eb7b616..83f60a3 100644
--- a/src/test/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessorTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/processor/KeepEdgeProcessorTest.java
@@ -36,6 +36,7 @@
   private static final Path KEEP_ANNO_PATH =
       Paths.get(ToolHelper.BUILD_DIR, "classes", "java", "keepanno");
   private static final Class<?> SOURCE = KeepClassAndDefaultConstructorSource.class;
+  private static final String EXPECTED = KeepSourceEdges.getExpected(SOURCE);
 
   private final TestParameters parameters;
 
@@ -63,6 +64,12 @@
     checkSynthesizedKeepEdgeClass(inspector, out);
     // The source is added as a classpath name but not part of the compilation unit output.
     assertThat(inspector.clazz(SOURCE), isAbsent());
+
+    testForJvm()
+        .addProgramClasses(SOURCE, KeepClassAndDefaultConstructorSource.A.class)
+        .addProgramFiles(out)
+        .run(parameters.getRuntime(), SOURCE)
+        .assertSuccessWithOutput(EXPECTED);
   }
 
   @Test
@@ -78,7 +85,7 @@
     testForJvm()
         .addProgramFiles(out)
         .run(parameters.getRuntime(), SOURCE)
-        .assertSuccessWithOutputLines("A is alive!")
+        .assertSuccessWithOutput(EXPECTED)
         .inspect(
             inspector -> {
               assertThat(inspector.clazz(SOURCE), isPresent());
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
index ca83a46..b9fb2b6 100644
--- a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepClassAndDefaultConstructorSource.java
+++ b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepClassAndDefaultConstructorSource.java
@@ -9,9 +9,11 @@
 @KeepEdge(
     consequences = {
       // Keep the class to allow lookup of it.
-      @KeepTarget(classConstant = KeepClassAndDefaultConstructorSource.class),
+      @KeepTarget(classConstant = KeepClassAndDefaultConstructorSource.A.class),
       // Keep the default constructor.
-      @KeepTarget(classConstant = KeepClassAndDefaultConstructorSource.class, methodName = "<init>")
+      @KeepTarget(
+          classConstant = KeepClassAndDefaultConstructorSource.A.class,
+          methodName = "<init>")
     })
 public class KeepClassAndDefaultConstructorSource {
 
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
index 03b63c6..5637edf 100644
--- a/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
+++ b/src/test/java/com/android/tools/r8/keepanno/testsource/KeepSourceEdges.java
@@ -10,6 +10,7 @@
 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.utils.StringUtils;
 import java.util.Collections;
 import java.util.Set;
 
@@ -27,8 +28,19 @@
     throw new RuntimeException();
   }
 
+  public static String getExpected(Class<?> clazz) {
+    if (clazz.equals(KeepClassAndDefaultConstructorSource.class)) {
+      return getKeepClassAndDefaultConstructorSourceExpected();
+    }
+    throw new RuntimeException();
+  }
+
+  public static String getKeepClassAndDefaultConstructorSourceExpected() {
+    return StringUtils.lines("A is alive!");
+  }
+
   public static Set<KeepEdge> getKeepClassAndDefaultConstructorSourceEdges() {
-    Class<?> clazz = KeepClassAndDefaultConstructorSource.class;
+    Class<?> clazz = KeepClassAndDefaultConstructorSource.A.class;
     // Build the class target.
     KeepQualifiedClassNamePattern name = KeepQualifiedClassNamePattern.exact(clazz.getTypeName());
     KeepItemPattern classItem = KeepItemPattern.builder().setClassPattern(name).build();