Retain signature of native methods in excluded classes

Bug: b/415977217
Change-Id: I66d8b401ff4475e67048b28911ce8f99d63e2611
diff --git a/src/main/java/com/android/tools/r8/partial/R8PartialUseCollector.java b/src/main/java/com/android/tools/r8/partial/R8PartialUseCollector.java
index 71ad575..6e30e4ee 100644
--- a/src/main/java/com/android/tools/r8/partial/R8PartialUseCollector.java
+++ b/src/main/java/com/android/tools/r8/partial/R8PartialUseCollector.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndField;
 import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
@@ -27,6 +28,7 @@
 import com.android.tools.r8.shaking.reflectiveidentification.ReflectiveIdentification;
 import com.android.tools.r8.tracereferences.TraceReferencesConsumer;
 import com.android.tools.r8.tracereferences.UseCollector;
+import com.android.tools.r8.tracereferences.UseCollectorEventConsumer;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.NopDiagnosticsHandler;
 import com.android.tools.r8.utils.timing.Timing;
@@ -55,6 +57,11 @@
             appView, new KeepAllReflectiveIdentificationEventConsumer(this));
   }
 
+  @Override
+  protected UseCollectorEventConsumer getEventConsumerForNativeMethod() {
+    return new KeepNativeMethodSignatureEventConsumer();
+  }
+
   public static Predicate<DexType> getTargetPredicate(
       AppView<? extends AppInfoWithClassHierarchy> appView) {
     return type -> appView.definitionFor(type) != null;
@@ -94,36 +101,40 @@
 
   @Override
   public void notifyPresentClass(DexClass clazz, DefinitionContext referencedFrom) {
-    notifyPresentItem(clazz, referencedFrom);
+    keepAllowObfuscation(clazz, referencedFrom);
   }
 
   @Override
   public void notifyPresentField(DexClassAndField field, DefinitionContext referencedFrom) {
-    notifyPresentItem(field, referencedFrom);
+    keepAllowObfuscation(field, referencedFrom);
   }
 
   @Override
   public void notifyPresentMethod(DexClassAndMethod method, DefinitionContext referencedFrom) {
-    notifyPresentItem(method, referencedFrom);
+    keepAllowObfuscation(method, referencedFrom);
   }
 
   @Override
   public void notifyPresentMethod(
       DexClassAndMethod method, DefinitionContext referencedFrom, DexMethod reference) {
-    notifyPresentItem(method, referencedFrom);
+    keepAllowObfuscation(method, referencedFrom);
   }
 
   @Override
   public void notifyPresentMethodOverride(
       DexClassAndMethod method, ProgramMethod override, DefinitionContext referencedFrom) {
-    if (seenDisallowObfuscation.add(method.getReference())) {
-      keep(method, referencedFrom, false);
+    keepDisallowObfuscation(method, referencedFrom);
+  }
+
+  private void keepAllowObfuscation(Definition definition, DefinitionContext referencedFrom) {
+    if (seenAllowObfuscation.add(definition.getReference())) {
+      keep(definition, referencedFrom, true);
     }
   }
 
-  private void notifyPresentItem(Definition definition, DefinitionContext referencedFrom) {
-    if (seenAllowObfuscation.add(definition.getReference())) {
-      keep(definition, referencedFrom, true);
+  private void keepDisallowObfuscation(Definition definition, DefinitionContext referencedFrom) {
+    if (seenDisallowObfuscation.add(definition.getReference())) {
+      keep(definition, referencedFrom, false);
     }
   }
 
@@ -137,6 +148,56 @@
     reflectiveIdentification.scanInvoke(invokedMethod, method);
   }
 
+  private class KeepNativeMethodSignatureEventConsumer implements UseCollectorEventConsumer {
+
+    @Override
+    public void notifyPresentClass(DexClass clazz, DefinitionContext referencedFrom) {
+      keepDisallowObfuscation(clazz, referencedFrom);
+    }
+
+    @Override
+    public void notifyMissingClass(DexType type, DefinitionContext referencedFrom) {
+      R8PartialUseCollector.this.notifyMissingClass(type, referencedFrom);
+    }
+
+    @Override
+    public void notifyPackageOf(Definition definition) {
+      R8PartialUseCollector.this.notifyPackageOf(definition);
+    }
+
+    @Override
+    public void notifyPresentField(DexClassAndField field, DefinitionContext referencedFrom) {
+      assert false;
+    }
+
+    @Override
+    public void notifyMissingField(DexField field, DefinitionContext referencedFrom) {
+      assert false;
+    }
+
+    @Override
+    public void notifyPresentMethod(DexClassAndMethod method, DefinitionContext referencedFrom) {
+      assert false;
+    }
+
+    @Override
+    public void notifyPresentMethod(
+        DexClassAndMethod method, DefinitionContext referencedFrom, DexMethod reference) {
+      assert false;
+    }
+
+    @Override
+    public void notifyPresentMethodOverride(
+        DexClassAndMethod method, ProgramMethod override, DefinitionContext referencedFrom) {
+      assert false;
+    }
+
+    @Override
+    public void notifyMissingMethod(DexMethod method, DefinitionContext referencedFrom) {
+      assert false;
+    }
+  }
+
   private static class MissingReferencesConsumer implements TraceReferencesConsumer {
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/tracereferences/UseCollector.java b/src/main/java/com/android/tools/r8/tracereferences/UseCollector.java
index 2171eac..2152274 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/UseCollector.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/UseCollector.java
@@ -101,6 +101,10 @@
     return this;
   }
 
+  protected UseCollectorEventConsumer getEventConsumerForNativeMethod() {
+    return getDefaultEventConsumer();
+  }
+
   protected void notifyReflectiveIdentification(DexMethod invokedMethod, ProgramMethod method) {
     // Intentionally empty. Overridden in R8PartialUseCollector.
   }
@@ -429,8 +433,10 @@
 
   private void registerMethod(ProgramMethod method, UseCollectorEventConsumer eventConsumer) {
     DefinitionContext referencedFrom = DefinitionContextUtils.create(method);
-    addTypes(method.getParameters(), referencedFrom, eventConsumer);
-    addType(method.getReturnType(), referencedFrom, eventConsumer);
+    UseCollectorEventConsumer signatureEventConsumer =
+        method.getAccessFlags().isNative() ? getEventConsumerForNativeMethod() : eventConsumer;
+    addTypes(method.getParameters(), referencedFrom, signatureEventConsumer);
+    addType(method.getReturnType(), referencedFrom, signatureEventConsumer);
     method
         .getAnnotations()
         .forEach(
diff --git a/src/test/java/com/android/tools/r8/partial/PartialCompilationNativeMethodTest.java b/src/test/java/com/android/tools/r8/partial/PartialCompilationNativeMethodTest.java
new file mode 100644
index 0000000..2a98f55
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/partial/PartialCompilationNativeMethodTest.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.partial;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static junit.framework.TestCase.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 PartialCompilationNativeMethodTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8Partial(parameters)
+        .addR8IncludedClasses(IncludedClass.class)
+        .addR8ExcludedClasses(ExcludedClass.class)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject includedClassSubject = inspector.clazz(IncludedClass.class);
+              assertThat(includedClassSubject, isPresentAndNotRenamed());
+
+              MethodSubject nativeMethodSubject =
+                  inspector.clazz(ExcludedClass.class).uniqueMethodWithOriginalName("m");
+              assertThat(nativeMethodSubject, isPresent());
+              assertEquals(
+                  includedClassSubject.asTypeSubject(), nativeMethodSubject.getParameter(0));
+            });
+  }
+
+  static class ExcludedClass {
+
+    public static native void m(IncludedClass includedClass);
+  }
+
+  static class IncludedClass {}
+}