blob: 12d33bc76a08244a34e203410baad64ac1561a7d [file]
// Copyright (c) 2026, 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.apimodel;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
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.apimodel.ParsedApiClassTrimming.JarTrimmer;
import com.android.tools.r8.apimodel.jar.ApiJarInfo;
import com.android.tools.r8.apimodel.jar.ApiJarMerging;
import com.android.tools.r8.apimodel.jar.ApiJarReader;
import com.android.tools.r8.references.ClassReference;
import com.android.tools.r8.references.MethodReference;
import com.android.tools.r8.references.Reference;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.internal.ListUtils;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class ParsedApiClassJarTrimmerTest extends TestBase {
@Parameterized.Parameters(name = "{0}")
public static TestParametersCollection data() {
return getTestParameters().withNoneRuntime().build();
}
public ParsedApiClassJarTrimmerTest(TestParameters parameters) {
parameters.assertNoneRuntime();
}
@SuppressWarnings("unused")
public static class TestClassA {
public void methodA() {}
public static void staticMethodA() {}
public int fieldA;
}
public static class TestClassB extends TestClassA {
public void methodB() {}
}
@SuppressWarnings("unused")
public interface TestInterfaceI {
void interfaceMethod();
default void defaultMethod() {}
static void staticInterfaceMethod() {}
}
public static class TestClassC implements TestInterfaceI {
@Override
public void interfaceMethod() {}
}
@Test
public void testTrimming() throws Exception {
Path jar =
writeClassesToJar(
TestClassA.class, TestClassB.class, TestInterfaceI.class, TestClassC.class);
// We need a complete hierarchy, so we also pass the android jar (which has Object).
List<Path> jars = Arrays.asList(jar, ToolHelper.getDefaultAndroidJar());
ClassReference classARef = Reference.classFromClass(TestClassA.class);
ClassReference classBRef = Reference.classFromClass(TestClassB.class);
ClassReference classCRef = Reference.classFromClass(TestClassC.class);
ClassReference interfaceIRef = Reference.classFromClass(TestInterfaceI.class);
ApiRange range = new ApiRange(AndroidApiLevel.B);
// Setup API classes.
ParsedApiClass apiClassA = new ParsedApiClass(classARef, range);
apiClassA.registerSupertype(Reference.classFromClass(Object.class), range);
MethodReference methodA = Reference.methodFromMethod(TestClassA.class.getMethod("methodA"));
MethodReference staticMethodA =
Reference.method(classARef, "staticMethodA", Collections.emptyList(), null);
FieldTypelessReference fieldA = new FieldTypelessReference(classARef, "fieldA");
MethodReference nonExistentMethod =
Reference.method(classARef, "nonExistentMethod", Collections.emptyList(), null);
FieldTypelessReference nonExistentField =
new FieldTypelessReference(classARef, "nonExistentField");
apiClassA.registerMethod(methodA, range);
apiClassA.registerMethod(staticMethodA, range);
apiClassA.registerField(fieldA, range);
apiClassA.registerMethod(nonExistentMethod, range);
apiClassA.registerField(nonExistentField, range);
ParsedApiClass apiClassB = new ParsedApiClass(classBRef, range);
apiClassB.registerSupertype(classARef, range);
MethodReference methodB = Reference.methodFromMethod(TestClassB.class.getMethod("methodB"));
MethodReference inheritedMethodA =
Reference.method(classBRef, "methodA", Collections.emptyList(), null);
MethodReference inheritedStaticMethodA =
Reference.method(classBRef, "staticMethodA", Collections.emptyList(), null);
FieldTypelessReference inheritedFieldA = new FieldTypelessReference(classBRef, "fieldA");
apiClassB.registerMethod(methodB, range);
apiClassB.registerMethod(inheritedMethodA, range);
apiClassB.registerMethod(inheritedStaticMethodA, range);
apiClassB.registerField(inheritedFieldA, range);
ParsedApiClass apiInterfaceI = new ParsedApiClass(interfaceIRef, range);
apiInterfaceI.registerSupertype(Reference.classFromClass(Object.class), range);
MethodReference interfaceMethod =
Reference.method(interfaceIRef, "interfaceMethod", Collections.emptyList(), null);
MethodReference defaultMethod =
Reference.method(interfaceIRef, "defaultMethod", Collections.emptyList(), null);
MethodReference staticInterfaceMethod =
Reference.method(interfaceIRef, "staticInterfaceMethod", Collections.emptyList(), null);
apiInterfaceI.registerMethod(interfaceMethod, range);
apiInterfaceI.registerMethod(defaultMethod, range);
apiInterfaceI.registerMethod(staticInterfaceMethod, range);
ParsedApiClass apiClassC = new ParsedApiClass(classCRef, range);
apiClassC.registerSupertype(Reference.classFromClass(Object.class), range);
apiClassC.registerInterface(interfaceIRef, range);
MethodReference inheritedDefaultMethod =
Reference.method(classCRef, "defaultMethod", Collections.emptyList(), null);
MethodReference nonInheritedStaticInterfaceMethod =
Reference.method(classCRef, "staticInterfaceMethod", Collections.emptyList(), null);
apiClassC.registerMethod(inheritedDefaultMethod, range);
apiClassC.registerMethod(nonInheritedStaticInterfaceMethod, range);
// Class not in JAR,
ClassReference classDRef =
Reference.classFromBinaryName("com/android/tools/r8/apimodel/NonExistentClassD");
ParsedApiClass apiClassD = new ParsedApiClass(classDRef, range);
Collection<ParsedApiClass> apiClasses =
Arrays.asList(apiClassA, apiClassB, apiInterfaceI, apiClassC, apiClassD);
// Run JAR trimmer.
List<ApiJarInfo> readJars = ListUtils.mapThrowing(jars, ApiJarReader::read);
ApiJarInfo jarInfo = ApiJarMerging.mergeJarInfos(readJars);
Collection<ParsedApiClass> trimmed =
ParsedApiClassTrimming.trim(apiClasses, new JarTrimmer(jarInfo));
// Verify results.
Map<ClassReference, ParsedApiClass> trimmedMap = new HashMap<>();
for (ParsedApiClass clazz : trimmed) {
trimmedMap.put(clazz.getClassReference(), clazz);
}
// Verify Class D is discarded.
assertFalse(trimmedMap.containsKey(classDRef));
// Verify Class A.
assertTrue(trimmedMap.containsKey(classARef));
ParsedApiClass trimmedA = trimmedMap.get(classARef);
assertTrue(trimmedA.hasMethod(methodA));
assertTrue(trimmedA.hasMethod(staticMethodA));
assertTrue(trimmedA.hasField(fieldA));
assertFalse(trimmedA.hasMethod(nonExistentMethod));
assertFalse(trimmedA.hasField(nonExistentField));
// Verify Class B.
assertTrue(trimmedMap.containsKey(classBRef));
ParsedApiClass trimmedB = trimmedMap.get(classBRef);
assertTrue(trimmedB.hasMethod(methodB));
assertTrue(trimmedB.hasMethod(inheritedMethodA));
assertTrue(trimmedB.hasMethod(inheritedStaticMethodA));
assertTrue(trimmedB.hasField(inheritedFieldA));
// Verify Interface I.
assertTrue(trimmedMap.containsKey(interfaceIRef));
ParsedApiClass trimmedI = trimmedMap.get(interfaceIRef);
assertTrue(trimmedI.hasMethod(interfaceMethod));
assertTrue(trimmedI.hasMethod(defaultMethod));
assertTrue(trimmedI.hasMethod(staticInterfaceMethod));
// Verify Class C.
assertTrue(trimmedMap.containsKey(classCRef));
ParsedApiClass trimmedC = trimmedMap.get(classCRef);
assertTrue(trimmedC.hasMethod(inheritedDefaultMethod));
assertFalse(trimmedC.hasMethod(nonInheritedStaticInterfaceMethod));
}
}