[KeepAnno] Remove KeepForApi default member targets on empty classes

Bug: b/248408342
Fixes: b/310545869

Change-Id: I63924732f525e79a890529857b1d2ecdf00e25a2
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
index bc466c5..cf705ed 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -3,6 +3,7 @@
 // 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.KeepItemKind;
 import com.android.tools.r8.keepanno.ast.AccessVisibility;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants;
 import com.android.tools.r8.keepanno.ast.AnnotationConstants.Binding;
@@ -91,6 +92,9 @@
   private static class KeepEdgeClassVisitor extends ClassVisitor {
     private final Parent<KeepDeclaration> parent;
     private String className;
+    private List<AnnotationVisitorBase> annotationVisitors = new ArrayList<>();
+    private boolean done = false;
+    private boolean hasMembers = false;
 
     KeepEdgeClassVisitor(Parent<KeepDeclaration> parent) {
       super(ASM_VERSION);
@@ -101,6 +105,17 @@
       return binaryName.replace('/', '.');
     }
 
+    public boolean classHasMembers() {
+      assert done;
+      return hasMembers;
+    }
+
+    @Override
+    public void visitEnd() {
+      done = true;
+      annotationVisitors.forEach(a -> a.onClassVisitEnd(this));
+    }
+
     @Override
     public void visit(
         int version,
@@ -119,20 +134,28 @@
       if (visible) {
         return null;
       }
+      AnnotationVisitorBase annotationVisitor = determineAnnotationVisitor(descriptor);
+      if (annotationVisitor != null) {
+        annotationVisitors.add(annotationVisitor);
+      }
+      return annotationVisitor;
+    }
+
+    private AnnotationVisitorBase determineAnnotationVisitor(String descriptor) {
       if (descriptor.equals(Edge.DESCRIPTOR)) {
         return new KeepEdgeVisitor(parent::accept, this::setContext);
       }
-      if (descriptor.equals(AnnotationConstants.UsesReflection.DESCRIPTOR)) {
+      if (descriptor.equals(UsesReflection.DESCRIPTOR)) {
         KeepClassItemPattern classItem =
             KeepClassItemPattern.builder()
                 .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
                 .build();
         return new UsesReflectionVisitor(parent::accept, this::setContext, classItem);
       }
-      if (descriptor.equals(AnnotationConstants.ForApi.DESCRIPTOR)) {
+      if (descriptor.equals(ForApi.DESCRIPTOR)) {
         return new ForApiClassVisitor(parent::accept, this::setContext, className);
       }
-      if (descriptor.equals(AnnotationConstants.UsedByReflection.DESCRIPTOR)
+      if (descriptor.equals(UsedByReflection.DESCRIPTOR)
           || descriptor.equals(AnnotationConstants.UsedByNative.DESCRIPTOR)) {
         return new UsedByReflectionClassVisitor(
             descriptor, parent::accept, this::setContext, className);
@@ -155,12 +178,14 @@
     @Override
     public MethodVisitor visitMethod(
         int access, String name, String descriptor, String signature, String[] exceptions) {
+      hasMembers = true;
       return new KeepEdgeMethodVisitor(parent::accept, className, name, descriptor);
     }
 
     @Override
     public FieldVisitor visitField(
         int access, String name, String descriptor, String signature, Object value) {
+      hasMembers = true;
       return new KeepEdgeFieldVisitor(parent::accept, className, name, descriptor);
     }
   }
@@ -320,6 +345,8 @@
       super(ASM_VERSION);
     }
 
+    public void onClassVisitEnd(KeepEdgeClassVisitor visitor) {}
+
     public abstract String getAnnotationName();
 
     private String errorMessagePrefix() {
@@ -441,8 +468,6 @@
       addContext.accept(metaInfoBuilder);
       // The class context/holder is the annotated class.
       visit(Item.className, className);
-      // The default kind is to target the class and its members.
-      visitEnum(null, Kind.DESCRIPTOR, Kind.CLASS_AND_MEMBERS);
     }
 
     @Override
@@ -478,9 +503,16 @@
     }
 
     @Override
-    public void visitEnd() {
+    public void onClassVisitEnd(KeepEdgeClassVisitor visitor) {
+      if (getKind() == null) {
+        // The default kind is to target the class and its members if any members exist.
+        KeepItemKind defaultKind =
+            visitor.classHasMembers() ? KeepItemKind.CLASS_AND_MEMBERS : KeepItemKind.ONLY_CLASS;
+        visitEnum(null, Kind.DESCRIPTOR, defaultKind.name());
+      }
       if (!getKind().equals(Kind.ONLY_CLASS) && isDefaultMemberDeclaration()) {
         // If no member declarations have been made, set public & protected as the default.
+        // Only do so if the class actually has any members.
         AnnotationVisitor v = visitArray(Item.memberAccess);
         v.visitEnum(null, MemberAccess.DESCRIPTOR, MemberAccess.PUBLIC);
         v.visitEnum(null, MemberAccess.DESCRIPTOR, MemberAccess.PROTECTED);
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileClassRuleInfo.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileClassRuleInfo.java
index 6dcde1d..29d745b 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileClassRuleInfo.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileClassRuleInfo.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.profile.art;
 
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
-import com.android.tools.r8.keepanno.annotations.KeepItemKind;
 
-@KeepForApi(kind = KeepItemKind.ONLY_CLASS)
+@KeepForApi
 public interface ArtProfileClassRuleInfo {}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepForApiWithoutMembersTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepForApiWithoutMembersTest.java
new file mode 100644
index 0000000..e715fff
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepForApiWithoutMembersTest.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2023, 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;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepForApiWithoutMembersTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("true");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public KeepForApiWithoutMembersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .compileWithExpectedDiagnostics(TestDiagnosticMessages::assertNoInfos)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, EmptyInterface.class, A.class);
+  }
+
+  @KeepForApi
+  interface EmptyInterface {}
+
+  static class A implements EmptyInterface {}
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      System.out.println(new A() instanceof EmptyInterface);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/UsedByReflectionWithoutMembersTest.java b/src/test/java/com/android/tools/r8/keepanno/UsedByReflectionWithoutMembersTest.java
new file mode 100644
index 0000000..7fa44ba
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/UsedByReflectionWithoutMembersTest.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2023, 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;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UsedByReflectionWithoutMembersTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("true");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+  }
+
+  public UsedByReflectionWithoutMembersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getInputClasses())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testWithRuleExtraction() throws Exception {
+    testForR8(parameters.getBackend())
+        .enableExperimentalKeepAnnotations()
+        .addProgramClasses(getInputClasses())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .compileWithExpectedDiagnostics(TestDiagnosticMessages::assertNoInfos)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  public List<Class<?>> getInputClasses() {
+    return ImmutableList.of(TestClass.class, EmptyInterface.class, A.class);
+  }
+
+  @UsedByReflection
+  interface EmptyInterface {}
+
+  static class A implements EmptyInterface {}
+
+  static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      System.out.println(new A() instanceof EmptyInterface);
+    }
+  }
+}