[KeepAnno] Regression test for matching class with any members
The semantics of keepclasseswithmembers does not match the semantics of
"any member" when no members are present.
Bug: b/322104143
Bug: b/323136645
Change-Id: I6008f4f78dff5e19cb53e8f1d8151a91afa644fc
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
index 34f12ed..030ae56 100644
--- a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -248,7 +248,14 @@
@Override
public ExternalR8TestBuilder addProgramClassFileData(Collection<byte[]> classes) {
- throw new Unimplemented("No support for adding classfile data directly");
+ try {
+ Path out = getState().getNewTempFolder().resolve("out.jar");
+ TestBase.writeClassFileDataToJar(out, classes);
+ programJars.add(out);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return self();
}
@Override
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
index 60f5701..5beaaf2 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -18,6 +18,7 @@
import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import org.junit.rules.TemporaryFolder;
@@ -53,6 +54,14 @@
public abstract KeepAnnoTestBuilder addProgramClasses(List<Class<?>> programClasses)
throws IOException;
+ public final KeepAnnoTestBuilder addProgramClassFileData(byte[]... programClasses)
+ throws IOException {
+ return addProgramClassFileData(Arrays.asList(programClasses));
+ }
+
+ public abstract KeepAnnoTestBuilder addProgramClassFileData(List<byte[]> programClasses)
+ throws IOException;
+
public final KeepAnnoTestBuilder addKeepMainRule(Class<?> mainClass) {
return applyIfShrinker(b -> b.addKeepMainRule(mainClass));
}
@@ -126,6 +135,13 @@
}
@Override
+ public KeepAnnoTestBuilder addProgramClassFileData(List<byte[]> programClasses)
+ throws IOException {
+ builder.addProgramClassFileData(programClasses);
+ return this;
+ }
+
+ @Override
public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
return builder.run(parameters().getRuntime(), mainClass);
}
@@ -165,6 +181,14 @@
}
@Override
+ public KeepAnnoTestBuilder addProgramClassFileData(List<byte[]> programClasses)
+ throws IOException {
+
+ builder.addProgramClassFileData(programClasses);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder inspectOutputConfig(Consumer<String> configConsumer) {
compileResultConsumers.add(
result -> configConsumer.accept(result.getProguardConfiguration()));
@@ -211,6 +235,16 @@
}
@Override
+ public KeepAnnoTestBuilder addProgramClassFileData(List<byte[]> programClasses)
+ throws IOException {
+ List<String> rules =
+ KeepAnnoTestUtils.extractRulesFromBytes(programClasses, extractorOptions);
+ builder.addProgramClassFileData(programClasses);
+ builder.addKeepRules(rules);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder inspectOutputConfig(Consumer<String> configConsumer) {
configConsumers.add(lines -> configConsumer.accept(String.join("\n", lines)));
return this;
@@ -253,6 +287,16 @@
}
@Override
+ public KeepAnnoTestBuilder addProgramClassFileData(List<byte[]> programClasses)
+ throws IOException {
+ List<String> rules =
+ KeepAnnoTestUtils.extractRulesFromBytes(programClasses, extractorOptions);
+ builder.addProgramClassFileData(programClasses);
+ builder.addKeepRules(rules);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder inspectOutputConfig(Consumer<String> configConsumer) {
configConsumers.add(lines -> configConsumer.accept(String.join("\n", lines)));
return this;
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
index 176c263..d15896f 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
@@ -13,6 +13,7 @@
import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ListUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -53,15 +54,28 @@
}
public static List<String> extractRules(
- List<Class<?>> inputClasses, KeepRuleExtractorOptions extractorOptions) throws IOException {
+ List<Class<?>> inputClasses, KeepRuleExtractorOptions extractorOptions) {
+ return extractRulesFromBytes(
+ ListUtils.map(
+ inputClasses,
+ clazz -> {
+ try {
+ return ToolHelper.getClassAsBytes(clazz);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }),
+ extractorOptions);
+ }
+
+ public static List<String> extractRulesFromBytes(
+ List<byte[]> inputClasses, KeepRuleExtractorOptions extractorOptions) {
List<String> rules = new ArrayList<>();
- for (Class<?> inputClass : inputClasses) {
- byte[] bytes = ToolHelper.getClassAsBytes(inputClass);
+ for (byte[] bytes : inputClasses) {
List<KeepDeclaration> declarations = KeepEdgeReader.readKeepEdges(bytes);
KeepRuleExtractor extractor = new KeepRuleExtractor(rules::add, extractorOptions);
declarations.forEach(extractor::extract);
}
return rules;
}
-
}
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java b/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java
new file mode 100644
index 0000000..f4f769b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepEmptyClassTest.java
@@ -0,0 +1,82 @@
+// Copyright (c) 2024, 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 static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+@RunWith(Parameterized.class)
+public class KeepEmptyClassTest extends KeepAnnoTestBase {
+
+ static final String EXPECTED = StringUtils.lines("B, #members: 0");
+ static final String UNEXPECTED = StringUtils.lines("b, #members: 0");
+
+ @Parameter public KeepAnnoParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static List<KeepAnnoParameters> data() {
+ return createParameters(
+ getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build());
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForKeepAnno(parameters)
+ .addProgramClasses(getInputClasses())
+ .addProgramClassFileData(
+ transformer(B.class).removeMethods(MethodPredicate.all()).transform())
+ .setExcludedOuterClass(getClass())
+ .run(TestClass.class)
+ // TODO(b/322104143): The any-member pattern does not match due to classeswithmembers.
+ .assertSuccessWithOutput(parameters.isReference() ? EXPECTED : UNEXPECTED)
+ .applyIf(parameters.isShrinker(), r -> r.inspect(this::checkOutput));
+ }
+
+ public List<Class<?>> getInputClasses() {
+ return ImmutableList.of(TestClass.class, A.class);
+ }
+
+ private void checkOutput(CodeInspector inspector) {
+ // TODO(b/322104143): The class should not be renamed.
+ assertThat(inspector.clazz(B.class), isPresentAndRenamed());
+ }
+
+ static class A {
+
+ // Pattern includes any members
+ @UsesReflection(@KeepTarget(classConstant = B.class, kind = KeepItemKind.CLASS_AND_MEMBERS))
+ public void foo() throws Exception {
+ String typeName = B.class.getTypeName();
+ int memberCount = B.class.getDeclaredMethods().length + B.class.getDeclaredFields().length;
+ System.out.println(typeName.substring(typeName.length() - 1) + ", #members: " + memberCount);
+ }
+ }
+
+ static class B {
+ // Completely empty class ensured by transformer (no default constructor).
+ }
+
+ static class TestClass {
+
+ @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+ public static void main(String[] args) throws Exception {
+ new A().foo();
+ }
+ }
+}