Add keep annotation processing of @UsesReflectionToConstruct
Bug: b/392865072
Change-Id: I98d699aedd94ffaefa6786182fdb238dfdef3b02
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
index 7104c92..88b07f1 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeReader.java
@@ -21,6 +21,7 @@
import com.android.tools.r8.keepanno.ast.AnnotationConstants.MethodAccess;
import com.android.tools.r8.keepanno.ast.AnnotationConstants.Target;
import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsedByReflection;
+import com.android.tools.r8.keepanno.ast.AnnotationConstants.UsesReflectionToConstruct;
import com.android.tools.r8.keepanno.ast.KeepAnnotationPattern;
import com.android.tools.r8.keepanno.ast.KeepBindingReference;
import com.android.tools.r8.keepanno.ast.KeepBindings;
@@ -29,6 +30,7 @@
import com.android.tools.r8.keepanno.ast.KeepCheck.KeepCheckKind;
import com.android.tools.r8.keepanno.ast.KeepClassBindingReference;
import com.android.tools.r8.keepanno.ast.KeepClassItemPattern;
+import com.android.tools.r8.keepanno.ast.KeepClassPattern;
import com.android.tools.r8.keepanno.ast.KeepCondition;
import com.android.tools.r8.keepanno.ast.KeepConsequences;
import com.android.tools.r8.keepanno.ast.KeepConstraint;
@@ -102,6 +104,7 @@
private static boolean isEmbeddedAnnotation(String descriptor) {
if (AnnotationConstants.Edge.isDescriptor(descriptor)
|| AnnotationConstants.UsesReflection.isDescriptor(descriptor)
+ || AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)
|| AnnotationConstants.ForApi.isDescriptor(descriptor)
|| AnnotationConstants.UsedByReflection.isDescriptor(descriptor)
|| AnnotationConstants.UsedByNative.isDescriptor(descriptor)
@@ -356,6 +359,16 @@
.setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
.build());
}
+ if (AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)) {
+ return new UsesReflectionToConstructVisitor(
+ parsingContext,
+ parent::accept,
+ setContext,
+ bindingsHelper ->
+ KeepClassItemPattern.builder()
+ .setClassNamePattern(KeepQualifiedClassNamePattern.exact(className))
+ .build());
+ }
if (ForApi.isDescriptor(descriptor)) {
return new ForApiClassVisitor(parsingContext, parent::accept, setContext, className);
}
@@ -498,6 +511,23 @@
bindingsHelper ->
createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
}
+ if (AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)) {
+ return new UsesReflectionToConstructVisitor(
+ parsingContext,
+ parent::accept,
+ setContext,
+ bindingsHelper ->
+ createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
+ }
+ if (AnnotationConstants.UsesReflectionToConstruct.isKotlinRepeatableContainerDescriptor(
+ descriptor)) {
+ return new UsesReflectionForInstantiationContainerVisitor(
+ parsingContext,
+ parent::accept,
+ setContext,
+ bindingsHelper ->
+ createMethodItemContext(className, methodName, methodDescriptor, bindingsHelper));
+ }
if (AnnotationConstants.ForApi.isDescriptor(descriptor)) {
return new ForApiMemberVisitor(
parsingContext,
@@ -1340,6 +1370,226 @@
}
}
+ private static class ParametersClassVisitor extends AnnotationVisitorBase {
+ private final ParsingContext parsingContext;
+ private final Consumer<KeepMethodParametersPattern> consumer;
+ private final KeepMethodParametersPattern.Builder builder =
+ KeepMethodParametersPattern.builder();
+
+ public ParametersClassVisitor(
+ PropertyParsingContext parsingContext, Consumer<KeepMethodParametersPattern> consumer) {
+ super(parsingContext);
+ this.parsingContext = parsingContext;
+ this.consumer = consumer;
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ assert name == null;
+ if (value instanceof String) {
+ builder.addParameterTypePattern(KeepTypePattern.fromDescriptor("L" + value + ";"));
+ } else if (value instanceof Type) {
+ builder.addParameterTypePattern(
+ KeepTypePattern.fromDescriptor(((Type) value).getDescriptor()));
+ } else {
+ super.visit(name, value);
+ }
+ }
+
+ @Override
+ public void visitEnd() {
+ consumer.accept(builder.build());
+ }
+ }
+
+ private static class ParametersClassNamesVisitor extends AnnotationVisitorBase {
+ private final ParsingContext parsingContext;
+ private final Consumer<KeepMethodParametersPattern> consumer;
+ private final KeepMethodParametersPattern.Builder builder =
+ KeepMethodParametersPattern.builder();
+
+ public ParametersClassNamesVisitor(
+ PropertyParsingContext parsingContext, Consumer<KeepMethodParametersPattern> consumer) {
+ super(parsingContext);
+ this.parsingContext = parsingContext;
+ this.consumer = consumer;
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ assert name == null;
+ if (value instanceof String) {
+ builder.addParameterTypePattern(KeepTypePattern.fromDescriptor("L" + value + ";"));
+ } else {
+ super.visit(name, value);
+ }
+ }
+
+ @Override
+ public void visitEnd() {
+ consumer.accept(builder.build());
+ }
+ }
+
+ private static class UsesReflectionForInstantiationContainerVisitor
+ extends AnnotationVisitorBase {
+
+ private final AnnotationParsingContext parsingContext;
+ private final Parent<KeepEdge> parent;
+ Consumer<KeepEdgeMetaInfo.Builder> addContext;
+ Function<UserBindingsHelper, KeepItemPattern> contextBuilder;
+
+ UsesReflectionForInstantiationContainerVisitor(
+ AnnotationParsingContext parsingContext,
+ Parent<KeepEdge> parent,
+ Consumer<KeepEdgeMetaInfo.Builder> addContext,
+ Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+ super(parsingContext);
+ this.parsingContext = parsingContext;
+ this.parent = parent;
+ this.addContext = addContext;
+ this.contextBuilder = contextBuilder;
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String name) {
+ if (name.equals("value")) {
+ return new UsesReflectionForInstantiationsVisitor(
+ parsingContext, parent, addContext, contextBuilder);
+ }
+ return super.visitArray(name);
+ }
+
+ @Override
+ public void visitEnd() {
+ super.visitEnd();
+ }
+ }
+
+ private static class UsesReflectionForInstantiationsVisitor extends AnnotationVisitorBase {
+ private final AnnotationParsingContext parsingContext;
+ private final Parent<KeepEdge> parent;
+ Consumer<KeepEdgeMetaInfo.Builder> addContext;
+ Function<UserBindingsHelper, KeepItemPattern> contextBuilder;
+
+ public UsesReflectionForInstantiationsVisitor(
+ AnnotationParsingContext parsingContext,
+ Parent<KeepEdge> parent,
+ Consumer<KeepEdgeMetaInfo.Builder> addContext,
+ Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+ super(parsingContext);
+ this.parsingContext = parsingContext;
+ this.parent = parent;
+ this.addContext = addContext;
+ this.contextBuilder = contextBuilder;
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String name, String descriptor) {
+ assert name == null;
+ if (AnnotationConstants.UsesReflectionToConstruct.isDescriptor(descriptor)) {
+ return new UsesReflectionToConstructVisitor(
+ parsingContext, parent, addContext, contextBuilder);
+ }
+ return super.visitAnnotation(name, descriptor);
+ }
+ }
+
+ private static class UsesReflectionToConstructVisitor extends AnnotationVisitorBase {
+
+ private final ParsingContext parsingContext;
+ private final Parent<KeepEdge> parent;
+ private final KeepEdge.Builder builder = KeepEdge.builder();
+ private final KeepPreconditions.Builder preconditions = KeepPreconditions.builder();
+ private final KeepEdgeMetaInfo.Builder metaInfoBuilder = KeepEdgeMetaInfo.builder();
+ private KeepMethodParametersPattern parameters = KeepMethodParametersPattern.any();
+ private final UserBindingsHelper bindingsHelper = new UserBindingsHelper();
+
+ private KeepQualifiedClassNamePattern qualifiedName;
+
+ UsesReflectionToConstructVisitor(
+ AnnotationParsingContext parsingContext,
+ Parent<KeepEdge> parent,
+ Consumer<KeepEdgeMetaInfo.Builder> addContext,
+ Function<UserBindingsHelper, KeepItemPattern> contextBuilder) {
+ super(parsingContext);
+ this.parsingContext = parsingContext;
+ this.parent = parent;
+ KeepItemPattern context = contextBuilder.apply(bindingsHelper);
+ KeepBindingReference contextBinding =
+ bindingsHelper.defineFreshItemBinding("CONTEXT", context);
+ preconditions.addCondition(KeepCondition.builder().setItemReference(contextBinding).build());
+ addContext.accept(metaInfoBuilder);
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ if (name.equals(UsesReflectionToConstruct.classConstant) && value instanceof Type) {
+ qualifiedName =
+ KeepQualifiedClassNamePattern.exactFromDescriptor(((Type) value).getDescriptor());
+ return;
+ }
+ if (name.equals(AnnotationConstants.UsesReflectionToConstruct.className)
+ && value instanceof String) {
+ qualifiedName = KeepQualifiedClassNamePattern.exact((String) value);
+ return;
+ }
+ super.visit(name, value);
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String name) {
+ PropertyParsingContext propertyParsingContext = parsingContext.property(name);
+ if (name.equals(UsesReflectionToConstruct.params)) {
+ return new ParametersClassVisitor(
+ propertyParsingContext, parameters -> this.parameters = parameters);
+ }
+ if (name.equals(UsesReflectionToConstruct.paramClassNames)) {
+ return new ParametersClassNamesVisitor(
+ propertyParsingContext, parameters -> this.parameters = parameters);
+ }
+ return super.visitArray(name);
+ }
+
+ @Override
+ public void visitEnd() {
+ KeepClassItemPattern classItemPattern =
+ KeepClassItemPattern.builder()
+ .setClassPattern(
+ KeepClassPattern.builder().setClassNamePattern(qualifiedName).build())
+ .build();
+ KeepClassBindingReference classBinding =
+ bindingsHelper.defineFreshClassBinding(classItemPattern);
+ KeepMemberItemPattern keepMemberItemPattern =
+ KeepMemberItemPattern.builder()
+ .setClassReference(classBinding)
+ .setMemberPattern(
+ KeepMethodPattern.builder()
+ .setNamePattern(KeepMethodNamePattern.instanceInitializer())
+ .setParametersPattern(parameters)
+ .setReturnTypeVoid()
+ .build())
+ .build();
+ KeepMemberBindingReference memberBinding =
+ bindingsHelper.defineFreshMemberBinding(keepMemberItemPattern);
+ builder.setConsequences(
+ KeepConsequences.builder()
+ .addTarget(
+ KeepTarget.builder()
+ .setItemReference(classBinding)
+ .setItemReference(memberBinding)
+ .build())
+ .addTarget(KeepTarget.builder().setItemReference(classBinding).build())
+ .build());
+ parent.accept(
+ builder
+ .setMetaInfo(metaInfoBuilder.build())
+ .setBindings(bindingsHelper.build())
+ .setPreconditions(preconditions.build())
+ .build());
+ }
+ }
+
private static class KeepBindingsVisitor extends AnnotationVisitorBase {
private final ParsingContext parsingContext;
private final UserBindingsHelper helper;
diff --git a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBase.java b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBase.java
index 45b8aa6..515c37d 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBase.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBase.java
@@ -7,13 +7,17 @@
import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.ANDROIDX;
import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.LEGACY;
+import com.android.tools.r8.KotlinCompileMemoizer;
import com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.keepanno.KeepAnnoParameters.KeepAnnoConfig;
import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
public abstract class KeepAnnoTestBase extends TestBase {
@@ -44,4 +48,17 @@
public KeepAnnoTestBuilder testForKeepAnnoAndroidX(KeepAnnoParameters params) throws IOException {
return testForKeepAnno(params, ANDROIDX);
}
+
+ protected static KotlinCompileMemoizer getCompileMemoizerWithKeepAnnoLib(
+ Collection<Path> sources) {
+ assert sources.size() > 0;
+ Path keepAnnoLib;
+ try {
+ keepAnnoLib = KeepAnnoTestUtils.getKeepAnnoLib(getStaticTemp());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return new KotlinCompileMemoizer(sources)
+ .configure(kotlinCompilerTool -> kotlinCompilerTool.addClasspathFiles(keepAnnoLib));
+ }
}
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 b9baff4..1f9fb46 100644
--- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -5,6 +5,9 @@
package com.android.tools.r8.keepanno;
import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.ANDROIDX;
+import static com.android.tools.r8.utils.FileUtils.isClassFile;
+import static com.android.tools.r8.utils.FileUtils.isJarFile;
+import static com.android.tools.r8.utils.FileUtils.isZipFile;
import com.android.tools.r8.ExternalR8TestBuilder;
import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
@@ -33,6 +36,8 @@
import com.android.tools.r8.keepanno.proto.KeepSpecProtos.KeepSpec;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.partial.R8PartialCompilationConfiguration.Builder;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -100,6 +105,9 @@
public abstract KeepAnnoTestBuilder addProgramClassFileData(List<byte[]> programClasses)
throws IOException;
+ public abstract KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+ throws IOException;
+
public final KeepAnnoTestBuilder addKeepMainRule(Class<?> mainClass) {
return applyIfShrinker(b -> b.addKeepMainRule(mainClass));
}
@@ -108,8 +116,16 @@
return applyIfShrinker(b -> b.addKeepClassRules(classes));
}
+ public final KeepAnnoTestBuilder addKeepRules(String... classes) {
+ return applyIfShrinker(b -> b.addKeepRules(classes));
+ }
+
+ public abstract void compile() throws Exception;
+
public abstract SingleTestRunResult<?> run(Class<?> mainClass) throws Exception;
+ public abstract SingleTestRunResult<?> run(String mainClass) throws Exception;
+
public KeepAnnoTestBuilder applyIfShrinker(
ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> builderConsumer) {
applyIfR8(builderConsumer);
@@ -132,6 +148,12 @@
return this;
}
+ public KeepAnnoTestBuilder applyIfR8OrR8Partial(
+ ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> r8BuilderConsumer,
+ ThrowableConsumer<R8PartialTestBuilder> r8PartialBuilderConsumer) {
+ return this;
+ }
+
public KeepAnnoTestBuilder applyIfPG(ThrowableConsumer<ProguardTestBuilder> builderConsumer) {
return this;
}
@@ -213,6 +235,14 @@
}
@Override
+ public KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+ throws IOException {
+ // TODO(b/392865072): Ensure annotations are not processed.
+ builder.addProgramFiles(programFiles);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder inspectOutputRules(Consumer<String> configConsumer) {
// Ignore the consumer.
return this;
@@ -225,9 +255,19 @@
}
@Override
+ public void compile() throws Exception {
+ // Do nothing.
+ }
+
+ @Override
public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
return builder.run(parameters().getRuntime(), mainClass);
}
+
+ @Override
+ public SingleTestRunResult<?> run(String mainClass) throws Exception {
+ return builder.run(parameters().getRuntime(), mainClass);
+ }
}
private abstract static class R8NativeBuilderBase<
@@ -275,9 +315,29 @@
}
@Override
+ public KeepAnnoTestBuilder applyIfR8OrR8Partial(
+ ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> r8BuilderConsumer,
+ ThrowableConsumer<R8PartialTestBuilder> r8PartialBuilderConsumer) {
+ r8BuilderConsumer.acceptWithRuntimeException(builder);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder addProgramFiles(List<Path> programFiles) throws IOException {
for (Path programFile : programFiles) {
- extractAndAdd(Files.readAllBytes(programFile));
+ if (isClassFile(programFile)) {
+ extractAndAdd(Files.readAllBytes(programFile));
+ } else if (isJarFile(programFile) || isZipFile(programFile)) {
+ ZipUtils.iter(
+ programFile,
+ (entry, input) -> {
+ if (isClassFile(entry.getName())) {
+ extractAndAdd(ByteStreams.toByteArray(input));
+ }
+ });
+ } else {
+ assert false : "Unsupported file format";
+ }
}
return this;
}
@@ -331,6 +391,14 @@
}
@Override
+ public KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+ throws IOException {
+ // TODO(b/392865072): Ensure annotations are not processed.
+ builder.addProgramFiles(programFiles);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder inspectOutputRules(Consumer<String> configConsumer) {
compileResultConsumers.add(
result -> configConsumer.accept(result.getProguardConfiguration()));
@@ -344,11 +412,24 @@
}
@Override
+ public void compile() throws Exception {
+ R compileResult = builder.compile();
+ compileResultConsumers.forEach(fn -> fn.accept(compileResult));
+ }
+
+ @Override
public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
R compileResult = builder.compile();
compileResultConsumers.forEach(fn -> fn.accept(compileResult));
return compileResult.run(parameters().getRuntime(), mainClass);
}
+
+ @Override
+ public SingleTestRunResult<?> run(String mainClass) throws Exception {
+ R compileResult = builder.compile();
+ compileResultConsumers.forEach(fn -> fn.accept(compileResult));
+ return compileResult.run(parameters().getRuntime(), mainClass);
+ }
}
private static class R8NativeBuilder
@@ -410,6 +491,14 @@
}
@Override
+ public KeepAnnoTestBuilder applyIfR8OrR8Partial(
+ ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> r8BuilderConsumer,
+ ThrowableConsumer<R8PartialTestBuilder> r8PartialBuilderConsumer) {
+ r8PartialBuilderConsumer.acceptWithRuntimeException(builder);
+ return this;
+ }
+
+ @Override
boolean isExtractRules() {
return config == KeepAnnoConfig.R8_PARTIAL_RULES;
}
@@ -455,6 +544,14 @@
}
@Override
+ public KeepAnnoTestBuilder applyIfR8OrR8Partial(
+ ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> r8BuilderConsumer,
+ ThrowableConsumer<R8PartialTestBuilder> r8PartialBuilderConsumer) {
+ r8BuilderConsumer.acceptWithRuntimeException(builder);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder addProgramFiles(List<Path> programFiles) throws IOException {
List<String> rules = KeepAnnoTestUtils.extractRulesFromFiles(programFiles, extractorOptions);
builder.addProgramFiles(programFiles);
@@ -484,6 +581,14 @@
}
@Override
+ public KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+ throws IOException {
+ // TODO(b/392865072): Ensure annotations are not processed.
+ builder.addProgramFiles(programFiles);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder inspectOutputRules(Consumer<String> configConsumer) {
configConsumers.add(lines -> configConsumer.accept(String.join("\n", lines)));
return this;
@@ -495,11 +600,23 @@
}
@Override
+ public void compile() throws Exception {
+ builder.compile();
+ }
+
+ @Override
public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
configConsumers.forEach(fn -> fn.accept(builder.getConfig()));
extractedRulesConsumers.forEach(fn -> fn.accept(extractedRules));
return builder.run(parameters().getRuntime(), mainClass);
}
+
+ @Override
+ public SingleTestRunResult<?> run(String mainClass) throws Exception {
+ configConsumers.forEach(fn -> fn.accept(builder.getConfig()));
+ extractedRulesConsumers.forEach(fn -> fn.accept(extractedRules));
+ return builder.run(parameters().getRuntime(), mainClass);
+ }
}
private static class PGBuilder extends KeepAnnoTestBuilder {
@@ -521,11 +638,7 @@
builder =
TestBase.testForProguard(KeepAnnoTestUtils.PG_VERSION, temp)
.applyIf(
- keepAnnotationLibrary == ANDROIDX,
- b ->
- b.addDefaultRuntimeLibrary(parameters())
- .addLibraryFiles(
- kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar()))
+ keepAnnotationLibrary == ANDROIDX, b -> b.addDefaultRuntimeLibrary(parameters()))
.addProgramFiles(KeepAnnoTestUtils.getKeepAnnoLib(temp, keepAnnotationLibrary))
.setMinApi(parameters());
}
@@ -566,6 +679,14 @@
}
@Override
+ public KeepAnnoTestBuilder addProgramFilesWithoutAnnotations(List<Path> programFiles)
+ throws IOException {
+ // TODO(b/392865072): Ensure annotations are not processed.
+ builder.addProgramFiles(programFiles);
+ return this;
+ }
+
+ @Override
public KeepAnnoTestBuilder inspectOutputRules(Consumer<String> configConsumer) {
configConsumers.add(lines -> configConsumer.accept(String.join("\n", lines)));
return this;
@@ -577,10 +698,22 @@
}
@Override
+ public void compile() throws Exception {
+ builder.compile();
+ }
+
+ @Override
public SingleTestRunResult<?> run(Class<?> mainClass) throws Exception {
configConsumers.forEach(fn -> fn.accept(builder.getConfig()));
extractedRulesConsumers.forEach(fn -> fn.accept(extractedRules));
return builder.run(parameters().getRuntime(), mainClass);
}
+
+ @Override
+ public SingleTestRunResult<?> run(String mainClass) throws Exception {
+ configConsumers.forEach(fn -> fn.accept(builder.getConfig()));
+ extractedRulesConsumers.forEach(fn -> fn.accept(extractedRules));
+ return builder.run(parameters().getRuntime(), mainClass);
+ }
}
}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
new file mode 100644
index 0000000..c652577
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
@@ -0,0 +1,201 @@
+// 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.keepanno.androidx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.KotlinCompileMemoizer;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
+import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.Version;
+import com.android.tools.r8.keepanno.KeepAnnoParameters;
+import com.android.tools.r8.keepanno.KeepAnnoTestBase;
+import com.android.tools.r8.utils.SemanticVersion;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.runners.Parameterized.Parameter;
+
+public abstract class KeepAnnoTestExtractedRulesBase extends KeepAnnoTestBase {
+ @Parameter(0)
+ public KeepAnnoParameters parameters;
+
+ @Parameter(1)
+ public KotlinTestParameters kotlinParameters;
+
+ protected static KotlinCompileMemoizer compilationResults;
+ protected static KotlinCompileMemoizer compilationResultsClassName;
+
+ protected abstract String getExpectedOutput();
+
+ protected static List<String> trimRules(List<String> rules) {
+ List<String> trimmedRules =
+ rules.stream()
+ .flatMap(s -> Arrays.stream(s.split("\n")))
+ .filter(rule -> !rule.startsWith("#"))
+ .collect(Collectors.toList());
+ return trimmedRules;
+ }
+
+ public static class ExpectedRule {
+ private final String conditionClass;
+ private final String conditionMembers;
+ private final String consequentClass;
+ private final String consequentMembers;
+
+ private ExpectedRule(Builder builder) {
+ this.conditionClass = builder.conditionClass;
+ this.conditionMembers = builder.conditionMembers;
+ this.consequentClass = builder.consequentClass;
+ this.consequentMembers = builder.consequentMembers;
+ }
+
+ public String getRule(boolean r8) {
+ return "-if class "
+ + conditionClass
+ + " "
+ + conditionMembers
+ + " -keepclasseswithmembers"
+ + (r8 ? ",allowaccessmodification" : "")
+ + " class "
+ + consequentClass
+ + " "
+ + consequentMembers;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String conditionClass;
+ private String conditionMembers;
+ private String consequentClass;
+ private String consequentMembers;
+
+ private Builder() {}
+
+ public Builder setConditionClass(Class<?> conditionClass) {
+ this.conditionClass = conditionClass.getTypeName();
+ return this;
+ }
+
+ public Builder setConditionClass(String conditionClass) {
+ this.conditionClass = conditionClass;
+ return this;
+ }
+
+ public Builder setConditionMembers(String conditionMembers) {
+ this.conditionMembers = conditionMembers;
+ return this;
+ }
+
+ public Builder setConsequentClass(Class<?> consequentClass) {
+ this.consequentClass = consequentClass.getTypeName();
+ return this;
+ }
+
+ public Builder setConsequentClass(String consequentClass) {
+ this.consequentClass = consequentClass;
+ return this;
+ }
+
+ public Builder setConsequentMembers(String consequentMembers) {
+ this.consequentMembers = consequentMembers;
+ return this;
+ }
+
+ public ExpectedRule build() {
+ return new ExpectedRule(this);
+ }
+ }
+ }
+
+ protected void runTestExtractedRulesJava(List<Class<?>> testClasses, ExpectedRule expectedRule)
+ throws Exception {
+ Class<?> mainClass = testClasses.iterator().next();
+ testForKeepAnnoAndroidX(parameters)
+ .applyIfPG(
+ b -> {
+ KotlinCompiler kotlinc =
+ new KotlinCompiler(KotlinCompilerVersion.MAX_SUPPORTED_VERSION);
+ b.addLibraryFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar());
+ })
+ .addProgramClasses(testClasses)
+ .addKeepMainRule(mainClass)
+ .setExcludedOuterClass(getClass())
+ .inspectExtractedRules(
+ rules -> {
+ if (parameters.isExtractRules()) {
+ assertEquals(
+ ImmutableList.of(expectedRule.getRule(parameters.isR8())), trimRules(rules));
+ }
+ })
+ .run(mainClass)
+ .assertSuccessWithOutput(getExpectedOutput());
+ }
+
+ protected void runTestExtractedRulesKotlin(
+ KotlinCompileMemoizer compilation, String mainClass, ExpectedRule expectedRule)
+ throws Exception {
+ // TODO(b/392865072): Legacy R8 fails with AssertionError: Synthetic class kinds should agree.
+ assumeFalse(parameters.isLegacyR8());
+ // TODO(b/392865072): Reference fails with AssertionError: Built-in class kotlin.Any is not
+ // found (in kotlin.reflect code).
+ assumeFalse(parameters.isReference());
+ // TODO(b/392865072): Proguard 7.4.1 fails with "Encountered corrupt @kotlin/Metadata for class
+ // <binary name> (version 2.1.0)".
+ assumeFalse(parameters.isPG());
+ testForKeepAnnoAndroidX(parameters)
+ .addProgramFiles(ImmutableList.of(compilation.getForConfiguration(kotlinParameters)))
+ .addProgramFilesWithoutAnnotations(
+ ImmutableList.of(
+ kotlinParameters.getCompiler().getKotlinStdlibJar(),
+ kotlinParameters.getCompiler().getKotlinReflectJar(),
+ kotlinParameters.getCompiler().getKotlinAnnotationJar()))
+ .applyIfR8(
+ b ->
+ b.applyIf(
+ b instanceof R8TestBuilder && Version.isMainVersion(),
+ bb ->
+ ((R8TestBuilder<?, ?, ?>) bb)
+ .setFakeCompilerVersion(SemanticVersion.max())
+ .allowDiagnosticMessages()))
+ .addKeepRules("-keepattributes RuntimeVisibleAnnotations")
+ .addKeepRules("-keep class kotlin.Metadata { *; }")
+ .addKeepRules(
+ "-keep class " + mainClass + " { public static void main(java.lang.String[]); }")
+ .applyIfR8OrR8Partial(
+ b ->
+ b.addOptionsModification(
+ options -> {
+ options.testing.allowedUnusedDontWarnPatterns.add(
+ "kotlin.reflect.jvm.internal.**");
+ options.testing.allowedUnusedDontWarnPatterns.add("java.lang.ClassValue");
+ }),
+ b ->
+ b.addR8PartialR8OptionsModification(
+ options -> {
+ options.testing.allowedUnusedDontWarnPatterns.add(
+ "kotlin.reflect.jvm.internal.**");
+ options.testing.allowedUnusedDontWarnPatterns.add("java.lang.ClassValue");
+ }))
+ .inspectExtractedRules(
+ rules -> {
+ if (parameters.isExtractRules()) {
+ assertEquals(
+ ImmutableList.of(expectedRule.getRule(parameters.isR8())), trimRules(rules));
+ }
+ })
+ .run(mainClass)
+ .applyIf(
+ parameters.isExtractRules(),
+ b -> b.assertSuccessWithOutput(getExpectedOutput()),
+ b -> b.assertSuccessWithOutput(""));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationNoArgsConstructorTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationNoArgsConstructorTest.java
new file mode 100644
index 0000000..dfa7b78
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationNoArgsConstructorTest.java
@@ -0,0 +1,166 @@
+// 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.keepanno.androidx;
+
+import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+import static org.junit.Assert.assertEquals;
+
+import androidx.annotation.keep.UsesReflectionToConstruct;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepUsesReflectionForInstantiationNoArgsConstructorTest
+ extends KeepAnnoTestExtractedRulesBase {
+
+ // String constant to be references from annotations.
+ static final String classNameOfKeptClass =
+ "com.android.tools.r8.keepanno.androidx.KeepUsesReflectionForInstantiationNoArgsConstructorTest$KeptClass";
+
+ @Parameterized.Parameters(name = "{0}, {1}")
+ public static Collection<Object[]> data() {
+ assertEquals(KeptClass.class.getTypeName(), classNameOfKeptClass);
+ return buildParameters(
+ createParameters(getTestParameters().withDefaultRuntimes().withMaximumApiLevel().build()),
+ getKotlinTestParameters().withLatestCompiler().build());
+ }
+
+ protected String getExpectedOutput() {
+ return StringUtils.lines("<init>()");
+ }
+
+ private static Collection<Path> getKotlinSources() {
+ try {
+ return getFilesInTestFolderRelativeToClass(
+ KeepUsesReflectionForInstantiationNoArgsConstructorTest.class,
+ "kt",
+ "OnlyNoArgsConstructor.kt",
+ "KeptClass.kt");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Collection<Path> getKotlinSourcesClassName() {
+ try {
+ return getFilesInTestFolderRelativeToClass(
+ KeepUsesReflectionForInstantiationNoArgsConstructorTest.class,
+ "kt",
+ "OnlyNoArgsConstructorClassName.kt",
+ "KeptClass.kt");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources());
+ compilationResultsClassName = getCompileMemoizerWithKeepAnnoLib(getKotlinSourcesClassName());
+ }
+
+ @Test
+ public void testOnlyNoArgsConstructor() throws Exception {
+ runTestExtractedRulesJava(
+ ImmutableList.of(OnlyNoArgsConstructor.class, KeptClass.class),
+ ExpectedRule.builder()
+ .setConditionClass(OnlyNoArgsConstructor.class)
+ .setConditionMembers("{ void foo(java.lang.Class); }")
+ .setConsequentClass(KeptClass.class)
+ .setConsequentMembers("{ void <init>(); }")
+ .build());
+ }
+
+ static class OnlyNoArgsConstructor {
+
+ @UsesReflectionToConstruct(
+ className = classNameOfKeptClass,
+ params = {})
+ public void foo(Class<KeptClass> clazz) throws Exception {
+ if (clazz != null) {
+ clazz.getDeclaredConstructor().newInstance();
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ new OnlyNoArgsConstructor().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+ }
+ }
+
+ @Test
+ public void testOnlyNoArgsConstructorClassNames() throws Exception {
+ runTestExtractedRulesJava(
+ ImmutableList.of(OnlyNoArgsConstructorClassNames.class, KeptClass.class),
+ ExpectedRule.builder()
+ .setConditionClass(OnlyNoArgsConstructorClassNames.class)
+ .setConditionMembers("{ void foo(java.lang.Class); }")
+ .setConsequentClass(KeptClass.class)
+ .setConsequentMembers("{ void <init>(); }")
+ .build());
+ }
+
+ static class OnlyNoArgsConstructorClassNames {
+
+ @UsesReflectionToConstruct(
+ className = classNameOfKeptClass,
+ params = {})
+ public void foo(Class<KeptClass> clazz) throws Exception {
+ if (clazz != null) {
+ clazz.getDeclaredConstructor().newInstance();
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ new OnlyNoArgsConstructorClassNames().foo(System.nanoTime() > 0 ? KeptClass.class : null);
+ }
+ }
+
+ @Test
+ public void testOnlyNoArgsConstructorKotlin() throws Exception {
+ runTestExtractedRulesKotlin(
+ compilationResults,
+ "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorKt",
+ ExpectedRule.builder()
+ .setConditionClass("com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructor")
+ .setConditionMembers("{ void foo(kotlin.reflect.KClass); }")
+ .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass")
+ .setConsequentMembers("{ void <init>(); }")
+ .build());
+ }
+
+ @Test
+ public void testOnlyNoArgsConstructorKotlinClassName() throws Exception {
+ runTestExtractedRulesKotlin(
+ compilationResultsClassName,
+ "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorClassNameKt",
+ ExpectedRule.builder()
+ .setConditionClass(
+ "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorClassName")
+ .setConditionMembers("{ void foo(kotlin.reflect.KClass); }")
+ .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass")
+ .setConsequentMembers("{ void <init>(); }")
+ .build());
+ }
+
+ static class KeptClass {
+ KeptClass() {
+ System.out.println("<init>()");
+ }
+
+ KeptClass(int i) {
+ System.out.println("<init>(int)");
+ }
+
+ KeptClass(long j) {
+ System.out.println("<init>(long)");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
new file mode 100644
index 0000000..fb55316
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/KeptClass.kt
@@ -0,0 +1,15 @@
+package com.android.tools.r8.keepanno.androidx.kt
+
+class KeptClass() {
+ init {
+ println("<init>()")
+ }
+
+ constructor(i: Int) : this() {
+ println("<init>(Int)")
+ }
+
+ constructor(l: Long) : this() {
+ println("<init>(Long)")
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructor.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructor.kt
new file mode 100644
index 0000000..4dd03cc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructor.kt
@@ -0,0 +1,16 @@
+package com.android.tools.r8.keepanno.androidx.kt
+
+import androidx.annotation.keep.UsesReflectionToConstruct
+import kotlin.reflect.KClass
+import kotlin.reflect.full.primaryConstructor
+
+class OnlyNoArgsConstructor {
+ @UsesReflectionToConstruct(classConstant = KeptClass::class, params = [])
+ fun foo(clazz: KClass<KeptClass>?) {
+ clazz?.primaryConstructor?.call()
+ }
+}
+
+fun main() {
+ OnlyNoArgsConstructor().foo(if (System.nanoTime() > 0) KeptClass::class else null)
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorClassName.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorClassName.kt
new file mode 100644
index 0000000..8652223
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorClassName.kt
@@ -0,0 +1,19 @@
+package com.android.tools.r8.keepanno.androidx.kt
+
+import androidx.annotation.keep.UsesReflectionToConstruct
+import kotlin.reflect.KClass
+import kotlin.reflect.full.primaryConstructor
+
+class OnlyNoArgsConstructorClassName {
+ @UsesReflectionToConstruct(
+ className = "com.android.tools.r8.keepanno.androidx.kt.KeptClass",
+ params = [],
+ )
+ fun foo(clazz: KClass<KeptClass>?) {
+ clazz?.primaryConstructor?.call()
+ }
+}
+
+fun main() {
+ OnlyNoArgsConstructorClassName().foo(if (System.nanoTime() > 0) KeptClass::class else null)
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
index c7a31d0..366f7b2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
@@ -7,6 +7,8 @@
import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static junit.framework.TestCase.assertNotNull;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
@@ -174,7 +176,9 @@
public static void verifyExpectedWarningsFromKotlinReflectAndStdLib(
TestCompileResult<?, ?> compileResult) {
compileResult.assertAllWarningMessagesMatch(
- equalTo("Resource 'META-INF/versions/9/module-info.class' already exists."));
+ anyOf(
+ equalTo("Resource 'META-INF/versions/9/module-info.class' already exists."),
+ containsString("-upto-")));
}
protected String unresolvedReferenceMessage(KotlinTestParameters param, String ref) {
diff --git a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
index a90871d..547024a 100644
--- a/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/testbase/java/com/android/tools/r8/ToolHelper.java
@@ -2712,12 +2712,16 @@
}
public static Collection<Path> getFilesInTestFolderRelativeToClass(
- Class<?> clazz, String folderName, String endsWith) throws IOException {
+ Class<?> clazz, String folderName, String... endsWith) throws IOException {
Path subFolder = getTestFolderForClass(clazz).resolve(folderName);
assert Files.isDirectory(subFolder);
- try (Stream<Path> walker = Files.walk(subFolder)) {
- return walker.filter(path -> path.toString().endsWith(endsWith)).collect(Collectors.toList());
+ List<Path> result = new ArrayList<>();
+ for (String s : endsWith) {
+ try (Stream<Path> walker = Files.walk(subFolder)) {
+ walker.filter(path -> path.toString().endsWith(s)).forEach(result::add);
+ }
}
+ return result;
}
/** This code only works if run with depot_tools on the path */
diff --git a/src/test/testbase/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java b/src/test/testbase/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
index 53f0924..36968f6 100644
--- a/src/test/testbase/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
+++ b/src/test/testbase/java/com/android/tools/r8/keepanno/KeepAnnoTestUtils.java
@@ -5,6 +5,10 @@
package com.android.tools.r8.keepanno;
import static com.android.tools.r8.R8TestBuilder.KeepAnnotationLibrary.ANDROIDX;
+import static com.android.tools.r8.utils.FileUtils.isClassFile;
+import static com.android.tools.r8.utils.FileUtils.isJarFile;
+import static com.android.tools.r8.utils.FileUtils.isZipFile;
+import static com.android.tools.r8.utils.ZipUtils.isClassFile;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.ByteDataView;
@@ -18,6 +22,9 @@
import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractorOptions;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -53,7 +60,7 @@
try (Stream<Path> paths = Files.list(annoDir)) {
paths.forEach(
p -> {
- if (FileUtils.isClassFile(p)) {
+ if (isClassFile(p)) {
byte[] data = FileUtils.uncheckedReadAllBytes(p);
String fileName = p.getFileName().toString();
String className = fileName.substring(0, fileName.lastIndexOf('.'));
@@ -74,17 +81,31 @@
public static List<String> extractRulesFromFiles(
List<Path> inputFiles, KeepRuleExtractorOptions extractorOptions) {
- return extractRulesFromBytes(
- ListUtils.map(
- inputFiles,
- path -> {
- try {
- return Files.readAllBytes(path);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }),
- extractorOptions);
+ List<String> result = new ArrayList<>();
+ for (Path inputFile : inputFiles) {
+ try {
+ if (isClassFile(inputFile)) {
+ result.addAll(
+ extractRulesFromBytes(
+ ImmutableList.of(Files.readAllBytes(inputFile)), extractorOptions));
+ } else if (isJarFile(inputFile) || isZipFile(inputFile)) {
+ List<byte[]> classFilesFromArchive = new ArrayList<>();
+ ZipUtils.iter(
+ inputFile,
+ (entry, input) -> {
+ if (ZipUtils.isClassFile(entry.getName())) {
+ classFilesFromArchive.add(ByteStreams.toByteArray(input));
+ }
+ });
+ result.addAll(extractRulesFromBytes(classFilesFromArchive, extractorOptions));
+ } else {
+ assert false : "Unsupported file format";
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return result;
}
public static List<String> extractRules(