Add ReflectiveCallExtractor

- Allows one to extract all reflective calls
  done from a jar.

Change-Id: Iae5aa812aa435f57230972079fef089ae5bac1fc
diff --git a/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
new file mode 100644
index 0000000..8702d0a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
@@ -0,0 +1,80 @@
+// 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;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import java.util.Set;
+
+public class InstrumentedReflectiveMethodList {
+
+  private DexItemFactory factory;
+  private ReflectiveReferences reflectiveReferences;
+
+  public InstrumentedReflectiveMethodList(DexItemFactory factory) {
+    this.factory = factory;
+    reflectiveReferences = new ReflectiveReferences(factory);
+  }
+
+  public Set<DexMethod> getInstrumentedMethodsForTesting() {
+    return getInstrumentedMethodsAndTargets().keySet();
+  }
+
+  Map<DexMethod, DexMethod> getInstrumentedMethodsAndTargets() {
+    return ImmutableMap.of(
+        factory.classMethods.newInstance,
+        getMethodReferenceWithClassParameter("onClassNewInstance"),
+        factory.classMethods.getDeclaredMethod,
+        getMethodReferenceWithClassMethodNameAndParameters("onClassGetDeclaredMethod"),
+        factory.classMethods.forName,
+        getMethodReferenceWithStringParameter("onClassForName"),
+        factory.classMethods.getDeclaredField,
+        getMethodReferenceWithClassAndStringParameter("onClassGetDeclaredField"),
+        factory.createMethod(
+            factory.classType,
+            factory.createProto(factory.createArrayType(1, factory.methodType)),
+            "getDeclaredMethods"),
+        getMethodReferenceWithClassParameter("onClassGetDeclaredMethods"),
+        factory.classMethods.getName,
+        getMethodReferenceWithClassParameter("onClassGetName"),
+        factory.classMethods.getCanonicalName,
+        getMethodReferenceWithClassParameter("onClassGetCanonicalName"),
+        factory.classMethods.getSimpleName,
+        getMethodReferenceWithClassParameter("onClassGetSimpleName"),
+        factory.classMethods.getTypeName,
+        getMethodReferenceWithClassParameter("onClassGetTypeName"),
+        factory.classMethods.getSuperclass,
+        getMethodReferenceWithClassParameter("onClassGetSuperclass"));
+  }
+
+  private DexMethod getMethodReferenceWithClassParameter(String name) {
+    return getMethodReferenceWithParameterTypes(name, factory.classType);
+  }
+
+  private DexMethod getMethodReferenceWithClassAndStringParameter(String name) {
+    return getMethodReferenceWithParameterTypes(name, factory.classType, factory.stringType);
+  }
+
+  private DexMethod getMethodReferenceWithStringParameter(String name) {
+    return getMethodReferenceWithParameterTypes(name, factory.stringType);
+  }
+
+  private DexMethod getMethodReferenceWithParameterTypes(String name, DexType... dexTypes) {
+    return factory.createMethod(
+        reflectiveReferences.reflectiveOracleType,
+        factory.createProto(factory.voidType, dexTypes),
+        name);
+  }
+
+  private DexMethod getMethodReferenceWithClassMethodNameAndParameters(String name) {
+    return factory.createMethod(
+        reflectiveReferences.reflectiveOracleType,
+        factory.createProto(
+            factory.voidType, factory.classType, factory.stringType, factory.classArrayType),
+        name);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/ReflectiveInstrumentation.java b/src/main/java/com/android/tools/r8/assistant/ReflectiveInstrumentation.java
index d3567a1..e2dc6ad 100644
--- a/src/main/java/com/android/tools/r8/assistant/ReflectiveInstrumentation.java
+++ b/src/main/java/com/android/tools/r8/assistant/ReflectiveInstrumentation.java
@@ -25,7 +25,7 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.utils.timing.Timing;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
+import java.util.Map;
 
 public class ReflectiveInstrumentation {
 
@@ -72,8 +72,8 @@
 
   // TODO(b/394013779): Do this in parallel.
   public void instrumentClasses() {
-    ImmutableMap<DexMethod, DexMethod> instrumentedMethodsAndTargets =
-        getInstrumentedMethodsAndTargets();
+    Map<DexMethod, DexMethod> instrumentedMethodsAndTargets =
+        new InstrumentedReflectiveMethodList(dexItemFactory).getInstrumentedMethodsAndTargets();
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       clazz.forEachProgramMethodMatching(
           method -> method.hasCode() && method.getCode().isDexCode(),
@@ -109,65 +109,6 @@
     }
   }
 
-  private ImmutableMap<DexMethod, DexMethod> getInstrumentedMethodsAndTargets() {
-    return ImmutableMap.of(
-        dexItemFactory.classMethods.newInstance,
-        getMethodReferenceWithClassParameter("onClassNewInstance"),
-        dexItemFactory.classMethods.getDeclaredMethod,
-        getMethodReferenceWithClassMethodNameAndParameters("onClassGetDeclaredMethod"),
-        dexItemFactory.classMethods.forName,
-        getMethodReferenceWithStringParameter("onClassForName"),
-        dexItemFactory.classMethods.getDeclaredField,
-        getMethodReferenceWithClassAndStringParameter("onClassGetDeclaredField"),
-        dexItemFactory.createMethod(
-            dexItemFactory.classType,
-            dexItemFactory.createProto(
-                dexItemFactory.createArrayType(1, dexItemFactory.methodType)),
-            "getDeclaredMethods"),
-        getMethodReferenceWithClassParameter("onClassGetDeclaredMethods"),
-        dexItemFactory.classMethods.getName,
-        getMethodReferenceWithClassParameter("onClassGetName"),
-        dexItemFactory.classMethods.getCanonicalName,
-        getMethodReferenceWithClassParameter("onClassGetCanonicalName"),
-        dexItemFactory.classMethods.getSimpleName,
-        getMethodReferenceWithClassParameter("onClassGetSimpleName"),
-        dexItemFactory.classMethods.getTypeName,
-        getMethodReferenceWithClassParameter("onClassGetTypeName"),
-        dexItemFactory.classMethods.getSuperclass,
-        getMethodReferenceWithClassParameter("onClassGetSuperclass"));
-  }
-
-  private DexMethod getMethodReferenceWithClassParameter(String name) {
-    return getMethodReferenceWithParameterTypes(name, dexItemFactory.classType);
-  }
-
-  private DexMethod getMethodReferenceWithClassAndStringParameter(String name) {
-    return getMethodReferenceWithParameterTypes(
-        name, dexItemFactory.classType, dexItemFactory.stringType);
-  }
-
-  private DexMethod getMethodReferenceWithStringParameter(String name) {
-    return getMethodReferenceWithParameterTypes(name, dexItemFactory.stringType);
-  }
-
-  private DexMethod getMethodReferenceWithParameterTypes(String name, DexType... dexTypes) {
-    return dexItemFactory.createMethod(
-        reflectiveReferences.reflectiveOracleType,
-        dexItemFactory.createProto(dexItemFactory.voidType, dexTypes),
-        name);
-  }
-
-  private DexMethod getMethodReferenceWithClassMethodNameAndParameters(String name) {
-    return dexItemFactory.createMethod(
-        reflectiveReferences.reflectiveOracleType,
-        dexItemFactory.createProto(
-            dexItemFactory.voidType,
-            dexItemFactory.classType,
-            dexItemFactory.stringType,
-            dexItemFactory.classArrayType),
-        name);
-  }
-
   private void insertCallToMethod(
       DexMethod method,
       IRCode code,
diff --git a/src/test/java/com/android/tools/r8/assistant/GsonReflectiveCallExtractorTest.java b/src/test/java/com/android/tools/r8/assistant/GsonReflectiveCallExtractorTest.java
new file mode 100644
index 0000000..02a40a5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/GsonReflectiveCallExtractorTest.java
@@ -0,0 +1,63 @@
+// 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;
+
+import static com.android.tools.r8.assistant.ReflectiveCallExtractor.extractReflectiveCalls;
+import static com.android.tools.r8.assistant.ReflectiveCallExtractor.printMethods;
+
+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.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Assert;
+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 GsonReflectiveCallExtractorTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  @Test
+  public void testGson() throws Exception {
+    DexItemFactory factory = new DexItemFactory();
+    Map<DexType, Collection<DexMethod>> reflectiveMethods =
+        extractReflectiveCalls(ToolHelper.GSON, factory);
+    Set<DexMethod> instrumentedMethodsForTesting =
+        new InstrumentedReflectiveMethodList(factory).getInstrumentedMethodsForTesting();
+    int supported = 0;
+    int unsupported = 0;
+    for (DexType dexType : reflectiveMethods.keySet()) {
+      Collection<DexMethod> methods = reflectiveMethods.get(dexType);
+      ArrayList<DexMethod> toRemove = new ArrayList<>();
+      for (DexMethod dexMethod : methods) {
+        if (instrumentedMethodsForTesting.contains(dexMethod)) {
+          toRemove.add(dexMethod);
+          supported++;
+        } else {
+          unsupported++;
+        }
+      }
+      methods.removeAll(toRemove);
+    }
+    Assert.assertEquals(6, supported);
+    Assert.assertEquals("Missing :\n" + printMethods(reflectiveMethods), 69, unsupported);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
new file mode 100644
index 0000000..4dd7e86
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
@@ -0,0 +1,94 @@
+// 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;
+
+import com.android.tools.r8.ArchiveProgramResourceProvider;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.LazyLoadedDexApplication;
+import com.android.tools.r8.synthesis.SyntheticItems.GlobalSyntheticsStrategy;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.timing.Timing;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ReflectiveCallExtractor {
+
+  public static Map<DexType, Collection<DexMethod>> extractReflectiveCalls(
+      Path jar, DexItemFactory factory) throws IOException {
+    AndroidApp app =
+        AndroidApp.builder()
+            .addProgramResourceProvider(ArchiveProgramResourceProvider.fromArchive(jar))
+            .addLibraryFile(ToolHelper.getAndroidJar(AndroidApiLevel.V))
+            .build();
+    InternalOptions options = new InternalOptions(factory, new Reporter());
+    ApplicationReader applicationReader = new ApplicationReader(app, options, Timing.empty());
+    LazyLoadedDexApplication dexApplication =
+        applicationReader.read(ThreadUtils.getExecutorService(options));
+    AppInfo appInfo =
+        AppInfo.createInitialAppInfo(dexApplication, GlobalSyntheticsStrategy.forNonSynthesizing());
+    Map<DexType, Collection<DexMethod>> methods = new IdentityHashMap<>();
+    for (DexProgramClass programClass : appInfo.classes()) {
+      for (DexEncodedMethod method : programClass.methods()) {
+        if (method.hasCode()) {
+          for (CfInstruction instruction : method.getCode().asCfCode().getInstructions()) {
+            if (instruction.isInvoke()) {
+              CfInvoke cfInvoke = instruction.asInvoke();
+              DexMethod theMethod = cfInvoke.getMethod();
+              DexType holderType = theMethod.getHolderType();
+              DexClass def = appInfo.definitionFor(holderType);
+              if (!holderType.isArrayType() && def.isLibraryClass()) {
+                if (holderType.toSourceString().startsWith("java.lang.reflect")
+                    || holderType.isIdenticalTo(appInfo.dexItemFactory().classType)) {
+                  methods
+                      .computeIfAbsent(holderType, t -> Sets.newIdentityHashSet())
+                      .add(theMethod);
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    for (DexType dexType : methods.keySet()) {
+      ArrayList<DexMethod> sorted = new ArrayList<>(methods.get(dexType));
+      sorted.sort(Comparator.naturalOrder());
+      methods.put(dexType, sorted);
+    }
+    return methods;
+  }
+
+  public static String printMethods(Map<DexType, Collection<DexMethod>> methods) {
+    StringBuilder sb = new StringBuilder();
+    List<DexType> types = new ArrayList<>(methods.keySet());
+    types.sort(Comparator.naturalOrder());
+    for (DexType type : types) {
+      sb.append("+++ ").append(type).append(" +++").append("\n");
+      for (DexMethod dexMethod : methods.get(type)) {
+        sb.append(dexMethod).append("\n");
+      }
+    }
+    return sb.toString();
+  }
+}