R8 assistant class flag support

Change-Id: Ib73e839312997ce3d478cd35f30c2609a45454e2
diff --git a/d8_r8/test_modules/tests_java_21/build.gradle.kts b/d8_r8/test_modules/tests_java_21/build.gradle.kts
index 75a1a4e..a44697d 100644
--- a/d8_r8/test_modules/tests_java_21/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_21/build.gradle.kts
@@ -23,12 +23,14 @@
 val testbaseJavaCompileTask = projectTask("testbase", "compileJava")
 val testbaseDepsJarTask = projectTask("testbase", "depsJar")
 val mainCompileTask = projectTask("main", "compileJava")
+val assistantCompileTask = projectTask("assistant", "compileJava")
 
 dependencies {
   implementation(files(testbaseDepsJarTask.outputs.files.getSingleFile()))
   implementation(testbaseJavaCompileTask.outputs.files)
   implementation(mainCompileTask.outputs.files)
   implementation(projectTask("main", "processResources").outputs.files)
+  implementation(assistantCompileTask.outputs.files)
 }
 
 tasks {
@@ -48,6 +50,11 @@
                    layout.buildDirectory.dir("classes/java/test").get().toString())
     systemProperty("TESTBASE_DATA_LOCATION",
                    testbaseJavaCompileTask.outputs.files.getAsPath().split(File.pathSeparator)[0])
+    systemProperty(
+      "BUILD_PROP_R8_RUNTIME_PATH",
+      mainCompileTask.outputs.files.getAsPath().split(File.pathSeparator)[0] +
+        File.pathSeparator + getRoot().resolveAll("src", "main", "resources") +
+        File.pathSeparator + assistantCompileTask.outputs.files.getAsPath().split(File.pathSeparator)[0])
   }
 
   val testJar by registering(Jar::class) {
diff --git a/d8_r8/test_modules/tests_java_21/settings.gradle.kts b/d8_r8/test_modules/tests_java_21/settings.gradle.kts
index 5961a4a..bac16df 100644
--- a/d8_r8/test_modules/tests_java_21/settings.gradle.kts
+++ b/d8_r8/test_modules/tests_java_21/settings.gradle.kts
@@ -25,5 +25,6 @@
 val root = rootProject.projectDir.parentFile.parentFile
 
 includeBuild(root.resolve("shared"))
+includeBuild(root.resolve("assistant"))
 includeBuild(root.resolve("main"))
 includeBuild(root.resolve("test_modules").resolve("testbase"))
\ No newline at end of file
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
index b5c8c45..3d4353c 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
@@ -30,4 +30,7 @@
 
   @Override
   public void onClassGetSuperclass(Stack stack, Class<?> clazz) {}
+
+  @Override
+  public void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag) {}
 }
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
new file mode 100644
index 0000000..a4a49eb
--- /dev/null
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2025, 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.assistant.runtime;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+
+@KeepForApi
+public class ReflectiveOperationLogger implements ReflectiveOperationReceiver {
+
+  @Override
+  public void onClassNewInstance(Stack stack, Class<?> clazz) {
+    System.out.println("Reflectively created new instance of " + clazz.getName());
+  }
+
+  @Override
+  public void onClassGetDeclaredMethod(
+      Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+    System.out.println("Reflectively got declared method " + method + " on " + clazz.getName());
+  }
+
+  @Override
+  public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {
+    System.out.println("Reflectively got declared field " + fieldName + " on " + clazz.getName());
+  }
+
+  @Override
+  public void onClassGetDeclaredMethods(Stack stack, Class<?> clazz) {
+    System.out.println("Reflectively got declared methods on " + clazz.getName());
+  }
+
+  @Override
+  public void onClassGetName(Stack stack, Class<?> clazz, NameLookupType lookupType) {
+    System.out.println(
+        "Reflectively got name on " + clazz.getName() + "(" + lookupType.toString() + ")");
+  }
+
+  @Override
+  public void onClassForName(Stack stack, String className) {
+    System.out.println("Reflectively called Class.forName on " + className);
+  }
+
+  @Override
+  public void onClassGetSuperclass(Stack stack, Class<?> clazz) {
+    System.out.println("Reflectively called Class.getSuperclass on " + clazz.getName());
+  }
+
+  @Override
+  public void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag) {
+    System.out.println("Reflectively got class flag " + classFlag);
+  }
+
+  public boolean requiresStackInformation() {
+    return true;
+  }
+}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
index aa80d62..9c87b87 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
@@ -27,6 +27,24 @@
 
   void onClassGetSuperclass(Stack stack, Class<?> clazz);
 
