blob: c5aa87a53e1b0698dca78451a9d670cf0dbfa03a [file] [log] [blame]
// 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.utils.FileUtils.isClassFile;
import static org.junit.Assume.assumeFalse;
import com.android.tools.r8.DataEntryResource;
import com.android.tools.r8.DataResourceProvider;
import com.android.tools.r8.KotlinCompileMemoizer;
import com.android.tools.r8.KotlinTestParameters;
import com.android.tools.r8.ProgramResource;
import com.android.tools.r8.ProgramResourceProvider;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.keepanno.KeepAnno;
import com.android.tools.r8.keepanno.KeepAnnoParameters;
import com.android.tools.r8.keepanno.KeepAnnoTestBase;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.references.ClassReference;
import com.android.tools.r8.shaking.ProguardKeepAttributes;
import com.android.tools.r8.transformers.ClassFileTransformer;
import com.android.tools.r8.transformers.ClassFileTransformer.AnnotationBuilder;
import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.junit.runners.Parameterized.Parameter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
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 String getExpectedOutput() {
assert false
: "implement either this or both of getExpectedOutputForJava and"
+ " getExpectedOutputForKotlin";
return null;
}
protected String getExpectedOutputForJava() {
return getExpectedOutput();
}
protected String getExpectedOutputForKotlin() {
return 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("#"))
.sorted()
.distinct()
.collect(Collectors.toList());
return trimmedRules;
}
protected static byte[] setAnnotationOnClass(
ClassFileTransformer transformer,
Consumer<AnnotationBuilder> builderConsumer) {
return transformer.setAnnotation(builderConsumer).transform();
}
protected static byte[] setAnnotationOnClass(
Class<?> clazz, Consumer<AnnotationBuilder> builderConsumer) throws IOException {
return setAnnotationOnClass(transformer(clazz), builderConsumer);
}
protected static byte[] setAnnotationOnClass(
ClassReference classReference,
byte[] classFileBytes,
ClassReference classReferenceToTransform,
Consumer<AnnotationBuilder> builderConsumer) {
if (!classReference.equals(classReferenceToTransform)) {
return classFileBytes;
}
return setAnnotationOnClass(transformer(classFileBytes, classReference), builderConsumer);
}
protected static byte[] setAnnotationOnMethod(
ClassFileTransformer transformer,
MethodPredicate methodPredicate,
Consumer<AnnotationBuilder> builderConsumer) {
return transformer.setAnnotation(methodPredicate, builderConsumer).transform();
}
protected static byte[] setAnnotationOnMethod(
Class<?> clazz, MethodPredicate methodPredicate, Consumer<AnnotationBuilder> builderConsumer)
throws IOException {
return setAnnotationOnMethod(transformer(clazz), methodPredicate, builderConsumer);
}
protected static byte[] setAnnotationOnMethod(
ClassReference classReference,
byte[] classFileBytes,
ClassReference classReferenceToTransform,
MethodPredicate methodPredicate,
Consumer<AnnotationBuilder> builderConsumer) {
if (!classReference.equals(classReferenceToTransform)) {
return classFileBytes;
}
return setAnnotationOnMethod(
transformer(classFileBytes, classReference),
methodPredicate,
builderConsumer);
}
public abstract static class ExpectedRule {
public abstract String getRule(boolean r8);
}
public static class ExpectedKeepRule extends ExpectedRule {
private final String keepVariant;
private final String conditionClass;
private final String conditionMembers;
private final String consequentClass;
private final String consequentMembers;
private ExpectedKeepRule(Builder builder) {
this.keepVariant = builder.keepVariant;
this.conditionClass = builder.conditionClass;
this.conditionMembers = builder.conditionMembers;
this.consequentClass = builder.consequentClass;
this.consequentMembers = builder.consequentMembers;
}
@Override
public String getRule(boolean r8) {
return "-if class "
+ conditionClass
+ (conditionMembers != null ? " " + conditionMembers : "")
+ " "
+ keepVariant
+ (r8 ? ",allowaccessmodification" : "")
+ " class "
+ consequentClass
+ " "
+ consequentMembers;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String keepVariant = "-keepclasseswithmembers";
private String conditionClass;
private String conditionMembers;
private String consequentClass;
private String consequentMembers;
private Builder() {}
public Builder setKeepVariant(String keepVariant) {
this.keepVariant = keepVariant;
return this;
}
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 Builder apply(Consumer<Builder> fn) {
fn.accept(this);
return this;
}
public ExpectedKeepRule build() {
return new ExpectedKeepRule(this);
}
}
}
public static class ExpectedKeepAttributesRule extends ExpectedRule {
private final boolean runtimeVisibleAnnotations;
private final boolean runtimeVisibleParameterAnnotations;
private final boolean runtimeVisibleTypeAnnotations;
private ExpectedKeepAttributesRule(Builder builder) {
this.runtimeVisibleAnnotations = builder.runtimeVisibleAnnotations;
this.runtimeVisibleParameterAnnotations = builder.runtimeVisibleParameterAnnotations;
this.runtimeVisibleTypeAnnotations = builder.runtimeVisibleTypeAnnotations;
}
@Override
public String getRule(boolean r8) {
List<String> attributes = new ArrayList<>();
if (runtimeVisibleAnnotations) {
attributes.add(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS);
}
if (runtimeVisibleParameterAnnotations) {
attributes.add(ProguardKeepAttributes.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS);
}
if (runtimeVisibleTypeAnnotations) {
attributes.add(ProguardKeepAttributes.RUNTIME_VISIBLE_TYPE_ANNOTATIONS);
}
return "-keepattributes " + String.join(",", attributes);
}
public static Builder builder() {
return new Builder();
}
public static ExpectedKeepAttributesRule buildAllRuntimeVisibleAnnotations() {
return builder()
.setRuntimeVisibleAnnotations()
.setRuntimeVisibleParameterAnnotations()
.setRuntimeVisibleTypeAnnotations()
.build();
}
public static class Builder {
private boolean runtimeVisibleAnnotations = false;
private boolean runtimeVisibleParameterAnnotations = false;
private boolean runtimeVisibleTypeAnnotations = false;
public Builder setRuntimeVisibleAnnotations() {
this.runtimeVisibleAnnotations = true;
return this;
}
public Builder setRuntimeVisibleParameterAnnotations() {
this.runtimeVisibleParameterAnnotations = true;
return this;
}
public Builder setRuntimeVisibleTypeAnnotations() {
this.runtimeVisibleTypeAnnotations = true;
return this;
}
public ExpectedKeepAttributesRule build() {
return new ExpectedKeepAttributesRule(this);
}
}
}
public static class ExpectedRules {
private final ImmutableList<ExpectedRule> rules;
private ExpectedRules(Builder builder) {
this.rules = builder.rules.build();
}
public ImmutableList<String> getRules(boolean r8) {
return rules.stream()
.map(rule -> rule.getRule(r8))
.sorted()
.collect(ImmutableList.toImmutableList());
}
public static Builder builder() {
return new Builder();
}
public static ExpectedRules singleRule(ExpectedRule rule) {
return new Builder().add(rule).build();
}
public static class Builder {
private final ImmutableList.Builder<ExpectedRule> rules = ImmutableList.builder();
public Builder add(ExpectedRule rule) {
rules.add(rule);
return this;
}
public Builder apply(Consumer<Builder> fn) {
fn.accept(this);
return this;
}
public ExpectedRules build() {
return new ExpectedRules(this);
}
}
}
// Add the expected rules for kotlin.Metadata (the class with members and the required
// attributes).
protected static void addConsequentKotlinMetadata(
ExpectedRules.Builder b, Consumer<ExpectedKeepRule.Builder> fn) {
b.add(ExpectedKeepAttributesRule.buildAllRuntimeVisibleAnnotations())
.add(
ExpectedKeepRule.builder()
.setKeepVariant("-keep")
.setConsequentClass("kotlin.Metadata")
.setConsequentMembers("{ *; }")
.apply(fn)
.build());
}
static class ArchiveResourceProviderClassFilesOnly
implements ProgramResourceProvider, DataResourceProvider {
private final List<ProgramResource> programResources = new ArrayList<>();
private final List<DataEntryResource> dataResources = new ArrayList<>();
ArchiveResourceProviderClassFilesOnly(Path path) throws ResourceException {
try {
ZipUtils.iter(
path,
(entry, inputStream) -> {
if (ZipUtils.isClassFile(entry.getName())) {
programResources.add(
ProgramResource.fromBytes(
Origin.unknown(),
ProgramResource.Kind.CF,
ByteStreams.toByteArray(inputStream),
Collections.singleton(
DescriptorUtils.guessTypeDescriptor(entry.getName()))));
} else if (FileUtils.isKotlinModuleFile(entry.getName())) {
dataResources.add(
DataEntryResource.fromBytes(
ByteStreams.toByteArray(inputStream), entry.getName(), Origin.unknown()));
} else if (FileUtils.isKotlinBuiltinsFile(entry.getName())) {
dataResources.add(
DataEntryResource.fromBytes(
ByteStreams.toByteArray(inputStream), entry.getName(), Origin.unknown()));
}
});
} catch (IOException e) {
throw new ResourceException(Origin.unknown(), "Caught IOException", e);
}
}
@Override
public Collection<ProgramResource> getProgramResources() throws ResourceException {
return programResources;
}
@Override
public DataResourceProvider getDataResourceProvider() {
return this;
}
@Override
public void accept(Visitor visitor) throws ResourceException {
dataResources.forEach(visitor::visit);
}
}
private void extractRules(Class<?> mainClass, Consumer<String> rulesConsumer) throws IOException {
extractRules(Files.readAllBytes(ToolHelper.getClassFileForTestClass(mainClass)), rulesConsumer);
}
private void extractRules(byte[] classFileData, Consumer<String> rulesConsumer) {
ClassReader reader = new ClassReader(classFileData);
ClassVisitor visitor = KeepAnno.createClassVisitorForKeepRulesExtraction(rulesConsumer);
reader.accept(visitor, ClassReader.SKIP_CODE);
}
protected void testExtractedRules(
Iterable<Class<?>> classes, Iterable<byte[]> classFileData, ExpectedRules expectedRules)
throws IOException {
if (parameters.isExtractRules()) {
List<String> extractedRules = new ArrayList<>();
for (Class<?> testClass : classes) {
extractRules(testClass, extractedRules::add);
}
for (byte[] data : classFileData) {
extractRules(data, extractedRules::add);
}
assertListsAreEqual(expectedRules.getRules(parameters.isR8()), trimRules(extractedRules));
}
}
protected void testExtractedRules(Iterable<byte[]> classFileData, ExpectedRules expectedRules)
throws IOException {
testExtractedRules(Collections.emptyList(), classFileData, expectedRules);
}
protected void testExtractedRules(
KotlinCompileMemoizer compilation, List<byte[]> classFileData, ExpectedRules expectedRules)
throws IOException {
List<byte[]> kotlinCompilationClassFileData = new ArrayList<>();
if (compilation != null) {
ZipUtils.iter(
compilation.getForConfiguration(kotlinParameters),
(entry, input) -> {
if (isClassFile(entry.getName())) {
kotlinCompilationClassFileData.add(ByteStreams.toByteArray(input));
}
});
}
testExtractedRules(
Iterables.concat(kotlinCompilationClassFileData, classFileData), expectedRules);
}
protected void testExtractedRules(
KotlinCompileMemoizer compilation,
BiFunction<ClassReference, byte[], byte[]> transformerForClass,
ExpectedRules expectedRules)
throws IOException {
testExtractedRules(getTransformedClasses(compilation, transformerForClass), expectedRules);
}
protected void testExtractedRulesAndRunJava(
Class<?> mainClass,
List<Class<?>> classes,
List<byte[]> classFileData,
ExpectedRules expectedRules)
throws Exception {
// TODO(b/392865072): Proguard 7.4.1 fails with "Encountered corrupt @kotlin/Metadata for class
// <binary name> (version 2.1.0)", as ti avoid missing classes warnings from ProGuard some of
// the Kotlin libraries has to be included.
assumeFalse(parameters.isPG());
testExtractedRules(
Iterables.concat(ImmutableList.of(mainClass), classes), classFileData, expectedRules);
testForKeepAnnoAndroidX(parameters)
.addProgramClasses(classes)
.addProgramClassFileData(classFileData)
.addKeepMainRule(mainClass)
.setExcludedOuterClass(getClass())
.inspectExtractedRules(
rules -> {
if (parameters.isExtractRules()) {
assertListsAreEqual(expectedRules.getRules(parameters.isR8()), trimRules(rules));
}
})
.run(mainClass)
.assertSuccessWithOutput(getExpectedOutputForJava());
}
protected void testExtractedRulesAndRunJava(List<Class<?>> classes, ExpectedRules expectedRules)
throws Exception {
testExtractedRulesAndRunJava(
classes.iterator().next(), classes, ImmutableList.of(), expectedRules);
}
private List<byte[]> getTransformedClasses(
KotlinCompileMemoizer compilation,
BiFunction<ClassReference, byte[], byte[]> transformerForClass)
throws IOException {
List<byte[]> result = new ArrayList<>();
ZipUtils.iter(
compilation.getForConfiguration(kotlinParameters),
(entry, inputStream) -> {
ClassReference classReference = ZipUtils.entryToClassReference(entry);
if (classReference == null) {
return;
}
result.add(
transformerForClass.apply(classReference, ByteStreams.toByteArray(inputStream)));
});
return result;
}
protected void testExtractedRulesAndRunKotlin(
KotlinCompileMemoizer compilation,
List<byte[]> classFileData,
String mainClass,
ExpectedRules expectedRules,
String expectedOutput)
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.7.0 compiled code fails with fails with
// "java.lang.annotation.IncompleteAnnotationException: kotlin.Metadata missing element bv".
assumeFalse(parameters.isPG());
testExtractedRules(compilation, classFileData, expectedRules);
testForKeepAnnoAndroidX(parameters)
.applyIf(
compilation != null,
b ->
b.addProgramFiles(
ImmutableList.of(compilation.getForConfiguration(kotlinParameters))))
.applyIfPG(
b ->
b.addProgramFiles(
ImmutableList.of(
kotlinParameters.getCompiler().getKotlinStdlibJar(),
kotlinParameters.getCompiler().getKotlinReflectJar(),
kotlinParameters.getCompiler().getKotlinAnnotationJar()))
.addKeepEnumsRule())
// addProgramResourceProviders is not implemented for ProGuard, so effectively exclusive
// from addProgramFiles above. If this changed ProGuard will report duplicate classes.
.addProgramResourceProviders(
// Only add class files from Kotlin libraries to ensure the keep annotations does not
// rely on rules like `-keepattributes RuntimeVisibleAnnotations` and
// `-keep class kotlin.Metadata { *; }` but are self-contained.
new ArchiveResourceProviderClassFilesOnly(
kotlinParameters.getCompiler().getKotlinStdlibJar()),
new ArchiveResourceProviderClassFilesOnly(
kotlinParameters.getCompiler().getKotlinReflectJar()),
new ArchiveResourceProviderClassFilesOnly(
kotlinParameters.getCompiler().getKotlinAnnotationJar()))
.addProgramClassFileData(classFileData)
// TODO(b/323816623): With native interpretation kotlin.Metadata still gets stripped
// without -keepattributes RuntimeVisibleAnnotations`.
.applyIfR8(
b ->
b.applyIf(
parameters.isNativeR8(),
bb -> bb.addKeepRules("-keepattributes RuntimeVisibleAnnotations")))
// Keep the main entry point for the test.
.addKeepRules(
"-keep class " + mainClass + " { public static void main(java.lang.String[]); }")
.inspectExtractedRules(
rules -> {
if (parameters.isExtractRules()) {
assertListsAreEqual(expectedRules.getRules(parameters.isR8()), trimRules(rules));
}
})
.run(mainClass)
.assertSuccessWithOutput(expectedOutput);
}
protected void testExtractedRulesAndRunKotlin(
KotlinCompileMemoizer compilation, String mainClass, ExpectedRules expectedRules)
throws Exception {
testExtractedRulesAndRunKotlin(
compilation, ImmutableList.of(), mainClass, expectedRules, getExpectedOutputForKotlin());
}
protected void testExtractedRulesAndRunKotlin(
KotlinCompileMemoizer compilation,
BiFunction<ClassReference, byte[], byte[]> transformerForClass,
String mainClass,
ExpectedRules expectedRules,
String expectedOutput)
throws Exception {
testExtractedRulesAndRunKotlin(
null,
getTransformedClasses(compilation, transformerForClass),
mainClass,
expectedRules,
expectedOutput);
}
protected void testExtractedRulesAndRunKotlin(
KotlinCompileMemoizer compilation,
BiFunction<ClassReference, byte[], byte[]> transformerForClass,
String mainClass,
ExpectedRules expectedRules)
throws Exception {
testExtractedRulesAndRunKotlin(
compilation, transformerForClass, mainClass, expectedRules, getExpectedOutputForKotlin());
}
}