+  void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag);
+
+  @KeepForApi
+  enum ClassFlag {
+    ANNOTATION,
+    ANONYMOUS_CLASS,
+    ARRAY,
+    ENUM,
+    HIDDEN,
+    INTERFACE,
+    LOCAL_CLASS,
+    MEMBER_CLASS,
+    PRIMITIVE,
+    RECORD,
+    SEALED,
+    SYNTHETIC
+  }
+
   @KeepForApi
   enum NameLookupType {
     NAME,
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
index 33eb47f..e31f5b8 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.assistant.runtime;
 
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.ClassFlag;
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
 import java.util.Arrays;
@@ -105,48 +106,51 @@
     getInstance().onClassGetSuperclass(Stack.createStack(), clazz);
   }
 
-  @KeepForApi
-  public static class ReflectiveOperationLogger implements ReflectiveOperationReceiver {
-    @Override
-    public void onClassNewInstance(Stack stack, Class<?> clazz) {
-      System.out.println("Reflectively created new instance of " + clazz.getName());
-    }
+  public static void onClassIsAnnotation(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.ANNOTATION);
+  }
 
-    @Override
-    public void onClassGetDeclaredMethod(
-        Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
-      System.out.println("Reflectively got declared method " + method + " on " + clazz.getName());
-    }
+  public static void onClassIsAnonymousClass(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.ANONYMOUS_CLASS);
+  }
 
-    @Override
-    public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {
-      System.out.println("Reflectively got declared field " + fieldName + " on " + clazz.getName());
-    }
+  public static void onClassIsArray(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.ARRAY);
+  }
 
-    @Override
-    public void onClassGetDeclaredMethods(Stack stack, Class<?> clazz) {
-      System.out.println("Reflectively got declared methods on " + clazz.getName());
-    }
+  public static void onClassIsEnum(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.ENUM);
+  }
 
-    @Override
-    public void onClassGetName(Stack stack, Class<?> clazz, NameLookupType lookupType) {
-      System.out.println(
-          "Reflectively got name on " + clazz.getName() + "(" + lookupType.toString() + ")");
-    }
+  public static void onClassIsHidden(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.HIDDEN);
+  }
 
-    @Override
-    public void onClassForName(Stack stack, String className) {
-      System.out.println("Reflectively called Class.forName on " + className);
-    }
+  public static void onClassIsInterface(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.INTERFACE);
+  }
 
-    @Override
-    public void onClassGetSuperclass(Stack stack, Class<?> clazz) {
-      System.out.println("Reflectively called Class.getSuperclass on " + clazz.getName());
-    }
+  public static void onClassIsLocalClass(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.LOCAL_CLASS);
+  }
 
-    @Override
-    public boolean requiresStackInformation() {
-      return true;
-    }
+  public static void onClassIsMemberClass(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.MEMBER_CLASS);
+  }
+
+  public static void onClassIsPrimitive(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.PRIMITIVE);
+  }
+
+  public static void onClassIsRecord(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.RECORD);
+  }
+
+  public static void onClassIsSealed(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.SEALED);
+  }
+
+  public static void onClassIsSynthetic(Class<?> clazz) {
+    getInstance().onClassFlag(Stack.createStack(), clazz, ClassFlag.SYNTHETIC);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/R8AssistantCommand.java b/src/main/java/com/android/tools/r8/R8AssistantCommand.java
index b319253..55efbd7 100644
--- a/src/main/java/com/android/tools/r8/R8AssistantCommand.java
+++ b/src/main/java/com/android/tools/r8/R8AssistantCommand.java
@@ -5,9 +5,9 @@
 
 import com.android.tools.r8.assistant.ClassInjectionHelper;
 import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationLogger;
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver;
 import com.android.tools.r8.assistant.runtime.ReflectiveOracle;
-import com.android.tools.r8.assistant.runtime.ReflectiveOracle.ReflectiveOperationLogger;
 import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Backend;
@@ -150,6 +150,7 @@
           EmptyReflectiveOperationReceiver.class,
           ReflectiveOperationLogger.class,
           ReflectiveOperationReceiver.NameLookupType.class,
+          ReflectiveOperationReceiver.ClassFlag.class,
           ReflectiveOperationReceiver.class,
           ReflectiveOracle.class,
           Stack.class);
diff --git a/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
index 8702d0a..335c1db 100644
--- a/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
+++ b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.google.common.collect.ImmutableMap;
 import java.util.Map;
@@ -25,30 +26,79 @@
   }
 
   Map<DexMethod, DexMethod> getInstrumentedMethodsAndTargets() {
-    return ImmutableMap.of(
+    ImmutableMap.Builder<DexMethod, DexMethod> builder = ImmutableMap.builder();
+
+    builder.put(
         factory.classMethods.newInstance,
-        getMethodReferenceWithClassParameter("onClassNewInstance"),
+        getMethodReferenceWithClassParameter("onClassNewInstance"));
+    builder.put(
         factory.classMethods.getDeclaredMethod,
-        getMethodReferenceWithClassMethodNameAndParameters("onClassGetDeclaredMethod"),
-        factory.classMethods.forName,
-        getMethodReferenceWithStringParameter("onClassForName"),
+        getMethodReferenceWithClassMethodNameAndParameters("onClassGetDeclaredMethod"));
+    builder.put(
+        factory.classMethods.forName, getMethodReferenceWithStringParameter("onClassForName"));
+    builder.put(
         factory.classMethods.getDeclaredField,
-        getMethodReferenceWithClassAndStringParameter("onClassGetDeclaredField"),
+        getMethodReferenceWithClassAndStringParameter("onClassGetDeclaredField"));
+    builder.put(
         factory.createMethod(
             factory.classType,
             factory.createProto(factory.createArrayType(1, factory.methodType)),
             "getDeclaredMethods"),
-        getMethodReferenceWithClassParameter("onClassGetDeclaredMethods"),
-        factory.classMethods.getName,
-        getMethodReferenceWithClassParameter("onClassGetName"),
+        getMethodReferenceWithClassParameter("onClassGetDeclaredMethods"));
+    builder.put(
+        factory.classMethods.getName, getMethodReferenceWithClassParameter("onClassGetName"));
+    builder.put(
         factory.classMethods.getCanonicalName,
-        getMethodReferenceWithClassParameter("onClassGetCanonicalName"),
+        getMethodReferenceWithClassParameter("onClassGetCanonicalName"));
+    builder.put(
         factory.classMethods.getSimpleName,
-        getMethodReferenceWithClassParameter("onClassGetSimpleName"),
+        getMethodReferenceWithClassParameter("onClassGetSimpleName"));
+    builder.put(
         factory.classMethods.getTypeName,
-        getMethodReferenceWithClassParameter("onClassGetTypeName"),
+        getMethodReferenceWithClassParameter("onClassGetTypeName"));
+    builder.put(
         factory.classMethods.getSuperclass,
         getMethodReferenceWithClassParameter("onClassGetSuperclass"));
+
+    DexProto toBoolean = factory.createProto(factory.booleanType);
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isAnnotation"),
+        getMethodReferenceWithClassParameter("onClassIsAnnotation"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isAnonymousClass"),
+        getMethodReferenceWithClassParameter("onClassIsAnonymousClass"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isArray"),
+        getMethodReferenceWithClassParameter("onClassIsArray"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isEnum"),
+        getMethodReferenceWithClassParameter("onClassIsEnum"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isHidden"),
+        getMethodReferenceWithClassParameter("onClassIsHidden"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isInterface"),
+        getMethodReferenceWithClassParameter("onClassIsInterface"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isLocalClass"),
+        getMethodReferenceWithClassParameter("onClassIsLocalClass"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isMemberClass"),
+        getMethodReferenceWithClassParameter("onClassIsMemberClass"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isPrimitive"),
+        getMethodReferenceWithClassParameter("onClassIsPrimitive"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isRecord"),
+        getMethodReferenceWithClassParameter("onClassIsRecord"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isSealed"),
+        getMethodReferenceWithClassParameter("onClassIsSealed"));
+    builder.put(
+        factory.createMethod(factory.classType, toBoolean, "isSynthetic"),
+        getMethodReferenceWithClassParameter("onClassIsSynthetic"));
+
+    return builder.build();
   }
 
   private DexMethod getMethodReferenceWithClassParameter(String name) {
diff --git a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
index 1419d3c..090c748 100644
--- a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
+++ b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
@@ -59,9 +59,7 @@
               DexType holderType = theMethod.getHolderType();
               DexClass def = appInfo.definitionFor(holderType);
               if (def != null && def.isLibraryClass()) {
-                if (holderType.toSourceString().startsWith("java.lang.reflect")
-                    || holderType.isIdenticalTo(factory.unsafeType)
-                    || holderType.isIdenticalTo(factory.classType)) {
+                if (isReflectiveType(holderType, factory)) {
                   methods.computeIfAbsent(holderType, t -> new TreeSet<>()).add(theMethod);
                 }
               }
@@ -73,6 +71,20 @@
     return methods;
   }
 
+  private static boolean isReflectiveType(DexType type, DexItemFactory factory) {
+    if (type.isIdenticalTo(factory.unsafeType) || type.isIdenticalTo(factory.classType)) {
+      return true;
+    }
+    String typeString = type.toSourceString();
+    if (!typeString.startsWith("java.lang.reflect")) {
+      return false;
+    }
+    if (type.isIdenticalTo(factory.createType("Ljava/lang/reflect/Modifier;"))) {
+      return false;
+    }
+    return !(typeString.endsWith("Exception") || typeString.endsWith("Error"));
+  }
+
   public static String printMethods(Map<DexType, Collection<DexMethod>> methods) {
     StringBuilder sb = new StringBuilder();
     List<DexType> types = new ArrayList<>(methods.keySet());
diff --git a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
index 82f58ef..26c7761 100644
--- a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
@@ -38,17 +38,17 @@
 
   @Test
   public void testGson() throws Exception {
-    test(ToolHelper.GSON, 6, 69);
+    test(ToolHelper.GSON, 13, 56);
   }
 
   @Test
   public void testGuava() throws Exception {
-    test(ToolHelper.GUAVA_JRE, 6, 99);
+    test(ToolHelper.GUAVA_JRE, 12, 82);
   }
 
   @Test
   public void testJacoco() throws Exception {
-    test(ToolHelper.JACOCO_AGENT, 3, 17);
+    test(ToolHelper.JACOCO_AGENT, 6, 13);
   }
 
   private void test(Path jar, int success, int failure) throws Exception {
diff --git a/src/test/java21/com/android/tools/r8/jdk21/assistant/JavaLangClass21Test.java b/src/test/java21/com/android/tools/r8/jdk21/assistant/JavaLangClass21Test.java
new file mode 100644
index 0000000..52e6945
--- /dev/null
+++ b/src/test/java21/com/android/tools/r8/jdk21/assistant/JavaLangClass21Test.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2025, 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.jdk21.assistant;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+import com.android.tools.r8.jdk21.assistant.JavaLangTestClass21.Foo;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class JavaLangClass21Test extends TestBase {
+
+  private static final String EXPECTED_RESULT_LOW_API =
+      StringUtils.lines(
+          "1",
+          "ANNOTATION",
+          "ANONYMOUS_CLASS",
+          "ARRAY",
+          "ENUM",
+          "HIDDEN",
+          "Missing isHidden",
+          "INTERFACE",
+          "LOCAL_CLASS",
+          "MEMBER_CLASS",
+          "PRIMITIVE",
+          "RECORD",
+          "Missing isRecord",
+          "SEALED",
+          "Missing isSealed",
+          "SYNTHETIC");
+
+  private static final String EXPECTED_RESULT_U =
+      StringUtils.lines(
+          "1",
+          "ANNOTATION",
+          "ANONYMOUS_CLASS",
+          "ARRAY",
+          "ENUM",
+          "HIDDEN",
+          "Missing isHidden",
+          "INTERFACE",
+          "LOCAL_CLASS",
+          "MEMBER_CLASS",
+          "PRIMITIVE",
+          "RECORD",
+          "SEALED",
+          "SYNTHETIC");
+
+  private static final String EXPECTED_RESULT_HIGH_API =
+      StringUtils.lines(
+          "1",
+          "ANNOTATION",
+          "ANONYMOUS_CLASS",
+          "ARRAY",
+          "ENUM",
+          "HIDDEN",
+          "INTERFACE",
+          "LOCAL_CLASS",
+          "MEMBER_CLASS",
+          "PRIMITIVE",
+          "RECORD",
+          "SEALED",
+          "SYNTHETIC");
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testInstrumentationWithCustomOracle() throws Exception {
+    testForAssistant()
+        .addProgramClasses(JavaLangTestClass21.class, Foo.class)
+        .addStrippedOuter(getClass())
+        .addInstrumentationClasses(Instrumentation.class)
+        .setCustomReflectiveOperationReceiver(Instrumentation.class)
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), JavaLangTestClass21.class)
+        .assertSuccessWithOutput(
+            parameters.getApiLevel().isLessThan(AndroidApiLevel.U)
+                ? EXPECTED_RESULT_LOW_API
+                : (parameters.getApiLevel().isEqualTo(AndroidApiLevel.U)
+                    ? EXPECTED_RESULT_U
+                    : EXPECTED_RESULT_HIGH_API));
+  }
+
+  public static class Instrumentation extends EmptyReflectiveOperationReceiver {
+
+    private void printNumIfTrue(boolean correct, int num) {
+      if (correct) {
+        System.out.println(num);
+      } else {
+        System.out.println("fail");
+      }
+    }
+
+    @Override
+    public void onClassForName(Stack stack, String className) {
+      printNumIfTrue(className.endsWith("Foo"), 1);
+    }
+
+    @Override
+    public void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag) {
+      System.out.println(classFlag);
+    }
+  }
+}
diff --git a/src/test/java21/com/android/tools/r8/jdk21/assistant/JavaLangTestClass21.java b/src/test/java21/com/android/tools/r8/jdk21/assistant/JavaLangTestClass21.java
new file mode 100644
index 0000000..89ef8c2
--- /dev/null
+++ b/src/test/java21/com/android/tools/r8/jdk21/assistant/JavaLangTestClass21.java
@@ -0,0 +1,41 @@
+// Copyright (c) 2025, 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.jdk21.assistant;
+
+public class JavaLangTestClass21 {
+
+  public static void main(String[] args) {
+    try {
+      Class<?> clazz = Class.forName(Foo.class.getName());
+      boolean ann = clazz.isAnnotation();
+      boolean anc = clazz.isAnonymousClass();
+      boolean arr = clazz.isArray();
+      boolean enu = clazz.isEnum();
+      try {
+        boolean hid = clazz.isHidden();
+      } catch (Throwable t) {
+        System.out.println("Missing isHidden");
+      }
+      boolean itf = clazz.isInterface();
+      boolean lcl = clazz.isLocalClass();
+      boolean mem = clazz.isMemberClass();
+      boolean pri = clazz.isPrimitive();
+      try {
+        boolean rec = clazz.isRecord();
+      } catch (Throwable t) {
+        System.out.println("Missing isRecord");
+      }
+      try {
+        boolean sea = clazz.isSealed();
+      } catch (Throwable t) {
+        System.out.println("Missing isSealed");
+      }
+      boolean syn = clazz.isSynthetic();
+    } catch (ClassNotFoundException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static class Foo {}
+}