Merge commit 'a7b4f924' into dev-release Change-Id: I9bcbe908dc03b3ecc28e2a5c575d59809cbd1ffd
diff --git a/.gitignore b/.gitignore index e310747..d3eb304 100644 --- a/.gitignore +++ b/.gitignore
@@ -2,7 +2,6 @@ !third_party/gmscore/*.sha1 !third_party/internal/*.sha1 !third_party/nest/*.sha1 -!third_party/proguard/*.sha1 !third_party/youtube/*sha1 #*# *.dex @@ -290,6 +289,7 @@ third_party/opensource-apps/android/nowinandroid third_party/opensource-apps/android/nowinandroid.tar.gz third_party/proguard/* +!third_party/proguard/*.sha1 third_party/proguardsettings.tar.gz third_party/proguardsettings/ third_party/proto/runtime/edition2023
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt index 17f6c74..000ca2b 100644 --- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt +++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -1089,7 +1089,7 @@ } fun getThirdPartyProguards(): List<ThirdPartyDependency> { - return listOf("proguard-7.0.0", "proguard-7.3.2", "proguard-7.4.1").map { + return listOf("proguard-7.0.0", "proguard-7.7.0").map { ThirdPartyDependency( it, Paths.get("third_party", "proguard", it).toFile(),
diff --git a/infra/config/global/generated/cr-buildbucket.cfg b/infra/config/global/generated/cr-buildbucket.cfg index 72dfcc9..3a60dff 100644 --- a/infra/config/global/generated/cr-buildbucket.cfg +++ b/infra/config/global/generated/cr-buildbucket.cfg
@@ -779,7 +779,7 @@ swarming_tags: "vpython:native-python-wrapper" dimensions: "cpu:x86-64" dimensions: "normal:true" - dimensions: "os:Ubuntu-20.04" + dimensions: "os:Ubuntu-22.04" dimensions: "pool:luci.r8.ci" exe { cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build" @@ -817,7 +817,7 @@ swarming_tags: "vpython:native-python-wrapper" dimensions: "cpu:x86-64" dimensions: "normal:true" - dimensions: "os:Ubuntu-20.04" + dimensions: "os:Ubuntu-22.04" dimensions: "pool:luci.r8.ci" exe { cipd_package: "infra_internal/recipe_bundles/chrome-internal.googlesource.com/chrome/tools/build"
diff --git a/infra/config/global/main.star b/infra/config/global/main.star index 4b51ba9..862f835 100755 --- a/infra/config/global/main.star +++ b/infra/config/global/main.star
@@ -404,7 +404,7 @@ r8_tester_with_default( "linux-android-5", ["--dex_vm=5.1.1", "--all_tests", "--command_cache_dir=/tmp/ccache"], - dimensions = get_dimensions(noble=False), + dimensions = get_dimensions(noble=True), ) r8_tester_with_default(
diff --git a/src/keepanno/java/androidx/annotation/keep/UsesReflectionToAccessField.kt b/src/keepanno/java/androidx/annotation/keep/UsesReflectionToAccessField.kt index b02671c..b453d72 100644 --- a/src/keepanno/java/androidx/annotation/keep/UsesReflectionToAccessField.kt +++ b/src/keepanno/java/androidx/annotation/keep/UsesReflectionToAccessField.kt
@@ -36,8 +36,8 @@ * @see UsesReflectionToConstruct * @see UsesReflectionToAccessMethod */ -@Repeatable @Retention(AnnotationRetention.BINARY) +@Repeatable @Target( AnnotationTarget.CLASS, AnnotationTarget.FIELD, @@ -51,14 +51,14 @@ * * Mutually exclusive with [className]. */ - val classConstant: KClass<*> = Unspecified::class, + @Suppress("KotlinDefaultParameterOrder") val classConstant: KClass<*> = Unspecified::class, /** * Class name (or class name pattern) containing the field accessed by reflection. * * Mutually exclusive with [classConstant]. */ - val className: String = "", + @Suppress("KotlinDefaultParameterOrder") val className: String = "", /** Name (or name pattern) of field accessed by reflection. */ val fieldName: String,
diff --git a/src/keepanno/java/androidx/annotation/keep/UsesReflectionToAccessMethod.kt b/src/keepanno/java/androidx/annotation/keep/UsesReflectionToAccessMethod.kt index 60b4e71..ddaefbf 100644 --- a/src/keepanno/java/androidx/annotation/keep/UsesReflectionToAccessMethod.kt +++ b/src/keepanno/java/androidx/annotation/keep/UsesReflectionToAccessMethod.kt
@@ -36,8 +36,8 @@ * @see UsesReflectionToConstruct * @see UsesReflectionToAccessField */ -@Repeatable @Retention(AnnotationRetention.BINARY) +@Repeatable @Target( AnnotationTarget.CLASS, AnnotationTarget.FIELD, @@ -51,14 +51,14 @@ * * Mutually exclusive with [className]. */ - val classConstant: KClass<*> = Unspecified::class, + @Suppress("KotlinDefaultParameterOrder") val classConstant: KClass<*> = Unspecified::class, /** * Class name (or class name pattern) containing the method accessed by reflection. * * Mutually exclusive with [classConstant]. */ - val className: String = "", + @Suppress("KotlinDefaultParameterOrder") val className: String = "", /** Name (or name pattern) of method accessed by reflection. */ val methodName: String, @@ -66,22 +66,22 @@ /** * Defines which method to keep by specifying set of parameter classes passed. * - * If neither `param` nor `paramTypeNames` is specified then methods with all parameter lists - * are kept. + * If neither `parameterTypes` nor `parameterTypeNames` is specified then methods with all + * parameter lists are kept. * - * Mutually exclusive with [paramTypeNames]. + * Mutually exclusive with [parameterTypeNames]. */ - val params: Array<KClass<*>> = [Unspecified::class], + val parameterTypes: Array<KClass<*>> = [Unspecified::class], /** * Defines which method to keep by specifying set of parameter classes passed. * - * If neither `param` nor `paramTypeNames` is specified then methods with all parameter lists - * are kept. + * If neither `parameterTypes` nor `parameterTypeNames` is specified then methods with all + * parameter lists are kept. * - * Mutually exclusive with [params]. + * Mutually exclusive with [parameterTypes]. */ - val paramTypeNames: Array<String> = [""], + val parameterTypeNames: Array<String> = [""], /** * Return type of the method accessed by reflection.
diff --git a/src/keepanno/java/androidx/annotation/keep/UsesReflectionToConstruct.kt b/src/keepanno/java/androidx/annotation/keep/UsesReflectionToConstruct.kt index 75f4d07..8aabcb5 100644 --- a/src/keepanno/java/androidx/annotation/keep/UsesReflectionToConstruct.kt +++ b/src/keepanno/java/androidx/annotation/keep/UsesReflectionToConstruct.kt
@@ -39,8 +39,8 @@ * @see UsesReflectionToAccessMethod * @see UsesReflectionToAccessField */ -@Repeatable @Retention(AnnotationRetention.BINARY) +@Repeatable @Target( AnnotationTarget.CLASS, AnnotationTarget.FIELD, @@ -66,20 +66,20 @@ /** * Defines which constructor to keep by specifying the parameter list types. * - * If neither `param` nor `paramTypeNames` is specified then constructors with all parameter - * lists are kept. + * If neither `parameterTypes` nor `parameterTypeNames` is specified then constructors with all + * parameter lists are kept. * - * Mutually exclusive with [paramTypeNames]. + * Mutually exclusive with [parameterTypeNames]. */ - val params: Array<KClass<*>> = [Unspecified::class], + val parameterTypes: Array<KClass<*>> = [Unspecified::class], /** * Defines which constructor to keep by specifying the parameter list types. * - * If neither `param` nor `paramTypeNames` is specified then constructors with all parameter - * lists are kept. + * If neither `parameterTypes` nor `parameterTypeNames` is specified then constructors with all + * parameter lists are kept. * - * Mutually exclusive with [params]. + * Mutually exclusive with [parameterTypes]. */ - val paramTypeNames: Array<String> = [""], + val parameterTypeNames: Array<String> = [""], )
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 d251880..6b0c939 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
@@ -34,6 +34,7 @@ import com.android.tools.r8.keepanno.ast.KeepCondition; import com.android.tools.r8.keepanno.ast.KeepConsequences; import com.android.tools.r8.keepanno.ast.KeepConstraint; +import com.android.tools.r8.keepanno.ast.KeepConstraint.Annotation; import com.android.tools.r8.keepanno.ast.KeepConstraints; import com.android.tools.r8.keepanno.ast.KeepDeclaration; import com.android.tools.r8.keepanno.ast.KeepEdge; @@ -67,6 +68,7 @@ import com.android.tools.r8.keepanno.ast.ParsingContext.MethodParsingContext; import com.android.tools.r8.keepanno.ast.ParsingContext.PropertyParsingContext; import com.google.common.collect.ImmutableList; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -109,7 +111,9 @@ || AnnotationConstants.UsedByReflection.isDescriptor(descriptor) || AnnotationConstants.UsedByNative.isDescriptor(descriptor) || AnnotationConstants.CheckRemoved.isDescriptor(descriptor) - || AnnotationConstants.CheckOptimizedOut.isDescriptor(descriptor)) { + || AnnotationConstants.CheckOptimizedOut.isDescriptor(descriptor) + || AnnotationConstants.UsesReflectionToConstruct.isKotlinRepeatableContainerDescriptor( + descriptor)) { return true; } return false; @@ -1544,11 +1548,11 @@ @Override public AnnotationVisitor visitArray(String name) { PropertyParsingContext propertyParsingContext = parsingContext.property(name); - if (name.equals(UsesReflectionToConstruct.params)) { + if (name.equals(UsesReflectionToConstruct.parameterTypes)) { return new ParametersClassVisitor( propertyParsingContext, parameters -> this.parameters = parameters); } - if (name.equals(UsesReflectionToConstruct.paramTypeNames)) { + if (name.equals(UsesReflectionToConstruct.parameterTypeNames)) { return new ParametersClassNamesVisitor( propertyParsingContext, parameters -> this.parameters = parameters); } @@ -1557,39 +1561,79 @@ @Override public void visitEnd() { - KeepClassItemPattern classItemPattern = - KeepClassItemPattern.builder() - .setClassPattern( - KeepClassPattern.builder().setClassNamePattern(qualifiedName).build()) - .build(); + String kotlinMetadataDescriptor = "Lkotlin/Metadata;"; + KeepClassBindingReference classBinding = - bindingsHelper.defineFreshClassBinding(classItemPattern); - KeepMemberItemPattern keepMemberItemPattern = - KeepMemberItemPattern.builder() - .setClassReference(classBinding) - .setMemberPattern( - KeepMethodPattern.builder() - .setNamePattern(KeepMethodNamePattern.instanceInitializer()) - .setParametersPattern(parameters) - .setReturnTypeVoid() - .build()) - .build(); + bindingsHelper.defineFreshClassBinding( + KeepClassItemPattern.builder() + .setClassPattern( + KeepClassPattern.builder().setClassNamePattern(qualifiedName).build()) + .build()); KeepMemberBindingReference memberBinding = - bindingsHelper.defineFreshMemberBinding(keepMemberItemPattern); - builder.setConsequences( + bindingsHelper.defineFreshMemberBinding( + KeepMemberItemPattern.builder() + .setClassReference(classBinding) + .setMemberPattern( + KeepMethodPattern.builder() + .setNamePattern(KeepMethodNamePattern.instanceInitializer()) + .setParametersPattern(parameters) + .setReturnTypeVoid() + .build()) + .build()); + + KeepClassBindingReference kotlinMetadataBinding = + bindingsHelper.defineFreshClassBinding( + KeepClassItemPattern.builder() + .setClassPattern(KeepClassPattern.exactFromDescriptor(kotlinMetadataDescriptor)) + .build()); + KeepMemberBindingReference kotlinMetadataMembersBinding = + bindingsHelper.defineFreshMemberBinding( + KeepMemberItemPattern.builder() + .setClassReference(kotlinMetadataBinding) + .setMemberPattern(KeepMemberPattern.allMembers()) + .build()); + + Annotation keepConstraintKotlinMetadataAnnotation = + KeepConstraint.annotation( + KeepAnnotationPattern.builder() + .setNamePattern( + KeepQualifiedClassNamePattern.exactFromDescriptor(kotlinMetadataDescriptor)) + .addRetentionPolicy(RetentionPolicy.RUNTIME) + .build()); + + KeepConsequences.Builder consequencesBuilder = KeepConsequences.builder() .addTarget( KeepTarget.builder() .setItemReference(classBinding) - .setItemReference(memberBinding) + .setConstraints( + KeepConstraints.defaultAdditions( + KeepConstraints.builder() + .add(keepConstraintKotlinMetadataAnnotation) + .build())) .build()) - .addTarget(KeepTarget.builder().setItemReference(classBinding).build()) - .build()); + .addTarget( + KeepTarget.builder() + .setItemReference(memberBinding) + // Keeping the kotlin.Metadata annotation on the members is not really needed, + // as the annotation is only supported on classes. However, having it here + // makes the keep rule extraction generate more compact rules. + .setConstraints( + KeepConstraints.defaultAdditions( + KeepConstraints.builder() + .add(keepConstraintKotlinMetadataAnnotation) + .build())) + .build()) + .addTarget(KeepTarget.builder().setItemReference(kotlinMetadataBinding).build()) + .addTarget( + KeepTarget.builder().setItemReference(kotlinMetadataMembersBinding).build()); + parent.accept( builder .setMetaInfo(metaInfoBuilder.build()) .setBindings(bindingsHelper.build()) .setPreconditions(preconditions.build()) + .setConsequences(consequencesBuilder.build()) .build()); } }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java index 73077b7..4d813f5 100644 --- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java +++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/AnnotationConstants.java
@@ -154,8 +154,8 @@ public static final String classConstant = "classConstant"; public static final String className = "className"; public static final String constructorParametersGroup = "constructor-parameters"; - public static final String params = "params"; - public static final String paramTypeNames = "paramTypeNames"; + public static final String parameterTypes = "parameterTypes"; + public static final String parameterTypeNames = "parameterTypeNames"; } public static final class UsesReflectionToAccessMethod { @@ -183,8 +183,8 @@ public static final String className = "className"; public static final String methodName = "methodName"; public static final String constructorParametersGroup = "constructor-parameters"; - public static final String params = "params"; - public static final String paramTypeNames = "paramTypeNames"; + public static final String parameterTypes = "parameterTypes"; + public static final String parameterTypeNames = "parameterTypeNames"; public static final String returnSelectionGroup = "return-selection"; public static final String returnType = "returnType"; public static final String returnTypeName = "returnTypeName";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackageComponentPattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackageComponentPattern.java index d60c8d0..f2e5c58 100644 --- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackageComponentPattern.java +++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackageComponentPattern.java
@@ -85,4 +85,12 @@ KeepPackageComponentPattern other = (KeepPackageComponentPattern) obj; return Objects.equals(singlePattern, other.singlePattern); } + + @Override + public String toString() { + if (isZeroOrMore()) { + return "**"; + } + return singlePattern.toString(); + } }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java index e1d24e1..2c3497d 100644 --- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java +++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepPackagePattern.java
@@ -209,4 +209,11 @@ public int hashCode() { return Objects.hash(isExact, componentPatterns); } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + componentPatterns.forEach(p -> sb.append(p).append("|")); + return sb.toString(); + } }
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java index fc1a44a..d99d59a 100644 --- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java +++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepQualifiedClassNamePattern.java
@@ -4,6 +4,7 @@ package com.android.tools.r8.keepanno.ast; import com.android.tools.r8.keepanno.proto.KeepSpecProtos.ClassNamePattern; +import com.android.tools.r8.keepanno.utils.DescriptorUtils; import java.util.Objects; import java.util.function.Consumer; @@ -21,7 +22,7 @@ } public static KeepQualifiedClassNamePattern exactFromDescriptor(String classDescriptor) { - if (!classDescriptor.startsWith("L") && classDescriptor.endsWith(";")) { + if (!DescriptorUtils.isValidClassDescriptor(classDescriptor)) { throw new KeepEdgeException("Invalid class descriptor: " + classDescriptor); } return exact(classDescriptor.substring(1, classDescriptor.length() - 1).replace('/', '.'));
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/utils/DescriptorUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/utils/DescriptorUtils.java new file mode 100644 index 0000000..6694cbc --- /dev/null +++ b/src/keepanno/java/com/android/tools/r8/keepanno/utils/DescriptorUtils.java
@@ -0,0 +1,25 @@ +// 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.utils; + +public class DescriptorUtils { + public static boolean isValidClassDescriptor(String string) { + if (string.length() < 3 + || string.charAt(0) != 'L' + || string.charAt(string.length() - 1) != ';') { + return false; + } + if (string.charAt(1) == '/' || string.charAt(string.length() - 2) == '/') { + return false; + } + int cp; + for (int i = 1; i < string.length() - 1; i += Character.charCount(cp)) { + cp = string.codePointAt(i); + if (cp != '/' && !IdentifierUtils.isRelaxedDexIdentifierPart(cp)) { + return false; + } + } + return true; + } +}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/utils/IdentifierUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/utils/IdentifierUtils.java new file mode 100644 index 0000000..50ae175 --- /dev/null +++ b/src/keepanno/java/com/android/tools/r8/keepanno/utils/IdentifierUtils.java
@@ -0,0 +1,37 @@ +// 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.utils; + +public class IdentifierUtils { + public static boolean isRelaxedDexIdentifierPart(int cp) { + return isSimpleNameChar(cp) || isUnicodeSpace(cp); + } + + public static boolean isUnicodeSpace(int cp) { + // Unicode 'Zs' category + return cp == ' ' + || cp == 0x00a0 + || cp == 0x1680 + || (0x2000 <= cp && cp <= 0x200a) + || cp == 0x202f + || cp == 0x205f + || cp == 0x3000; + } + + public static boolean isSimpleNameChar(int cp) { + // See https://source.android.com/devices/tech/dalvik/dex-format#string-syntax. + return ('A' <= cp && cp <= 'Z') + || ('a' <= cp && cp <= 'z') + || ('0' <= cp && cp <= '9') + || cp == '$' + || cp == '-' + || cp == '_' + || (0x00a1 <= cp && cp <= 0x1fff) + || (0x2010 <= cp && cp <= 0x2027) + || (0x2030 <= cp && cp <= 0xd7ff) + || (0xe000 <= cp && cp < 0xfeff) // Don't include BOM. + || (0xfeff < cp && cp <= 0xffef) + || (0x10000 <= cp && cp <= 0x10ffff); + } +}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java index 1d2c0ec..402e18b 100644 --- a/src/main/java/com/android/tools/r8/R8.java +++ b/src/main/java/com/android/tools/r8/R8.java
@@ -467,7 +467,8 @@ } // Compute after initial round of tree shaking to not trigger on pruned classes. - enableListIterationRewriter = ListIterationRewriter.shouldEnable(appView, subtypingInfo); + enableListIterationRewriter = + ListIterationRewriter.shouldEnableForR8(appView, subtypingInfo); timing.end(); } finally {
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java index 0499fe5..1e5c7d9 100644 --- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java +++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -52,6 +52,7 @@ import com.android.tools.r8.shaking.MainDexInfo; import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.ArrayUtils; +import com.android.tools.r8.utils.BooleanUtils; import com.android.tools.r8.utils.DescriptorUtils; import com.android.tools.r8.utils.DexVersion; import com.android.tools.r8.utils.ExceptionUtils; @@ -66,6 +67,7 @@ import com.android.tools.r8.utils.PredicateUtils; import com.android.tools.r8.utils.Reporter; import com.android.tools.r8.utils.StringDiagnostic; +import com.android.tools.r8.utils.SupplierUtils; import com.android.tools.r8.utils.ThreadUtils; import com.android.tools.r8.utils.timing.Timing; import com.android.tools.r8.utils.timing.TimingMerger; @@ -88,6 +90,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; public class ApplicationWriter { @@ -723,18 +726,25 @@ private void insertAttributeAnnotationsForClass(DexProgramClass clazz) { EnclosingMethodAttribute enclosingMethod = clazz.getEnclosingMethodAttribute(); List<InnerClassAttribute> innerClasses = clazz.getInnerClasses(); - if (enclosingMethod == null - && innerClasses.isEmpty() - && clazz.getClassSignature().hasNoSignature() - && !clazz.isInANest() - && !clazz.isRecord() - && !clazz.hasPermittedSubclassAttributes()) { - return; - } + + IntBox allocatedSize = new IntBox(); + Supplier<List<DexAnnotation>> annotationsSupplier = + SupplierUtils.nonThreadSafeMemoize( + () -> { + allocatedSize.set( + BooleanUtils.intValue(enclosingMethod != null) + + innerClasses.size() + + 1 + + BooleanUtils.intValue(clazz.getClassSignature().hasSignature()) + + BooleanUtils.intValue(options.canUseNestBasedAccess() && clazz.isInANest()) + + BooleanUtils.intValue( + clazz.hasPermittedSubclassAttributes() && options.canUseSealedClasses())); + return new ArrayList<>(allocatedSize.get()); + }); // EnclosingMember translates directly to an enclosing class/method if present. - List<DexAnnotation> annotations = new ArrayList<>(2 + innerClasses.size()); if (enclosingMethod != null) { + List<DexAnnotation> annotations = annotationsSupplier.get(); if (enclosingMethod.getEnclosingMethod() != null) { annotations.add( DexAnnotation.createEnclosingMethodAnnotation( @@ -752,6 +762,7 @@ // it relates to the present class. If it relates to the outer-type (and is named) it becomes // part of the member-classes annotation. if (!innerClasses.isEmpty()) { + List<DexAnnotation> annotations = annotationsSupplier.get(); List<DexType> memberClasses = new ArrayList<>(innerClasses.size()); for (InnerClassAttribute innerClass : innerClasses) { if (clazz.type == innerClass.getInner()) { @@ -782,6 +793,7 @@ } if (clazz.getClassSignature().hasSignature()) { + List<DexAnnotation> annotations = annotationsSupplier.get(); annotations.add( DexAnnotation.createSignatureAnnotation( clazz.getClassSignature().toRenamedString(getNamingLens(), isTypeMissing), @@ -790,12 +802,14 @@ if (options.canUseNestBasedAccess()) { if (clazz.isNestHost()) { + List<DexAnnotation> annotations = annotationsSupplier.get(); annotations.add( DexAnnotation.createNestMembersAnnotation( clazz.getNestMembersClassAttributes(), options.itemFactory)); } if (clazz.isNestMember()) { + List<DexAnnotation> annotations = annotationsSupplier.get(); annotations.add( DexAnnotation.createNestHostAnnotation( clazz.getNestHostClassAttribute(), options.itemFactory)); @@ -803,16 +817,21 @@ } if (clazz.hasPermittedSubclassAttributes() && options.canUseSealedClasses()) { + List<DexAnnotation> annotations = annotationsSupplier.get(); annotations.add( DexAnnotation.createPermittedSubclassesAnnotation( clazz.getPermittedSubclassAttributes(), options.itemFactory)); } if (clazz.isRecord() && options.emitRecordAnnotationsInDex) { + List<DexAnnotation> annotations = annotationsSupplier.get(); annotations.add(DexAnnotation.createRecordAnnotation(clazz, appView)); } - if (!annotations.isEmpty()) { + if (allocatedSize.get() > 0) { + List<DexAnnotation> annotations = annotationsSupplier.get(); + assert allocatedSize.get() >= annotations.size(); + // Append the annotations to annotations array of the class. DexAnnotation[] copy = ObjectArrays.concat(
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java index db3a696..78e812d 100644 --- a/src/main/java/com/android/tools/r8/graph/DexClass.java +++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -569,6 +569,10 @@ return methodCollection.getMethod(method); } + public DexEncodedMethod lookupMethod(DexMethodSignature method) { + return lookupMethod(method.getProto(), method.getName()); + } + public DexEncodedMethod lookupMethod(DexProto methodProto, DexString methodName) { return methodCollection.getMethod(methodProto, methodName); }
diff --git a/src/main/java/com/android/tools/r8/graph/MethodResolution.java b/src/main/java/com/android/tools/r8/graph/MethodResolution.java index 9a75cfe..fad11a2 100644 --- a/src/main/java/com/android/tools/r8/graph/MethodResolution.java +++ b/src/main/java/com/android/tools/r8/graph/MethodResolution.java
@@ -509,7 +509,7 @@ * Section 5.4.3.4 of the JVM Spec</a>. * * <p>The resolved method is not the method that will actually be invoked. Which methods gets - * invoked depends on the invoke instruction used. However, it is always save to rewrite any + * invoked depends on the invoke instruction used. However, it is always safe to rewrite any * invoke on the given descriptor to a corresponding invoke on the resolved descriptor, as the * resolved method is used as basis for dispatch. */
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/UndoConstructorInlining.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/UndoConstructorInlining.java index 70a2a9a..af621ab 100644 --- a/src/main/java/com/android/tools/r8/horizontalclassmerging/UndoConstructorInlining.java +++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/UndoConstructorInlining.java
@@ -63,7 +63,6 @@ import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -247,9 +246,8 @@ info.getProgramClass(), info.getInvokedMethod(), ensureConstructorsOnClasses, - newConstructor -> - profileCollectionAdditions.addMethodIfContextIsInProfile( - newConstructor, method)); + method, + profileCollectionAdditions); if (newInvokedMethod.getArity() != info.getInvokedMethod().getArity()) { assert newInvokedMethod.getArity() > info.getInvokedMethod().getArity(); return rewriteIR(method, code); @@ -310,9 +308,8 @@ noSkipClass, invokedMethod, ensureConstructorsOnClasses, - newConstructor -> - profileCollectionAdditions.addMethodIfContextIsInProfile( - newConstructor, method)); + method, + profileCollectionAdditions); InvokeDirect.Builder invokeDirectBuilder = InvokeDirect.builder() .setArguments(invoke.arguments()) @@ -484,19 +481,30 @@ DexProgramClass clazz, DexMethod target, Map<DexType, DexProgramClass> ensureConstructorsOnClasses, - Consumer<ProgramMethod> creationConsumer) { - return constructorCache - .computeIfAbsent(clazz, ignoreKey(IdentityHashMap::new)) - .computeIfAbsent( - target, - k -> createConstructor(clazz, target, ensureConstructorsOnClasses, creationConsumer)); + ProgramMethod context, + ProfileCollectionAdditions profileCollectionAdditions) { + ProgramMethod constructor = + constructorCache + .computeIfAbsent(clazz, ignoreKey(IdentityHashMap::new)) + .computeIfAbsent( + target, + k -> + createConstructor( + clazz, + target, + ensureConstructorsOnClasses, + context, + profileCollectionAdditions)); + profileCollectionAdditions.addMethodIfContextIsInProfile(constructor, context); + return constructor; } private ProgramMethod createConstructor( DexProgramClass clazz, DexMethod target, Map<DexType, DexProgramClass> ensureConstructorsOnClasses, - Consumer<ProgramMethod> creationConsumer) { + ProgramMethod context, + ProfileCollectionAdditions profileCollectionAdditions) { // Create a fresh constructor on the given class that calls target. If there is a class in the // hierarchy inbetween `clazz` and `target.holder`, which is also subject to class merging, // then we must create a constructor that calls a constructor on that intermediate class, @@ -510,7 +518,11 @@ if (ensureConstructorsOnClasses.containsKey(currentType)) { target = getOrCreateConstructor( - currentClass, target, ensureConstructorsOnClasses, creationConsumer) + currentClass, + target, + ensureConstructorsOnClasses, + context, + profileCollectionAdditions) .getReference(); break; } @@ -538,9 +550,7 @@ .setApiLevelForDefinition( appView.apiLevelCompute().computeInitialMinApiLevel(appView.options())) .build(); - ProgramMethod programMethod = method.asProgramMethod(clazz); - creationConsumer.accept(programMethod); - return programMethod; + return method.asProgramMethod(clazz); } private LirCode<Integer> createConstructorCode(DexMethod methodReference, DexMethod target) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java index 8ee1cd3..29925a7 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryD8L8IRConverter.java
@@ -30,6 +30,7 @@ import com.android.tools.r8.ir.desugar.itf.InterfaceProcessor; import com.android.tools.r8.ir.desugar.itf.L8InnerOuterAttributeEraser; import com.android.tools.r8.ir.desugar.lambda.LambdaDeserializationMethodRemover; +import com.android.tools.r8.ir.optimize.ListIterationRewriter; import com.android.tools.r8.ir.optimize.info.OptimizationFeedback; import com.android.tools.r8.position.MethodPosition; import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions; @@ -51,6 +52,9 @@ public PrimaryD8L8IRConverter(AppView<AppInfo> appView, Timing timing) { super(appView); this.timing = timing; + if (ListIterationRewriter.shouldEnableForD8(appView)) { + rewriterPassCollection.enableListIterationRewriter(appView); + } } @SuppressWarnings("BadImport")
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java index 2bef810..de9c246 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/CodeRewriterPass.java
@@ -33,6 +33,10 @@ return (AppView<T>) appView; } + protected T appInfo() { + return appView().appInfo(); + } + public final CodeRewriterResult run( IRCode code, MethodProcessor methodProcessor,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ListIterationRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ListIterationRewriter.java index 9f4445e..60e9dea 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/ListIterationRewriter.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/ListIterationRewriter.java
@@ -5,6 +5,7 @@ package com.android.tools.r8.ir.optimize; import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext; +import com.android.tools.r8.graph.AppInfo; import com.android.tools.r8.graph.AppInfoWithClassHierarchy; import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DexClass; @@ -34,7 +35,6 @@ import com.android.tools.r8.ir.conversion.MethodProcessor; import com.android.tools.r8.ir.conversion.passes.CodeRewriterPass; import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult; -import com.android.tools.r8.shaking.AppInfoWithLiveness; import com.android.tools.r8.utils.InternalOptions.TestingOptions; import com.android.tools.r8.utils.TraversalContinuation; import com.google.common.collect.ImmutableList; @@ -82,7 +82,7 @@ * The reason for the transformation is that the code runs ~3x faster and saves an allocation. * This transformation requires 2 extra registers and saves 2 bytes. */ -public class ListIterationRewriter extends CodeRewriterPass<AppInfoWithLiveness> { +public class ListIterationRewriter extends CodeRewriterPass<AppInfo> { private final DexString iteratorName; private final DexProto iteratorProto; private final DexType listType; @@ -114,12 +114,17 @@ this.immutableListType = dexItemFactory.comGoogleCommonCollectImmutableListType; } + public static boolean shouldEnableForD8(AppView<AppInfo> appView) { + return appView.options().isRelease() + && appView.testing().listIterationRewritingRewriteCustomIterators; + } + /** * Returns whether to enable the optimization. * * @param subtypingInfo May contain pruned items, but must not be missing any types. */ - public static boolean shouldEnable( + public static boolean shouldEnableForR8( AppView<? extends AppInfoWithClassHierarchy> appView, ImmediateAppSubtypingInfo subtypingInfo) { TestingOptions opts = appView.options().testing; @@ -154,6 +159,11 @@ } @Override + protected AppInfoWithClassHierarchy appInfo() { + return appView.appInfoForDesugaring(); + } + + @Override protected String getRewriterId() { return "ListIterationRewriter"; } @@ -211,14 +221,14 @@ // go to from 1010 to 1731. // While this would be safe for lists like: List.of(), singletonList(), etc, it would require // expensive intra-procedural analysis to track when it is safe. - return appView().appInfo().isSubtype(valueType, listType) + return appInfo().isSubtype(valueType, listType) // LinkedList.get() is not O(1). && valueType.isNotIdenticalTo(linkedListType) // CopyOnWriteArrayList.iterator() provides a snapshot of the list. && valueType.isNotIdenticalTo(copyOnWriteArrayListType); } // TODO(b/145280859): Add support for kotlin.collections.ArrayList. - return appView().appInfo().isSubtype(valueType, arrayListType) + return appInfo().isSubtype(valueType, arrayListType) || valueType.isIdenticalTo(immutableListType); } @@ -379,7 +389,7 @@ MethodResolutionResult resolvedSizeMethod = listClass == null || listClass.isInterface() ? null - : appView().appInfo().resolveMethodOnClass(listClass, sizeMethod); + : appInfo().resolveMethodOnClass(listClass, sizeMethod); InvokeMethodWithReceiver sizeInstr = resolvedSizeMethod == null || resolvedSizeMethod.getResolvedHolder().isInterface() ? new InvokeInterface(sizeMethod, sizeValue, sizeArgs) @@ -426,7 +436,7 @@ MethodResolutionResult resolvedGetMethod = listClass == null || listClass.isInterface() ? null - : appView().appInfo().resolveMethodOnClass(listClass, getMethod); + : appInfo().resolveMethodOnClass(listClass, getMethod); InvokeMethodWithReceiver getInstr = resolvedGetMethod == null || resolvedGetMethod.getResolvedHolder().isInterface() ? new InvokeInterface(getMethod, elementValue, getArgs)
diff --git a/src/main/java/com/android/tools/r8/naming/KotlinModuleSynthesizer.java b/src/main/java/com/android/tools/r8/naming/KotlinModuleSynthesizer.java index 1421e46..f38fdaf 100644 --- a/src/main/java/com/android/tools/r8/naming/KotlinModuleSynthesizer.java +++ b/src/main/java/com/android/tools/r8/naming/KotlinModuleSynthesizer.java
@@ -16,6 +16,7 @@ import com.android.tools.r8.kotlin.KotlinMultiFileClassPartInfo; import com.android.tools.r8.origin.Origin; import com.android.tools.r8.utils.Box; +import com.android.tools.r8.utils.FileUtils; import com.android.tools.r8.utils.Pair; import com.google.common.collect.ImmutableList; import java.util.ArrayList; @@ -43,7 +44,7 @@ } public boolean isKotlinModuleFile(DataEntryResource file) { - return file.getName().endsWith(".kotlin_module"); + return FileUtils.isKotlinModuleFile(file.getName()); } public List<DataEntryResource> synthesizeKotlinModuleFiles() {
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java index e18fa27..7fcedfc 100644 --- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java +++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -321,40 +321,58 @@ originalClass.accessFlags.isPublic() ? null : method.holder.getPackageDescriptor(); if (packageDescriptor == null || !packageDescriptor.equals(target.getHolderType().getPackageDescriptor())) { - DexProgramClass bridgeHolder = + DexClass bridgeHolder = findHolderForVisibilityBridge(originalClass, target.getHolder(), packageDescriptor); assert bridgeHolder != null; - bridges.accept(bridgeHolder, method, target); - return target.getReference().withHolder(bridgeHolder.getType(), appView.dexItemFactory()); + if (bridgeHolder.isClasspathClass()) { + // Intentionally empty. We do not need to insert a bridge on a classpath class. + } else if (bridgeHolder.isLibraryClass()) { + // Caution is needed when member rebinding into the library. + // This is handled by validMemberRebindingTargetForNonProgramMethod. + return null; + } else { + // Insert a bridge on this program class. + bridges.accept(bridgeHolder.asProgramClass(), method, target); + } + return target.getReference().withHolder(bridgeHolder, appView.dexItemFactory()); } return target.getReference(); } - private DexProgramClass findHolderForVisibilityBridge( - DexClass originalClass, DexClass targetClass, String packageDescriptor) { - if (originalClass == targetClass || originalClass.isNotProgramClass()) { + private DexClass findHolderForVisibilityBridge( + DexClass currentClass, DexClass resolvedHolder, String initialResolutionPackage) { + if (currentClass == resolvedHolder) { return null; } - DexProgramClass newHolder = null; + DexClass newHolder = null; // Recurse through supertype chain. - if (appView.appInfo().isSubtype(originalClass.superType, targetClass.type)) { - DexClass superClass = appView.definitionFor(originalClass.superType); - newHolder = findHolderForVisibilityBridge(superClass, targetClass, packageDescriptor); + if (appView.appInfo().isSubtype(currentClass.superType, resolvedHolder.type)) { + DexClass superClass = appView.definitionFor(currentClass.superType); + newHolder = + findHolderForVisibilityBridge(superClass, resolvedHolder, initialResolutionPackage); } else { - for (DexType iface : originalClass.interfaces.values) { - if (appView.appInfo().isSubtype(iface, targetClass.type)) { + for (DexType iface : currentClass.interfaces.values) { + if (appView.appInfo().isSubtype(iface, resolvedHolder.type)) { DexClass interfaceClass = appView.definitionFor(iface); - newHolder = findHolderForVisibilityBridge(interfaceClass, targetClass, packageDescriptor); + newHolder = + findHolderForVisibilityBridge( + interfaceClass, resolvedHolder, initialResolutionPackage); } } } if (newHolder != null) { // A supertype fulfills the visibility requirements. return newHolder; - } else if (originalClass.accessFlags.isPublic() - || originalClass.type.getPackageDescriptor().equals(packageDescriptor)) { - // This class is visible. Return it if it is a program class, otherwise null. - return originalClass.asProgramClass(); + } else { + boolean hasAccessToTarget = + AccessControl.isClassAccessible(resolvedHolder, currentClass, appView).isTrue(); + boolean isVisibleToCallers = + currentClass.isPublic() + || currentClass.type.getPackageDescriptor().equals(initialResolutionPackage); + if (hasAccessToTarget && isVisibleToCallers) { + // This class is visible and can access the target class. + return currentClass; + } } return null; }
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java index 617a1ed..a7203b9 100644 --- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java +++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
@@ -3,7 +3,6 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.optimize.bridgehoisting; - import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.BottomUpClassHierarchyTraversal; import com.android.tools.r8.graph.DexClass; @@ -11,6 +10,7 @@ import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexMethod; import com.android.tools.r8.graph.DexProgramClass; +import com.android.tools.r8.graph.DexType; import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo; import com.android.tools.r8.graph.MethodResolutionResult; import com.android.tools.r8.graph.ProgramMethod; @@ -19,8 +19,11 @@ import com.android.tools.r8.ir.optimize.info.bridge.VirtualBridgeInfo; import com.android.tools.r8.lightir.LirCode; import com.android.tools.r8.shaking.AppInfoWithLiveness; +import com.android.tools.r8.utils.DepthFirstSearchWorkListBase.DepthFirstSearchWorkList; import com.android.tools.r8.utils.ListUtils; import com.android.tools.r8.utils.MethodSignatureEquivalence; +import com.android.tools.r8.utils.TraversalContinuation; +import com.android.tools.r8.utils.collections.DexMethodSignatureSet; import com.android.tools.r8.utils.timing.Timing; import com.google.common.base.Equivalence; import com.google.common.base.Equivalence.Wrapper; @@ -29,12 +32,14 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.function.Function; /** * An optimization pass that hoists bridges upwards with the purpose of sharing redundant bridge @@ -66,6 +71,16 @@ private final AppView<AppInfoWithLiveness> appView; + // Cache that for a given non-interface class stores the default interface methods present on the + // class and all of its subclasses. + private final Map<DexProgramClass, DexMethodSignatureSet> + defaultInterfaceMethodsInClassAndSubclassesCache = new IdentityHashMap<>(); + + // Cache that for a given interface stores the default methods on that interface and all of its + // transitive superinterfaces. + private final Map<DexClass, DexMethodSignatureSet> inheritedDefaultInterfaceMethodsCache = + new IdentityHashMap<>(); + // Structure that keeps track of the changes for construction of the Proguard map and // AppInfoWithLiveness maintenance. private final BridgeHoistingResult result; @@ -103,7 +118,7 @@ } } for (ProgramMethod candidate : getCandidatesForHoisting(eligibleSubclasses)) { - hoistBridgeIfPossible(candidate, clazz, eligibleSubclasses); + hoistBridgeIfPossible(candidate, clazz, eligibleSubclasses, immediateSubtypingInfo); } } @@ -125,7 +140,10 @@ } private void hoistBridgeIfPossible( - ProgramMethod method, DexProgramClass clazz, List<DexProgramClass> subclasses) { + ProgramMethod method, + DexProgramClass clazz, + List<DexProgramClass> subclasses, + ImmediateProgramSubtypingInfo immediateSubtypingInfo) { // If the method is defined on the parent class, we cannot hoist the bridge. // TODO(b/153147967): If the declared method is abstract, we could replace it by the bridge. // Add a test. @@ -160,23 +178,38 @@ .appInfo() .resolveMethodOnClassLegacy(subclass, methodReference) .getSingleTarget(); - if (resolutionTarget == null || resolutionTarget.isAbstract()) { - // The fact that this class does not declare the bridge (or the bridge is abstract) should - // not prevent us from hoisting the bridge. - // - // Strictly speaking, there could be an invoke instruction that targets the bridge on this - // subclass and fails with an AbstractMethodError or a NoSuchMethodError in the input - // program. After hoisting the bridge to the superclass such an instruction would no - // longer fail with an error in the generated program. - // - // If this ever turns out be an issue, it would be possible to track if there is an invoke - // instruction targeting the bridge on this subclass that fails in the Enqueuer, but this - // should never be the case in practice. - continue; + if (resolutionTarget != null && !resolutionTarget.isAbstract()) { + // Hoisting would change the program behavior. + return; } - // Hoisting would change the program behavior. - return; + if (appView.options().canUseDefaultAndStaticInterfaceMethods()) { + DexMethodSignatureSet defaultInterfaceMethodsBelowSubclass = + getOrComputeDefaultInterfaceMethodsOnClassAndSubclasses( + subclass, immediateSubtypingInfo); + if (defaultInterfaceMethodsBelowSubclass.contains(method)) { + // Hoisting would change the program behavior. Virtual methods on classes takes + // precedence over default interface methods. By hoisting the current bridge method to + // the superclass, we may change virtual calls that would previously have dispatched to + // a default interface method into calling the hoisted bridge method. + // + // See also b/369040938. + return; + } + } + + // The fact that this class does not declare the bridge (or the bridge is abstract) should + // not prevent us from hoisting the bridge. + // + // Strictly speaking, there could be an invoke instruction that targets the bridge on this + // subclass and fails with an AbstractMethodError or a NoSuchMethodError in the input + // program. After hoisting the bridge to the superclass such an instruction would no + // longer fail with an error in the generated program. + // + // If this ever turns out be an issue, it would be possible to track if there is an invoke + // instruction targeting the bridge on this subclass that fails in the Enqueuer, but this + // should never be the case in practice. + continue; } BridgeInfo currentBridgeInfo = definition.getOptimizationInfo().getBridgeInfo(); @@ -320,4 +353,104 @@ return item; }); } + + private DexMethodSignatureSet getOrComputeDefaultInterfaceMethodsOnClassAndSubclasses( + DexProgramClass clazz, ImmediateProgramSubtypingInfo immediateSubtypingInfo) { + if (!defaultInterfaceMethodsInClassAndSubclassesCache.containsKey(clazz)) { + new DefaultInterfaceMethodsTraversal(immediateSubtypingInfo).run(List.of(clazz)); + } + DexMethodSignatureSet cachedResult = + defaultInterfaceMethodsInClassAndSubclassesCache.get(clazz); + assert cachedResult != null; + return cachedResult; + } + + // A depth-first traversal over the class hierarchy that will collect and cache the default + // interface method present on a given class and its subclasses. + private class DefaultInterfaceMethodsTraversal + extends DepthFirstSearchWorkList<DexProgramClass, Void, Void> { + + private final ImmediateProgramSubtypingInfo immediateSubtypingInfo; + + DefaultInterfaceMethodsTraversal(ImmediateProgramSubtypingInfo immediateSubtypingInfo) { + this.immediateSubtypingInfo = immediateSubtypingInfo; + } + + @Override + protected TraversalContinuation<Void, Void> process( + DFSNode<DexProgramClass> node, + Function<DexProgramClass, DFSNode<DexProgramClass>> childNodeConsumer) { + // Enqueue unseen subclasses for processing. + DexProgramClass currentClass = node.getNode(); + assert !currentClass.isInterface(); + assert !defaultInterfaceMethodsInClassAndSubclassesCache.containsKey(currentClass); + for (DexProgramClass subclass : immediateSubtypingInfo.getSubclasses(currentClass)) { + if (!defaultInterfaceMethodsInClassAndSubclassesCache.containsKey(subclass)) { + DFSNode<DexProgramClass> unusedChildNode = childNodeConsumer.apply(subclass); + } + } + return TraversalContinuation.doContinue(); + } + + @Override + public TraversalContinuation<Void, Void> joiner(DFSNode<DexProgramClass> node) { + // All subclasses of the current class have now been processed. + DexProgramClass currentClass = node.getNode(); + assert !currentClass.isInterface(); + assert !defaultInterfaceMethodsInClassAndSubclassesCache.containsKey(currentClass); + // Compute the default interface methods that this class inherits from its implemented + // interfaces. + DexMethodSignatureSet defaultInterfaceMethods = DexMethodSignatureSet.create(); + for (DexType implementedType : currentClass.getInterfaces()) { + DexClass implementedInterface = appView.definitionFor(implementedType); + if (implementedInterface != null) { + defaultInterfaceMethods.addAll( + getOrComputeInheritedDefaultInterfaceMethodsForInterface(implementedInterface)); + } + } + // Include the default interface methods that are present on the class' subclasses. + for (DexProgramClass subclass : immediateSubtypingInfo.getSubclasses(currentClass)) { + DexMethodSignatureSet defaultInterfaceMethodsInSubclass = + defaultInterfaceMethodsInClassAndSubclassesCache.get(subclass); + assert defaultInterfaceMethodsInSubclass != null; + defaultInterfaceMethods.addAll(defaultInterfaceMethodsInSubclass); + } + // Remove all virtual methods declared by the class itself, since we are only interested in + // the occurrence of default interface method that are not overridden by a non-default + // interface method. + defaultInterfaceMethods.removeIf(m -> currentClass.lookupMethod(m) != null); + // Cache the computed result. + defaultInterfaceMethodsInClassAndSubclassesCache.put(currentClass, defaultInterfaceMethods); + return TraversalContinuation.doContinue(); + } + + // For a given interface, returns the default interface methods present on that interface and + // all transitive superinterfaces. + private DexMethodSignatureSet getOrComputeInheritedDefaultInterfaceMethodsForInterface( + DexClass itf) { + assert itf.isInterface(); + // Check if we have already computed the default interface methods in the current interface + // and its superinterfaces. + DexMethodSignatureSet cachedResult = inheritedDefaultInterfaceMethodsCache.get(itf); + if (cachedResult != null) { + return cachedResult; + } + DexMethodSignatureSet inheritedDefaultInterfaceMethods = DexMethodSignatureSet.create(); + // First add the default interface methods that are present on the interface directly. + for (DexEncodedMethod defaultInterfaceMethod : + itf.virtualMethods(DexEncodedMethod::hasCode)) { + inheritedDefaultInterfaceMethods.add(defaultInterfaceMethod); + } + // Then add the default interface methods that are present on the superinterfaces. + for (DexType implementedType : itf.getInterfaces()) { + DexClass implementedInterface = appView.definitionFor(implementedType); + if (implementedInterface != null) { + inheritedDefaultInterfaceMethods.addAll( + getOrComputeInheritedDefaultInterfaceMethodsForInterface(implementedInterface)); + } + } + inheritedDefaultInterfaceMethodsCache.put(itf, inheritedDefaultInterfaceMethods); + return inheritedDefaultInterfaceMethods; + } + } }
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java index c5238b0..f2bf9a4 100644 --- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java +++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -4600,11 +4600,20 @@ if (clazz.isLibraryClass()) { libraryClasses.add(clazz.asLibraryClass()); } else if (clazz.isClasspathClass()) { - classpathClasses.add(clazz.asClasspathClass()); + DexClasspathClass classpathClass = clazz.asClasspathClass(); + if (appView.getSyntheticItems().isPendingSynthetic(classpathClass.getType())) { + // This class will be committed later. + } else { + classpathClasses.add(classpathClass); + } } else { assert false; } } + // Verify the synthetic classpath classes are live before we commit them to the app below. + assert appView.getSyntheticItems().getPendingSyntheticClasses().stream() + .filter(DexClass::isClasspathClass) + .allMatch(liveNonProgramTypes::contains); // Add just referenced non-program types. We can't replace the program classes at this point as // they are needed in tree pruning.
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java index f7cc1e5..f2eea5b 100644 --- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java +++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -870,7 +870,6 @@ return true; } - @SuppressWarnings("MixedMutabilityReturnType") private DexType createExternalType( SyntheticKind kind, String externalSyntheticTypePrefix, @@ -879,7 +878,8 @@ Predicate<DexType> reserved) { DexItemFactory factory = appView.dexItemFactory(); if (kind.isFixedSuffixSynthetic()) { - return SyntheticNaming.createExternalType(kind, externalSyntheticTypePrefix, "", factory); + return SyntheticNaming.createExternalType( + kind, externalSyntheticTypePrefix, "", factory, appView.options()); } NumberGenerator generator = generators.computeIfAbsent(externalSyntheticTypePrefix, k -> new NumberGenerator()); @@ -887,7 +887,11 @@ do { externalType = SyntheticNaming.createExternalType( - kind, externalSyntheticTypePrefix, Integer.toString(generator.next()), factory); + kind, + externalSyntheticTypePrefix, + Integer.toString(generator.next()), + factory, + appView.options()); // If the generated external type matches an external synthetic from the input, which is kept, // then continue. if (reserved.test(externalType)) {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java index 676989f..f50b1db 100644 --- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java +++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -3,6 +3,8 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.synthesis; +import static com.android.tools.r8.utils.DescriptorUtils.INNER_CLASS_SEPARATOR; + import com.android.tools.r8.Version; import com.android.tools.r8.errors.Unreachable; import com.android.tools.r8.graph.AppView; @@ -12,6 +14,7 @@ import com.android.tools.r8.references.ClassReference; import com.android.tools.r8.references.Reference; import com.android.tools.r8.utils.DescriptorUtils; +import com.android.tools.r8.utils.InternalOptions; import com.android.tools.r8.utils.structural.Equatable; import com.android.tools.r8.utils.structural.Ordered; import com.google.common.hash.Hasher; @@ -478,14 +481,23 @@ } static DexType createExternalType( - SyntheticKind kind, String externalSyntheticTypePrefix, String id, DexItemFactory factory) { + SyntheticKind kind, + String externalSyntheticTypePrefix, + String id, + DexItemFactory factory, + InternalOptions options) { assert kind.isFixedSuffixSynthetic() == id.isEmpty(); - return createType( - kind.isFixedSuffixSynthetic() ? "" : EXTERNAL_SYNTHETIC_CLASS_SEPARATOR, - kind, - externalSyntheticTypePrefix, - id, - factory); + if (kind.isFixedSuffixSynthetic()) { + assert id.isEmpty(); + return createType("", kind, externalSyntheticTypePrefix, id, factory); + } else if (options.desugarSpecificOptions().minimizeSyntheticNames) { + return factory.createType( + DescriptorUtils.getDescriptorFromClassBinaryName( + externalSyntheticTypePrefix + INNER_CLASS_SEPARATOR + id)); + } else { + return createType( + EXTERNAL_SYNTHETIC_CLASS_SEPARATOR, kind, externalSyntheticTypePrefix, id, factory); + } } private static DexType createType( @@ -505,7 +517,7 @@ public static String createDescriptor( String separator, SyntheticKind kind, String externalSyntheticTypePrefix, String id) { return DescriptorUtils.getDescriptorFromClassBinaryName( - externalSyntheticTypePrefix + separator + kind.descriptor + id); + externalSyntheticTypePrefix + separator + (kind != null ? kind.descriptor : "") + id); } public static boolean verifyNotInternalSynthetic(DexType type) { @@ -542,6 +554,12 @@ createDescriptor(EXTERNAL_SYNTHETIC_CLASS_SEPARATOR, kind, context.getBinaryName(), id)); } + static ClassReference makeMinimalSyntheticReferenceForTest(ClassReference context, String id) { + return Reference.classFromDescriptor( + createDescriptor( + Character.toString(INNER_CLASS_SEPARATOR), null, context.getBinaryName(), id)); + } + static boolean isSynthetic(ClassReference clazz, Phase phase, SyntheticKind kind) { String typeName = clazz.getTypeName(); if (kind.isFixedSuffixSynthetic()) {
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java index 08da5f1..39624d2 100644 --- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java +++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -734,22 +734,7 @@ } public static boolean isValidClassDescriptor(String string) { - if (string.length() < 3 - || string.charAt(0) != 'L' - || string.charAt(string.length() - 1) != ';') { - return false; - } - if (string.charAt(1) == '/' || string.charAt(string.length() - 2) == '/') { - return false; - } - int cp; - for (int i = 1; i < string.length() - 1; i += Character.charCount(cp)) { - cp = string.codePointAt(i); - if (cp != '/' && !IdentifierUtils.isRelaxedDexIdentifierPart(cp)) { - return false; - } - } - return true; + return com.android.tools.r8.keepanno.utils.DescriptorUtils.isValidClassDescriptor(string); } public static boolean isValidBinaryName(String binaryName) {
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java index 9153858..72ac1a3 100644 --- a/src/main/java/com/android/tools/r8/utils/FileUtils.java +++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -31,6 +31,9 @@ public static final String JAVA_EXTENSION = ".java"; public static final String KT_EXTENSION = ".kt"; public static final String MODULE_INFO_CLASS = "module-info.class"; + public static final String KOTLIN_MODULE = ".kotlin_module"; + public static final String KOTLIN_BUILTINS = ".kotlin_builtins"; + public static final String MODULES_PREFIX = "/modules"; public static final String GLOBAL_SYNTHETIC_EXTENSION = ".global"; @@ -93,6 +96,14 @@ || name.endsWith(AAR_EXTENSION); } + public static boolean isKotlinModuleFile(String name) { + return name.endsWith(KOTLIN_MODULE); + } + + public static boolean isKotlinBuiltinsFile(String name) { + return name.endsWith(KOTLIN_BUILTINS); + } + public static String readTextFile(Path file) throws IOException { return readTextFile(file, StandardCharsets.UTF_8); }
diff --git a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java index 5591c14..3acd909 100644 --- a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java +++ b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
@@ -24,29 +24,10 @@ } public static boolean isUnicodeSpace(int cp) { - // Unicode 'Zs' category - return cp == ' ' - || cp == 0x00a0 - || cp == 0x1680 - || (0x2000 <= cp && cp <= 0x200a) - || cp == 0x202f - || cp == 0x205f - || cp == 0x3000; + return com.android.tools.r8.keepanno.utils.IdentifierUtils.isUnicodeSpace(cp); } private static boolean isSimpleNameChar(int cp) { - // See https://source.android.com/devices/tech/dalvik/dex-format#string-syntax. - return ('A' <= cp && cp <= 'Z') - || ('a' <= cp && cp <= 'z') - || ('0' <= cp && cp <= '9') - || cp == '$' - || cp == '-' - || cp == '_' - || (0x00a1 <= cp && cp <= 0x1fff) - || (0x2010 <= cp && cp <= 0x2027) - || (0x2030 <= cp && cp <= 0xd7ff) - || (0xe000 <= cp && cp < 0xfeff) // Don't include BOM. - || (0xfeff < cp && cp <= 0xffef) - || (0x10000 <= cp && cp <= 0x10ffff); + return com.android.tools.r8.keepanno.utils.IdentifierUtils.isSimpleNameChar(cp); } }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java index 07fd942..6bedcb4 100644 --- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java +++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1683,6 +1683,9 @@ // See b/182065081 for why this is here. public boolean lambdaClassFieldsFinal = System.getProperty("com.android.tools.r8.lambdaClassFieldsNotFinal") == null; + public boolean minimizeSyntheticNames = + SystemPropertyUtils.parseSystemPropertyOrDefault( + "com.android.tools.r8.desugar.minimizeSyntheticNames", false); } public class RewriteArrayOptions {
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java index fb33e96..b77eaff 100644 --- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java +++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -15,6 +15,7 @@ import com.android.tools.r8.androidapi.AndroidApiDataAccess; import com.android.tools.r8.errors.CompilationError; import com.android.tools.r8.references.ClassReference; +import com.android.tools.r8.references.Reference; import com.google.common.io.ByteStreams; import com.google.common.io.Closer; import java.io.BufferedOutputStream; @@ -392,4 +393,12 @@ } return -1; } + + public static ClassReference entryToClassReference(ZipEntry entry) { + if (!FileUtils.isClassFile(entry.getName())) { + return null; + } + return Reference.classFromBinaryName( + entry.getName().substring(0, entry.getName().length() - CLASS_EXTENSION.length())); + } }
diff --git a/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java b/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java index d8429ea..3047ca7 100644 --- a/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java +++ b/src/test/bootstrap/com/android/tools/r8/bootstrap/SanityCheck.java
@@ -16,6 +16,7 @@ import com.android.tools.r8.TestParametersCollection; import com.android.tools.r8.ToolHelper; import com.android.tools.r8.naming.ClassNameMapper; +import com.android.tools.r8.utils.FileUtils; import com.android.tools.r8.utils.ZipUtils; import com.google.common.collect.Sets; import java.io.IOException; @@ -70,7 +71,7 @@ while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String name = entry.getName(); - if (ZipUtils.isClassFile(name) || name.endsWith(".kotlin_builtins")) { + if (ZipUtils.isClassFile(name) || FileUtils.isKotlinBuiltinsFile(name)) { assertThat(name, startsWith("com/android/tools/r8/")); } else if (name.equals("META-INF/MANIFEST.MF")) { // Allow.
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingWithDifferentProfileFlagsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingWithDifferentProfileFlagsTest.java new file mode 100644 index 0000000..a0b1bc5 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingWithDifferentProfileFlagsTest.java
@@ -0,0 +1,128 @@ +// 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.classmerging.horizontal; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.NeverClassInline; +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.profile.art.ArtProfileMethodRuleInfoImpl; +import com.android.tools.r8.profile.art.model.ExternalArtProfile; +import com.android.tools.r8.references.Reference; +import com.android.tools.r8.utils.MethodReferenceUtils; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.InstructionSubject; +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 ConstructorMergingWithDifferentProfileFlagsTest extends TestBase { + + @Parameter(0) + public TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + @Test + public void test() throws Exception { + testForR8(parameters) + .addInnerClasses(getClass()) + .addKeepMainRule(Main.class) + .addArtProfileForRewriting( + ExternalArtProfile.builder() + .addMethodRule( + MethodReferenceUtils.instanceConstructor( + Reference.classFromClass(A.class), Reference.INT), + ArtProfileMethodRuleInfoImpl.builder().setIsHot().build()) + .addMethodRule( + MethodReferenceUtils.instanceConstructor( + Reference.classFromClass(B.class), Reference.INT), + ArtProfileMethodRuleInfoImpl.builder().setIsPostStartup().build()) + .addMethodRule( + MethodReferenceUtils.instanceConstructor( + Reference.classFromClass(C.class), Reference.INT), + ArtProfileMethodRuleInfoImpl.builder().setIsStartup().build()) + .build()) + .addHorizontallyMergedClassesInspector( + inspector -> + inspector + .assertIsCompleteMergeGroup(A.class, B.class, C.class) + .assertNoOtherClassesMerged()) + .enableInliningAnnotations() + .enableNeverClassInliningAnnotations() + .compile() + .inspectResidualArtProfile( + (profile, inspector) -> { + ClassSubject classSubject = inspector.clazz(A.class); + assertThat(classSubject, isPresent()); + + MethodSubject switchInitializerSubject = classSubject.init("int", "int"); + assertThat(switchInitializerSubject, isPresent()); + assertTrue( + switchInitializerSubject + .streamInstructions() + .anyMatch(InstructionSubject::isSwitch)); + + profile + .assertContainsMethodRule(switchInitializerSubject) + .inspectMethodRule( + switchInitializerSubject, + methodRuleInspector -> + methodRuleInspector + .assertIsHot() + .assertIsPostStartup() + .assertIsStartup()); + }) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("A.f=0", "B.f=0", "C.f=0"); + } + + static class Main { + + public static void main(String[] args) { + new A(args.length); + new B(args.length); + new C(args.length); + } + } + + @NeverClassInline + static class A { + + @NeverInline + A(int f) { + System.out.println("A.f=" + f); + } + } + + @NeverClassInline + static class B { + + @NeverInline + B(int f) { + System.out.println("B.f=" + f); + } + } + + @NeverClassInline + static class C { + + @NeverInline + C(int f) { + System.out.println("C.f=" + f); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithDifferentProfileFlagsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithDifferentProfileFlagsTest.java new file mode 100644 index 0000000..775e2f8 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithDifferentProfileFlagsTest.java
@@ -0,0 +1,138 @@ +// 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.classmerging.horizontal; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.profile.art.ArtProfileMethodRuleInfoImpl; +import com.android.tools.r8.profile.art.model.ExternalArtProfile; +import com.android.tools.r8.references.Reference; +import com.android.tools.r8.utils.MethodReferenceUtils; +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 EquivalentConstructorsWithDifferentProfileFlagsTest extends TestBase { + + @Parameter(0) + public TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withAllRuntimesAndApiLevels().build(); + } + + @Test + public void test() throws Exception { + testForR8(parameters) + .addInnerClasses(getClass()) + .addKeepMainRule(Main.class) + .addArtProfileForRewriting( + ExternalArtProfile.builder() + .addMethodRule( + MethodReferenceUtils.instanceConstructor( + Reference.classFromClass(A.class), Reference.INT), + ArtProfileMethodRuleInfoImpl.builder().setIsHot().build()) + .addMethodRule( + MethodReferenceUtils.instanceConstructor( + Reference.classFromClass(B.class), Reference.INT), + ArtProfileMethodRuleInfoImpl.builder().setIsPostStartup().build()) + .addMethodRule( + MethodReferenceUtils.instanceConstructor( + Reference.classFromClass(C.class), Reference.INT), + ArtProfileMethodRuleInfoImpl.builder().setIsStartup().build()) + .build()) + .addHorizontallyMergedClassesInspector( + inspector -> + inspector + .assertIsCompleteMergeGroup(A.class, B.class, C.class) + .assertNoOtherClassesMerged()) + .enableInliningAnnotations() + .compile() + .inspectResidualArtProfile( + (profile, inspector) -> { + ClassSubject classSubject = inspector.clazz(A.class); + assertThat(classSubject, isPresent()); + + MethodSubject initializerSubject = classSubject.init("int", "int"); + assertThat(initializerSubject, isPresent()); + + profile + .assertContainsMethodRule(initializerSubject) + .inspectMethodRule( + initializerSubject, + methodRuleInspector -> + methodRuleInspector + .assertIsHot() + .assertIsPostStartup() + .assertIsStartup()); + }) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("f=0", "f=0", "f=0"); + } + + static class Main { + + public static void main(String[] args) { + System.out.println(new A(args.length)); + System.out.println(new B(args.length)); + System.out.println(new C(args.length)); + } + } + + static class A { + + int f; + + @NeverInline + A(int f) { + this.f = f; + } + + @Override + public String toString() { + return "f=" + f; + } + } + + static class B { + + int f; + + @NeverInline + B(int f) { + this.f = f; + } + + @Override + public String toString() { + return "f=" + f; + } + } + + static class C { + + int f; + + @NeverInline + C(int f) { + this.f = f; + } + + @Override + public String toString() { + return "f=" + f; + } + } +}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingVirtualMethodMergingWithLibraryTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingVirtualMethodMergingWithLibraryTest.java index e44992d..7861ac8 100644 --- a/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingVirtualMethodMergingWithLibraryTest.java +++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/HorizontalClassMergingVirtualMethodMergingWithLibraryTest.java
@@ -50,11 +50,7 @@ .enableNoVerticalClassMergingAnnotations() .compile() .run(parameters.getRuntime(), Main.class) - .applyIf( - parameters.canUseDefaultAndStaticInterfaceMethods(), - // TODO(b/369040938): Disallow bridge hoisting of B.m(). - rr -> rr.assertSuccessWithOutputLines("A.bridgeTarget()", "A.bridgeTarget()"), - rr -> rr.assertSuccessWithOutputLines("A.bridgeTarget()", "I.m()")); + .assertSuccessWithOutputLines("A.bridgeTarget()", "I.m()"); } static class Main {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java index b156a3c..1427b8c 100644 --- a/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java +++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java
@@ -24,6 +24,7 @@ @Test public void test() throws Exception { + parameters.assumeDexRuntime(); testForR8(parameters.getBackend()) .addInnerClasses(getClass()) .addKeepMainRule(Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/UndoConstructorInliningProfileFlagsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/UndoConstructorInliningProfileFlagsTest.java new file mode 100644 index 0000000..68c15f2 --- /dev/null +++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/UndoConstructorInliningProfileFlagsTest.java
@@ -0,0 +1,118 @@ +// 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.classmerging.horizontal; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assume.assumeTrue; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.profile.art.ArtProfileMethodRuleInfoImpl; +import com.android.tools.r8.profile.art.model.ExternalArtProfile; +import com.android.tools.r8.references.Reference; +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 UndoConstructorInliningProfileFlagsTest extends TestBase { + + @Parameter(0) + public TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withDexRuntimesAndAllApiLevels().build(); + } + + @Test + public void test() throws Exception { + assumeTrue(parameters.canInitNewInstanceUsingSuperclassConstructor()); + testForR8(parameters) + .addInnerClasses(getClass()) + .addKeepClassAndMembersRules(Main.class) + .addArtProfileForRewriting( + ExternalArtProfile.builder() + .addMethodRule( + Reference.methodFromMethod(Main.class.getDeclaredMethod("hot")), + ArtProfileMethodRuleInfoImpl.builder().setIsHot().build()) + .addMethodRule( + Reference.methodFromMethod(Main.class.getDeclaredMethod("postStartup")), + ArtProfileMethodRuleInfoImpl.builder().setIsPostStartup().build()) + .addMethodRule( + Reference.methodFromMethod(Main.class.getDeclaredMethod("startup")), + ArtProfileMethodRuleInfoImpl.builder().setIsStartup().build()) + .build()) + .addHorizontallyMergedClassesInspector( + inspector -> + inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged()) + .addOptionsModification(options -> options.getSingleCallerInlinerOptions().setEnable(false)) + .compile() + .inspectResidualArtProfile( + (profile, inspector) -> { + ClassSubject classSubject = inspector.clazz(A.class); + assertThat(classSubject, isPresent()); + + MethodSubject initializerSubject = classSubject.init("int"); + assertThat(initializerSubject, isPresent()); + + profile + .assertContainsMethodRule(initializerSubject) + .inspectMethodRule( + initializerSubject, + methodRuleInspector -> + methodRuleInspector + .assertIsHot() + .assertIsPostStartup() + .assertIsStartup()); + }); + } + + static class Main { + + static void hot() { + System.out.println(new A()); + System.out.println(new B()); + } + + static void postStartup() { + System.out.println(new A()); + System.out.println(new B()); + } + + static void startup() { + System.out.println(new A()); + System.out.println(new B()); + } + } + + // Needed to ensure that all merge classes are in the same SCC. + static class Base {} + + static class A extends Base { + + A() {} + + @Override + public String toString() { + return "A"; + } + } + + static class B extends Base { + + B() {} + + @Override + public String toString() { + return "B"; + } + } +}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DayOfWeekTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DayOfWeekTest.java new file mode 100644 index 0000000..64c20c7 --- /dev/null +++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DayOfWeekTest.java
@@ -0,0 +1,80 @@ +// 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.desugar.desugaredlibrary; + +import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS; +import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.getJdk8Jdk11; + +import com.android.tools.r8.SingleTestRunResult; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification; +import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification; +import com.android.tools.r8.utils.AndroidApiLevel; +import java.time.format.DateTimeFormatter; +import java.time.temporal.IsoFields; +import java.time.temporal.TemporalAccessor; +import java.util.List; +import java.util.Locale; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class DayOfWeekTest extends DesugaredLibraryTestBase { + private final TestParameters parameters; + private final LibraryDesugaringSpecification libraryDesugaringSpecification; + private final CompilationSpecification compilationSpecification; + + @Parameters(name = "{0}, spec: {1}, {2}") + public static List<Object[]> data() { + return buildParameters( + getTestParameters().withAllRuntimes().withAllApiLevels().build(), + getJdk8Jdk11(), + DEFAULT_SPECIFICATIONS); + } + + public DayOfWeekTest( + TestParameters parameters, + LibraryDesugaringSpecification libraryDesugaringSpecification, + CompilationSpecification compilationSpecification) { + this.parameters = parameters; + this.libraryDesugaringSpecification = libraryDesugaringSpecification; + this.compilationSpecification = compilationSpecification; + } + + @Test + public void testDay() throws Exception { + if (parameters.isCfRuntime()) { + testForJvm(parameters) + .addInnerClasses(DayOfWeekTest.class) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("6", "7"); + return; + } + testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification) + .addInnerClasses(getClass()) + .addKeepMainRule(Main.class) + .run(parameters.getRuntime(), Main.class) + .applyIf( + // TODO(b/432689492): Fix non standalone names. + libraryDesugaringSpecification == LibraryDesugaringSpecification.JDK8 + || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O), + b -> b.assertSuccessWithOutputLines("6", "7"), + SingleTestRunResult::assertFailure); + } + + static class Main { + + public static void main(String[] args) { + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + TemporalAccessor access1 = formatter.parse("Sun, 06 Jul 2025 10:07:45 GMT"); + System.out.println(access1.get(IsoFields.DAY_OF_QUARTER)); + TemporalAccessor access2 = formatter.parse("Mon, 07 Jul 2025 10:07:45 GMT"); + System.out.println(access2.get(IsoFields.DAY_OF_QUARTER)); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaMinimizeSyntheticNamesTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaMinimizeSyntheticNamesTest.java new file mode 100644 index 0000000..11a51e5 --- /dev/null +++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaMinimizeSyntheticNamesTest.java
@@ -0,0 +1,73 @@ +// 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.desugar.lambdas; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.synthesis.SyntheticItemsTestUtils; +import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +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 LambdaMinimizeSyntheticNamesTest extends TestBase { + + @Parameter(0) + public TestParameters parameters; + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withDexRuntimesAndAllApiLevels().build(); + } + + @Test + public void testD8() throws Exception { + testForD8(parameters) + .addInnerClasses(getClass()) + .addOptionsModification(this::configure) + .release() + .compile() + .inspect(this::inspect); + } + + @Test + public void testR8() throws Exception { + testForR8(parameters) + .addInnerClasses(getClass()) + .addKeepMainRule(Main.class) + .addDontObfuscate() + .addOptionsModification(this::configure) + .compile() + .inspect(this::inspect); + } + + private void configure(InternalOptions options) { + assertFalse(options.desugarSpecificOptions().minimizeSyntheticNames); + options.desugarSpecificOptions().minimizeSyntheticNames = true; + } + + private void inspect(CodeInspector inspector) { + ClassSubject lambdaClass = + inspector.clazz(SyntheticItemsTestUtils.syntheticClassWithMinimalName(Main.class, 0)); + assertThat(lambdaClass, isPresentAndNotRenamed()); + } + + static class Main { + + public static void main(String[] args) { + Runnable runnable = () -> {}; + System.out.println(runnable); + } + } +}
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 282b101..74380cf 100644 --- a/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java +++ b/src/test/java/com/android/tools/r8/keepanno/KeepAnnoTestBuilder.java
@@ -12,6 +12,7 @@ import com.android.tools.r8.ExternalR8TestBuilder; import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler; import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion; +import com.android.tools.r8.ProgramResourceProvider; import com.android.tools.r8.ProguardTestBuilder; import com.android.tools.r8.R8FullTestBuilder; import com.android.tools.r8.R8PartialTestBuilder; @@ -89,6 +90,9 @@ public abstract KeepAnnoTestBuilder addProgramFiles(List<Path> programFiles) throws IOException; + public abstract KeepAnnoTestBuilder addProgramResourceProviders( + ProgramResourceProvider... providers); + public final KeepAnnoTestBuilder addProgramClasses(Class<?>... programClasses) throws IOException { return addProgramClasses(Arrays.asList(programClasses)); @@ -126,6 +130,14 @@ public abstract SingleTestRunResult<?> run(String mainClass) throws Exception; + public KeepAnnoTestBuilder applyIf( + boolean condition, ThrowableConsumer<KeepAnnoTestBuilder> consumer) { + if (condition) { + consumer.acceptWithRuntimeException(this); + } + return this; + } + public KeepAnnoTestBuilder applyIfShrinker( ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> builderConsumer) { applyIfR8(builderConsumer); @@ -222,6 +234,12 @@ } @Override + public KeepAnnoTestBuilder addProgramResourceProviders(ProgramResourceProvider... providers) { + assert false : "not supported"; + return this; + } + + @Override public KeepAnnoTestBuilder addProgramClasses(List<Class<?>> programClasses) { builder.addProgramClasses(programClasses); return this; @@ -343,6 +361,12 @@ } @Override + public KeepAnnoTestBuilder addProgramResourceProviders(ProgramResourceProvider... providers) { + builder.addProgramResourceProviders(Arrays.asList(providers)); + return this; + } + + @Override public KeepAnnoTestBuilder addProgramClasses(List<Class<?>> programClasses) throws IOException { for (Class<?> programClass : programClasses) { extractAndAdd(ToolHelper.getClassAsBytes(programClass)); @@ -561,6 +585,12 @@ } @Override + public KeepAnnoTestBuilder addProgramResourceProviders(ProgramResourceProvider... providers) { + builder.addProgramResourceProviders(Arrays.asList(providers)); + return this; + } + + @Override public KeepAnnoTestBuilder addProgramClasses(List<Class<?>> programClasses) throws IOException { List<String> rules = KeepAnnoTestUtils.extractRules(programClasses, extractorOptions); builder.addProgramClasses(programClasses); @@ -659,6 +689,12 @@ } @Override + public KeepAnnoTestBuilder addProgramResourceProviders(ProgramResourceProvider... providers) { + builder.addProgramResourceProviders(Arrays.asList(providers)); + return this; + } + + @Override public KeepAnnoTestBuilder addProgramClasses(List<Class<?>> programClasses) throws IOException { List<String> rules = KeepAnnoTestUtils.extractRules(programClasses, extractorOptions); builder.addProgramClasses(programClasses);
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 index 4103ea9..2497a5b 100644 --- a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java +++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepAnnoTestExtractedRulesBase.java
@@ -3,23 +3,42 @@ // 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.DataEntryResource; +import com.android.tools.r8.DataResourceProvider; 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.ProgramResource; +import com.android.tools.r8.ProgramResourceProvider; +import com.android.tools.r8.ResourceException; 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.io.ByteStreams; +import java.io.IOException; +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; public abstract class KeepAnnoTestExtractedRulesBase extends KeepAnnoTestBase { + @Parameter(0) public KeepAnnoParameters parameters; @@ -29,36 +48,120 @@ protected static KotlinCompileMemoizer compilationResults; protected static KotlinCompileMemoizer compilationResultsClassName; - protected abstract String getExpectedOutput(); + 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; } - public static class ExpectedRule { + protected static byte[] setAnnotationOnClass( + ClassFileTransformer transformer, + Class<?> annotationClass, + Consumer<AnnotationBuilder> builderConsumer) { + return transformer.setAnnotation(annotationClass, builderConsumer).transform(); + } + + protected static byte[] setAnnotationOnMethod( + ClassFileTransformer transformer, + MethodPredicate methodPredicate, + Class<?> annotationClass, + Consumer<AnnotationBuilder> builderConsumer) { + return transformer.setAnnotation(methodPredicate, annotationClass, builderConsumer).transform(); + } + + protected static byte[] setAnnotationOnClass( + Class<?> clazz, Class<?> annotationClass, Consumer<AnnotationBuilder> builderConsumer) + throws IOException { + return setAnnotationOnClass(transformer(clazz), annotationClass, builderConsumer); + } + + protected static byte[] setAnnotationOnMethod( + Class<?> clazz, + MethodPredicate methodPredicate, + Class<?> annotationClass, + Consumer<AnnotationBuilder> builderConsumer) + throws IOException { + return setAnnotationOnMethod( + transformer(clazz), methodPredicate, annotationClass, builderConsumer); + } + + protected static byte[] setAnnotationOnClass( + ClassReference classReference, + byte[] classFileBytes, + ClassReference classReferenceToTransform, + Class<?> annotationClass, + Consumer<AnnotationBuilder> builderConsumer) { + if (!classReference.equals(classReferenceToTransform)) { + return classFileBytes; + } + return setAnnotationOnClass( + transformer(classFileBytes, classReference), annotationClass, builderConsumer); + } + + protected static byte[] setAnnotationOnMethod( + ClassReference classReference, + byte[] classFileBytes, + ClassReference classReferenceToTransform, + MethodPredicate methodPredicate, + Class<?> annotationClass, + Consumer<AnnotationBuilder> builderConsumer) { + if (!classReference.equals(classReferenceToTransform)) { + return classFileBytes; + } + return setAnnotationOnMethod( + transformer(classFileBytes, classReference), + methodPredicate, + annotationClass, + 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 ExpectedRule(Builder builder) { + 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 : "") + " " - + conditionMembers - + " -keepclasseswithmembers" + + keepVariant + (r8 ? ",allowaccessmodification" : "") + " class " + consequentClass @@ -71,6 +174,8 @@ } public static class Builder { + + private String keepVariant = "-keepclasseswithmembers"; private String conditionClass; private String conditionMembers; private String consequentClass; @@ -78,6 +183,11 @@ private Builder() {} + public Builder setKeepVariant(String keepVariant) { + this.keepVariant = keepVariant; + return this; + } + public Builder setConditionClass(Class<?> conditionClass) { this.conditionClass = conditionClass.getTypeName(); return this; @@ -108,85 +218,305 @@ return this; } - public ExpectedRule build() { - return new ExpectedRule(this); + public Builder apply(Consumer<Builder> fn) { + fn.accept(this); + return this; + } + + public ExpectedKeepRule build() { + return new ExpectedKeepRule(this); } } } - protected void runTestExtractedRulesJava(List<Class<?>> testClasses, ExpectedRule expectedRule) + 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); + } + } + + protected void runTestExtractedRulesJava( + Class<?> mainClass, + List<Class<?>> testClasses, + List<byte[]> testClassFileData, + ExpectedRules expectedRules) throws Exception { - Class<?> mainClass = testClasses.iterator().next(); + // 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()); testForKeepAnnoAndroidX(parameters) - .applyIfPG( - b -> { - KotlinCompiler kotlinc = - new KotlinCompiler(KotlinCompilerVersion.MAX_SUPPORTED_VERSION); - b.addLibraryFiles(kotlinc.getKotlinStdlibJar(), kotlinc.getKotlinAnnotationJar()); - }) .addProgramClasses(testClasses) + .addProgramClassFileData(testClassFileData) .addKeepMainRule(mainClass) .setExcludedOuterClass(getClass()) .inspectExtractedRules( rules -> { if (parameters.isExtractRules()) { - assertEquals( - ImmutableList.of(expectedRule.getRule(parameters.isR8())), trimRules(rules)); + assertListsAreEqual(expectedRules.getRules(parameters.isR8()), trimRules(rules)); } }) .run(mainClass) - .assertSuccessWithOutput(getExpectedOutput()); + .assertSuccessWithOutput(getExpectedOutputForJava()); + } + + protected void runTestExtractedRulesJava(List<Class<?>> testClasses, ExpectedRules expectedRules) + throws Exception { + runTestExtractedRulesJava( + testClasses.iterator().next(), testClasses, ImmutableList.of(), expectedRules); } protected void runTestExtractedRulesKotlin( - KotlinCompileMemoizer compilation, String mainClass, ExpectedRule expectedRule) + KotlinCompileMemoizer compilation, + List<byte[]> classFileData, + String mainClass, + ExpectedRules expectedRules) 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)". + // 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()); testForKeepAnnoAndroidX(parameters) - .addProgramFiles(ImmutableList.of(compilation.getForConfiguration(kotlinParameters))) - .addProgramFilesWithoutAnnotations( - ImmutableList.of( - kotlinParameters.getCompiler().getKotlinStdlibJar(), - kotlinParameters.getCompiler().getKotlinReflectJar(), + .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())) - .applyIfR8Current(R8TestBuilder::allowDiagnosticWarningMessages) - .addKeepRules("-keepattributes RuntimeVisibleAnnotations") - .addKeepRules("-keep class kotlin.Metadata { *; }") + .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[]); }") - .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)); + assertListsAreEqual(expectedRules.getRules(parameters.isR8()), trimRules(rules)); } }) .run(mainClass) - .applyIf( - parameters.isExtractRules(), - b -> b.assertSuccessWithOutput(getExpectedOutput()), - b -> b.assertSuccessWithOutput("")); + .assertSuccessWithOutput(getExpectedOutputForKotlin()); + } + + protected void runTestExtractedRulesKotlin( + KotlinCompileMemoizer compilation, String mainClass, ExpectedRules expectedRules) + throws Exception { + runTestExtractedRulesKotlin(compilation, ImmutableList.of(), mainClass, expectedRules); + } + + protected void runTestExtractedRulesKotlin( + KotlinCompileMemoizer compilation, + BiFunction<ClassReference, byte[], byte[]> transformerForClass, + String mainClass, + ExpectedRules expectedRules) + throws Exception { + 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))); + }); + runTestExtractedRulesKotlin(null, result, mainClass, expectedRules); } }
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationMultipleConstructorsTest.java b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationMultipleConstructorsTest.java new file mode 100644 index 0000000..80ea899 --- /dev/null +++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationMultipleConstructorsTest.java
@@ -0,0 +1,227 @@ +// 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.ToolHelper.DexVm.Version; +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 java.util.function.Consumer; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class KeepUsesReflectionForInstantiationMultipleConstructorsTest + extends KeepAnnoTestExtractedRulesBase { + + // String constant to be referenced from annotations. + static final String classNameOfKeptClass = + "com.android.tools.r8.keepanno.androidx.KeepUsesReflectionForInstantiationMultipleConstructorsTest$KeptClass"; + + @Parameterized.Parameters(name = "{0}, {1}") + public static Collection<Object[]> data() { + assertEquals(KeptClass.class.getTypeName(), classNameOfKeptClass); + return buildParameters( + createParameters( + getTestParameters() + .withDexRuntime(Version.V14_0_0) + .withDefaultCfRuntime() + .withMaximumApiLevel() + .build()), + getKotlinTestParameters().withLatestCompiler().build()); + } + + @Override + protected String getExpectedOutputForJava() { + return StringUtils.lines("<init>(int)", "<init>(long)"); + } + + @Override + protected String getExpectedOutputForKotlin() { + // Kotlin secondary constructors has to delegate to the primary constructor. + return StringUtils.lines( + "fun `<init>`(kotlin.Int): com.android.tools.r8.keepanno.androidx.kt.KeptClass", + "<init>()", + "<init>(Int)", + "fun `<init>`(kotlin.Long): com.android.tools.r8.keepanno.androidx.kt.KeptClass", + "<init>()", + "<init>(Long)"); + } + + private static Collection<Path> getKotlinSources() { + try { + return getFilesInTestFolderRelativeToClass( + KeepUsesReflectionForInstantiationMultipleConstructorsTest.class, + "kt", + "IntAndLongArgsConstructors.kt", + "KeptClass.kt"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Collection<Path> getKotlinSourcesClassName() { + try { + return getFilesInTestFolderRelativeToClass( + KeepUsesReflectionForInstantiationMultipleConstructorsTest.class, + "kt", + "IntAndLongArgsConstructorsClassName.kt", + "KeptClass.kt"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @BeforeClass + public static void beforeClass() throws Exception { + compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources()); + compilationResultsClassName = getCompileMemoizerWithKeepAnnoLib(getKotlinSourcesClassName()); + } + + private ExpectedRules expectedRulesJava(Class<?> conditionClass) { + Consumer<ExpectedKeepRule.Builder> setCondition = + b -> + b.setConditionClass(conditionClass) + .setConditionMembers("{ void foo(java.lang.Class); }"); + return ExpectedRules.builder() + .add( + ExpectedKeepRule.builder() + .apply(setCondition) + .setConsequentClass(KeptClass.class) + .setConsequentMembers("{ void <init>(int); }") + .build()) + .add( + ExpectedKeepRule.builder() + .apply(setCondition) + .setConsequentClass(KeptClass.class) + .setConsequentMembers("{ void <init>(long); }") + .build()) + .apply(b -> addConsequentKotlinMetadata(b, bb -> bb.apply(setCondition))) + .build(); + } + + private ExpectedRules expectedRulesKotlin(String conditionClass) { + String conditionMember = "{ void foo(kotlin.reflect.KClass); }"; + return ExpectedRules.builder() + .add( + ExpectedKeepRule.builder() + .setConditionClass(conditionClass) + .setConditionMembers(conditionMember) + .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass") + .setConsequentMembers("{ void <init>(int); }") + .build()) + .add( + ExpectedKeepRule.builder() + .setConditionClass(conditionClass) + .setConditionMembers(conditionMember) + .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass") + .setConsequentMembers("{ void <init>(long); }") + .build()) + .apply( + b -> + addConsequentKotlinMetadata( + b, + bb -> + bb.setConditionClass(conditionClass).setConditionMembers(conditionMember))) + .build(); + } + + @Test + public void testIntAndLongArgsConstructors() throws Exception { + runTestExtractedRulesJava( + ImmutableList.of(IntAndLongArgsConstructors.class, KeptClass.class), + expectedRulesJava(IntAndLongArgsConstructors.class)); + } + + static class IntAndLongArgsConstructors { + + @UsesReflectionToConstruct( + classConstant = KeptClass.class, + parameterTypes = {int.class}) + @UsesReflectionToConstruct( + classConstant = KeptClass.class, + parameterTypes = {long.class}) + public void foo(Class<KeptClass> clazz) throws Exception { + if (clazz != null) { + clazz.getDeclaredConstructor(int.class).newInstance(1); + clazz.getDeclaredConstructor(long.class).newInstance(2L); + } + } + + public static void main(String[] args) throws Exception { + new IntAndLongArgsConstructors().foo(System.nanoTime() > 0 ? KeptClass.class : null); + } + } + + @Test + public void testIntLongArgsConstructorsClassNames() throws Exception { + runTestExtractedRulesJava( + ImmutableList.of(IntAndLongConstructorsClassName.class, KeptClass.class), + expectedRulesJava(IntAndLongConstructorsClassName.class)); + } + + static class IntAndLongConstructorsClassName { + + @UsesReflectionToConstruct( + className = classNameOfKeptClass, + parameterTypes = {int.class}) + @UsesReflectionToConstruct( + className = classNameOfKeptClass, + parameterTypes = {long.class}) + public void foo(Class<KeptClass> clazz) throws Exception { + if (clazz != null) { + clazz.getDeclaredConstructor(int.class).newInstance(1); + clazz.getDeclaredConstructor(long.class).newInstance(2L); + } + } + + public static void main(String[] args) throws Exception { + new IntAndLongConstructorsClassName().foo(System.nanoTime() > 0 ? KeptClass.class : null); + } + } + + @Test + public void testIntLongArgsConstructorsKotlin() throws Exception { + runTestExtractedRulesKotlin( + compilationResults, + "com.android.tools.r8.keepanno.androidx.kt.IntAndLongArgsConstructorsKt", + expectedRulesKotlin( + "com.android.tools.r8.keepanno.androidx.kt.IntAndLongArgsConstructors")); + } + + @Test + public void testIntLongArgsConstructorsKotlinClassName() throws Exception { + runTestExtractedRulesKotlin( + compilationResultsClassName, + "com.android.tools.r8.keepanno.androidx.kt.IntAndLongArgsConstructorsClassNameKt", + expectedRulesKotlin( + "com.android.tools.r8.keepanno.androidx.kt.IntAndLongArgsConstructorsClassName")); + } + + static class KeptClass { + KeptClass() { + System.out.println("<init>()"); + } + + KeptClass(int i) { + System.out.println("<init>(int)"); + } + + KeptClass(long j) { + System.out.println("<init>(long)"); + } + + KeptClass(String s) { + System.out.println("<init>(String)"); + } + } +}
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 index dfa7b78..6309009 100644 --- a/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationNoArgsConstructorTest.java +++ b/src/test/java/com/android/tools/r8/keepanno/androidx/KeepUsesReflectionForInstantiationNoArgsConstructorTest.java
@@ -7,15 +7,23 @@ import static org.junit.Assert.assertEquals; import androidx.annotation.keep.UsesReflectionToConstruct; +import com.android.tools.r8.KotlinCompileMemoizer; +import com.android.tools.r8.ToolHelper.DexVm.Version; +import com.android.tools.r8.references.Reference; +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.StringUtils; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Path; import java.util.Collection; +import java.util.function.Consumer; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.objectweb.asm.Type; @RunWith(Parameterized.class) public class KeepUsesReflectionForInstantiationNoArgsConstructorTest @@ -28,15 +36,32 @@ @Parameterized.Parameters(name = "{0}, {1}") public static Collection<Object[]> data() { assertEquals(KeptClass.class.getTypeName(), classNameOfKeptClass); + // Test with Android 14, which has `java.lang.ClassValue` to avoid having to deal with R8 + // missing class warnings for tests using the kotlin-reflect library. return buildParameters( - createParameters(getTestParameters().withDefaultRuntimes().withMaximumApiLevel().build()), + createParameters( + getTestParameters() + .withDexRuntime(Version.V14_0_0) + .withDefaultCfRuntime() + .withMaximumApiLevel() + .build()), getKotlinTestParameters().withLatestCompiler().build()); } - protected String getExpectedOutput() { + @Override + protected String getExpectedOutputForJava() { return StringUtils.lines("<init>()"); } + @Override + protected String getExpectedOutputForKotlin() { + return StringUtils.lines( + "fun `<init>`(): com.android.tools.r8.keepanno.androidx.kt.KeptClass", + "<init>()", + "fun `<init>`(): com.android.tools.r8.keepanno.androidx.kt.KeptClass", + "<init>()"); + } + private static Collection<Path> getKotlinSources() { try { return getFilesInTestFolderRelativeToClass( @@ -61,29 +86,78 @@ } } + protected static KotlinCompileMemoizer compilationResultsWithoutAnnotation; + + private static Collection<Path> getKotlinSourcesWithoutAnnotation() { + try { + return getFilesInTestFolderRelativeToClass( + KeepUsesReflectionForInstantiationNoArgsConstructorTest.class, + "kt", + "OnlyNoArgsConstructorWithoutAnnotation.kt", + "KeptClass.kt"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + @BeforeClass public static void beforeClass() throws Exception { compilationResults = getCompileMemoizerWithKeepAnnoLib(getKotlinSources()); compilationResultsClassName = getCompileMemoizerWithKeepAnnoLib(getKotlinSourcesClassName()); + compilationResultsWithoutAnnotation = + new KotlinCompileMemoizer(getKotlinSourcesWithoutAnnotation()); + } + + private static ExpectedRules getExpectedRulesJava(Class<?> conditionClass) { + return getExpectedRulesJava(conditionClass, null); + } + + private static ExpectedRules getExpectedRulesJava( + Class<?> conditionClass, String contitionMembers) { + Consumer<ExpectedKeepRule.Builder> setCondition = + b -> b.setConditionClass(conditionClass).setConditionMembers(contitionMembers); + + ExpectedRules.Builder builder = + ExpectedRules.builder() + .add( + ExpectedKeepRule.builder() + .apply(setCondition) + .setConsequentClass(KeptClass.class) + .setConsequentMembers("{ void <init>(); }") + .build()); + addConsequentKotlinMetadata(builder, b -> b.apply(setCondition)); + return builder.build(); + } + + private static ExpectedRules getExpectedRulesKotlin(String conditionClass) { + Consumer<ExpectedKeepRule.Builder> setCondition = + b -> + b.setConditionClass(conditionClass) + .setConditionMembers("{ void foo(kotlin.reflect.KClass); }"); + ExpectedRules.Builder builder = + ExpectedRules.builder() + .add( + ExpectedKeepRule.builder() + .apply(setCondition) + .setConsequentClass("com.android.tools.r8.keepanno.androidx.kt.KeptClass") + .setConsequentMembers("{ void <init>(); }") + .build()); + addConsequentKotlinMetadata(builder, b -> b.apply(setCondition)); + return builder.build(); } @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()); + getExpectedRulesJava(OnlyNoArgsConstructor.class, "{ void foo(java.lang.Class); }")); } static class OnlyNoArgsConstructor { @UsesReflectionToConstruct( - className = classNameOfKeptClass, - params = {}) + classConstant = KeptClass.class, + parameterTypes = {}) public void foo(Class<KeptClass> clazz) throws Exception { if (clazz != null) { clazz.getDeclaredConstructor().newInstance(); @@ -96,22 +170,18 @@ } @Test - public void testOnlyNoArgsConstructorClassNames() throws Exception { + public void testOnlyNoArgsConstructorClassName() 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()); + ImmutableList.of(OnlyNoArgsConstructorClassName.class, KeptClass.class), + getExpectedRulesJava( + OnlyNoArgsConstructorClassName.class, "{ void foo(java.lang.Class); }")); } - static class OnlyNoArgsConstructorClassNames { + static class OnlyNoArgsConstructorClassName { @UsesReflectionToConstruct( className = classNameOfKeptClass, - params = {}) + parameterTypes = {}) public void foo(Class<KeptClass> clazz) throws Exception { if (clazz != null) { clazz.getDeclaredConstructor().newInstance(); @@ -119,7 +189,7 @@ } public static void main(String[] args) throws Exception { - new OnlyNoArgsConstructorClassNames().foo(System.nanoTime() > 0 ? KeptClass.class : null); + new OnlyNoArgsConstructorClassName().foo(System.nanoTime() > 0 ? KeptClass.class : null); } } @@ -128,12 +198,7 @@ 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()); + getExpectedRulesKotlin("com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructor")); } @Test @@ -141,13 +206,138 @@ 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()); + getExpectedRulesKotlin( + "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorClassName")); + } + + private static void buildNoArgsConstructor(AnnotationBuilder builder, Object clazz) { + if (clazz instanceof String) { + builder.setField("className", clazz); + } else { + assert clazz instanceof Class<?> || clazz instanceof Type; + builder.setField("classConstant", clazz); + } + // Set the empty array for the no args constructor. + builder.setArray("parameterTypes"); + } + + @Test + // This test is similar to testOnlyNoArgsConstructor() except that the annotation is inserted + // by a transformer. + public void testOnlyNoArgsConstructorUsingTransformer() throws Exception { + runTestExtractedRulesJava( + OnlyNoArgsConstructorWithoutAnnotation.class, + ImmutableList.of(KeptClass.class), + ImmutableList.of( + setAnnotationOnMethod( + OnlyNoArgsConstructorWithoutAnnotation.class, + MethodPredicate.onName("foo"), + UsesReflectionToConstruct.class, + builder -> buildNoArgsConstructor(builder, KeptClass.class))), + getExpectedRulesJava( + OnlyNoArgsConstructorWithoutAnnotation.class, "{ void foo(java.lang.Class); }")); + } + + @Test + public void testOnlyNoArgsConstructorOnClass() throws Exception { + runTestExtractedRulesJava( + OnlyNoArgsConstructorWithoutAnnotation.class, + ImmutableList.of(KeptClass.class), + ImmutableList.of( + setAnnotationOnClass( + OnlyNoArgsConstructorWithoutAnnotation.class, + UsesReflectionToConstruct.class, + builder -> buildNoArgsConstructor(builder, KeptClass.class))), + getExpectedRulesJava(OnlyNoArgsConstructorWithoutAnnotation.class)); + } + + @Test + // This test is similar to testOnlyNoArgsConstructorClassName() except that the annotation is + // inserted by a transformer. + public void testOnlyNoArgsConstructorClassNameUsingTransformer() throws Exception { + runTestExtractedRulesJava( + OnlyNoArgsConstructorWithoutAnnotation.class, + ImmutableList.of(KeptClass.class), + ImmutableList.of( + setAnnotationOnMethod( + OnlyNoArgsConstructorWithoutAnnotation.class, + MethodPredicate.onName("foo"), + UsesReflectionToConstruct.class, + builder -> buildNoArgsConstructor(builder, classNameOfKeptClass))), + getExpectedRulesJava( + OnlyNoArgsConstructorWithoutAnnotation.class, "{ void foo(java.lang.Class); }")); + } + + @Test + public void testOnlyNoArgsConstructorClassNameOnClass() throws Exception { + runTestExtractedRulesJava( + OnlyNoArgsConstructorWithoutAnnotation.class, + ImmutableList.of(KeptClass.class), + ImmutableList.of( + setAnnotationOnClass( + OnlyNoArgsConstructorWithoutAnnotation.class, + UsesReflectionToConstruct.class, + builder -> buildNoArgsConstructor(builder, classNameOfKeptClass))), + getExpectedRulesJava(OnlyNoArgsConstructorWithoutAnnotation.class)); + } + + @Test + public void testOnlyNoArgsConstructorKotlinUsingTransformer() throws Exception { + runTestExtractedRulesKotlin( + compilationResultsWithoutAnnotation, + (classReference, classFileBytes) -> + setAnnotationOnMethod( + classReference, + classFileBytes, + Reference.classFromTypeName( + "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorWithoutAnnotation"), + MethodPredicate.onName("foo"), + UsesReflectionToConstruct.class, + builder -> + buildNoArgsConstructor( + builder, + Type.getType( + DescriptorUtils.javaTypeToDescriptor( + "com.android.tools.r8.keepanno.androidx.kt.KeptClass")))), + "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorWithoutAnnotationKt", + getExpectedRulesKotlin( + "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorWithoutAnnotation")); + } + + @Test + public void testOnlyNoArgsConstructorKotlinClassNameUsingTransformer() throws Exception { + runTestExtractedRulesKotlin( + compilationResultsWithoutAnnotation, + (classReference, classFileBytes) -> + setAnnotationOnMethod( + classReference, + classFileBytes, + Reference.classFromTypeName( + "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorWithoutAnnotation"), + MethodPredicate.onName("foo"), + UsesReflectionToConstruct.class, + builder -> + buildNoArgsConstructor( + builder, "com.android.tools.r8.keepanno.androidx.kt.KeptClass")), + "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorWithoutAnnotationKt", + getExpectedRulesKotlin( + "com.android.tools.r8.keepanno.androidx.kt.OnlyNoArgsConstructorWithoutAnnotation")); + } + + // Test class without annotation to be used by multiple tests inserting annotations using a + // transformer. + static class OnlyNoArgsConstructorWithoutAnnotation { + + public void foo(Class<KeptClass> clazz) throws Exception { + if (clazz != null) { + clazz.getDeclaredConstructor().newInstance(); + } + } + + public static void main(String[] args) throws Exception { + new OnlyNoArgsConstructorWithoutAnnotation() + .foo(System.nanoTime() > 0 ? KeptClass.class : null); + } } static class KeptClass {
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/IntAndLongArgsConstructors.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/IntAndLongArgsConstructors.kt new file mode 100644 index 0000000..2f0dcd2 --- /dev/null +++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/IntAndLongArgsConstructors.kt
@@ -0,0 +1,32 @@ +// 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.kt + +import androidx.annotation.keep.UsesReflectionToConstruct +import kotlin.reflect.KClass +import kotlin.reflect.full.createType + +class IntAndLongArgsConstructors { + + @UsesReflectionToConstruct(classConstant = KeptClass::class, parameterTypes = [Int::class]) + @UsesReflectionToConstruct(classConstant = KeptClass::class, parameterTypes = [Long::class]) + fun foo(clazz: KClass<KeptClass>?) { + val intConstructor = + clazz?.constructors?.first { + it.parameters.size == 1 && it.parameters.first().type == Int::class.createType() + } + println(intConstructor) + intConstructor?.call(1) + val longConstructor = + clazz?.constructors?.first { + it.parameters.size == 1 && it.parameters.first().type == Long::class.createType() + } + println(longConstructor) + longConstructor?.call(2L) + } +} + +fun main() { + IntAndLongArgsConstructors().foo(if (System.nanoTime() > 0) KeptClass::class else null) +}
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/IntAndLongArgsConstructorsClassName.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/IntAndLongArgsConstructorsClassName.kt new file mode 100644 index 0000000..0737754 --- /dev/null +++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/IntAndLongArgsConstructorsClassName.kt
@@ -0,0 +1,38 @@ +// 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.kt + +import androidx.annotation.keep.UsesReflectionToConstruct +import kotlin.reflect.KClass +import kotlin.reflect.full.createType + +class IntAndLongArgsConstructorsClassName { + + @UsesReflectionToConstruct( + className = "com.android.tools.r8.keepanno.androidx.kt.KeptClass", + parameterTypes = [Int::class], + ) + @UsesReflectionToConstruct( + className = "com.android.tools.r8.keepanno.androidx.kt.KeptClass", + parameterTypes = [Long::class], + ) + fun foo(clazz: KClass<KeptClass>?) { + val intConstructor = + clazz?.constructors?.first { + it.parameters.size == 1 && it.parameters.first().type == Int::class.createType() + } + println(intConstructor) + intConstructor?.call(1) + val longConstructor = + clazz?.constructors?.first { + it.parameters.size == 1 && it.parameters.first().type == Long::class.createType() + } + println(longConstructor) + longConstructor?.call(2L) + } +} + +fun main() { + IntAndLongArgsConstructorsClassName().foo(if (System.nanoTime() > 0) KeptClass::class else null) +}
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 index fb55316..89e2d02 100644 --- 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
@@ -1,3 +1,6 @@ +// 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.kt class KeptClass() { @@ -12,4 +15,8 @@ constructor(l: Long) : this() { println("<init>(Long)") } + + constructor(s: String) : this() { + println("<init>(String)") + } }
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 index 4dd03cc..4e9e3eb 100644 --- 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
@@ -1,3 +1,6 @@ +// 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.kt import androidx.annotation.keep.UsesReflectionToConstruct @@ -5,9 +8,13 @@ import kotlin.reflect.full.primaryConstructor class OnlyNoArgsConstructor { - @UsesReflectionToConstruct(classConstant = KeptClass::class, params = []) + @UsesReflectionToConstruct(classConstant = KeptClass::class, parameterTypes = []) fun foo(clazz: KClass<KeptClass>?) { + println(clazz?.primaryConstructor) clazz?.primaryConstructor?.call() + val noArgsConstructor = clazz?.constructors?.find { it.parameters.isEmpty() } + println(noArgsConstructor) + noArgsConstructor?.call() } }
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 index 8652223..6eb2c08 100644 --- 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
@@ -1,3 +1,6 @@ +// 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.kt import androidx.annotation.keep.UsesReflectionToConstruct @@ -7,10 +10,14 @@ class OnlyNoArgsConstructorClassName { @UsesReflectionToConstruct( className = "com.android.tools.r8.keepanno.androidx.kt.KeptClass", - params = [], + parameterTypes = [], ) fun foo(clazz: KClass<KeptClass>?) { + println(clazz?.primaryConstructor) clazz?.primaryConstructor?.call() + val noArgsConstructor = clazz?.constructors?.find { it.parameters.isEmpty() } + println(noArgsConstructor) + noArgsConstructor?.call() } }
diff --git a/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorWithoutAnnotation.kt b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorWithoutAnnotation.kt new file mode 100644 index 0000000..3ada64f --- /dev/null +++ b/src/test/java/com/android/tools/r8/keepanno/androidx/kt/OnlyNoArgsConstructorWithoutAnnotation.kt
@@ -0,0 +1,22 @@ +// 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.kt + +import kotlin.reflect.KClass +import kotlin.reflect.full.primaryConstructor + +class OnlyNoArgsConstructorWithoutAnnotation { + fun foo(clazz: KClass<KeptClass>?) { + println(clazz?.primaryConstructor) + clazz?.primaryConstructor?.call() + val noArgsConstructor = clazz?.constructors?.find { it.parameters.isEmpty() } + println(noArgsConstructor) + noArgsConstructor?.call() + } +} + +fun main() { + OnlyNoArgsConstructorWithoutAnnotation() + .foo(if (System.nanoTime() > 0) KeptClass::class else null) +}
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java index 3ff6678..e060bbf 100644 --- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java +++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -134,6 +134,7 @@ final String kotlinName; String valueType = null; String valueDefault = null; + boolean suppressKotlinDefaultParameterOrder = false; GroupMember(String name) { this(name, name); @@ -154,6 +155,11 @@ return this; } + public GroupMember setSuppressKotlinDefaultParameterOrder() { + this.suppressKotlinDefaultParameterOrder = true; + return this; + } + @Override public GroupMember self() { return this; @@ -165,12 +171,17 @@ generator.println("@Deprecated"); } if (generator.generateKotlin()) { - if (kotlinValueDefault() == null) { - generator.println("val " + kotlinName + ": " + kotlinValueType() + ","); - } else { - generator.println( - "val " + kotlinName + ": " + kotlinValueType() + " = " + kotlinValueDefault() + ","); + StringBuilder builder = new StringBuilder(); + if (suppressKotlinDefaultParameterOrder) { + builder.append("@Suppress(\"KotlinDefaultParameterOrder\") "); } + builder.append("val ").append(kotlinName).append(": ").append(kotlinValueType()); + if (kotlinValueDefault() == null) { + builder.append(","); + } else { + builder.append(" = ").append(kotlinValueDefault()).append(","); + } + generator.println(builder.toString()); } else { if (valueDefault == null) { generator.println(valueType + " " + name + "();"); @@ -688,22 +699,30 @@ } } - private void printOpenAnnotationClassTargetingClassFieldMethodCtor(String clazz) { + private void printOpenAnnotationClassTargetingClassFieldMethodCtor(ClassReference clazz) { + String unqualifiedName = getUnqualifiedName(clazz); if (generateKotlin()) { println("@Retention(AnnotationRetention.BINARY)"); + if (REPEATABLE_ANNOTATIONS.contains(clazz)) { + println("@Repeatable"); + } println("@Target("); println(" AnnotationTarget.CLASS,"); println(" AnnotationTarget.FIELD,"); println(" AnnotationTarget.FUNCTION,"); println(" AnnotationTarget.CONSTRUCTOR,"); println(")"); - println("public annotation class " + clazz + "("); + println("public annotation class " + unqualifiedName + "("); } else { + if (REPEATABLE_ANNOTATIONS.contains(clazz)) { + throw new RuntimeException( + "Repeatable annotations not supported for Java code generation"); + } println( "@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD," + " ElementType.CONSTRUCTOR})"); println("@Retention(RetentionPolicy.CLASS)"); - println("public @interface " + clazz + " {"); + println("public @interface " + unqualifiedName + " {"); } } @@ -1745,7 +1764,7 @@ "When a member is annotated, the member patterns cannot be used as the annotated" + " member itself fully defines the item to be kept (i.e., itself).") .printDoc(this::println); - printOpenAnnotationClassTargetingClassFieldMethodCtor("KeepForApi"); + printOpenAnnotationClassTargetingClassFieldMethodCtor(KEEP_FOR_API); println(); withIndent( () -> { @@ -1834,7 +1853,7 @@ " // unreachable", " }") .printDoc(this::println); - printOpenAnnotationClassTargetingClassFieldMethodCtor(getUnqualifiedName(USES_REFLECTION)); + printOpenAnnotationClassTargetingClassFieldMethodCtor(USES_REFLECTION); println(); withIndent( () -> { @@ -1861,10 +1880,10 @@ private Group createAndroidXParameterSelection( Consumer<GroupMember> paramsConsumer, Consumer<GroupMember> paramTypeNamesConsumer) { - GroupMember params = new GroupMember("params").defaultUnspecifiedArray(); + GroupMember params = new GroupMember("parameterTypes").defaultUnspecifiedArray(); paramsConsumer.accept(params); GroupMember paramTypeNames = - new GroupMember("paramTypeNames") + new GroupMember("parameterTypeNames") .defaultArrayValue(Reference.classFromClass(String.class), "\"\""); paramTypeNamesConsumer.accept(paramTypeNames); return new Group("constructor-parameters") @@ -1941,9 +1960,7 @@ .addSection("@see UsesReflectionToAccessMethod") .addSection("@see UsesReflectionToAccessField") .printDoc(this::println); - println("@Repeatable"); - printOpenAnnotationClassTargetingClassFieldMethodCtor( - getUnqualifiedName(USES_REFLECTION_TO_CONSTRUCT)); + printOpenAnnotationClassTargetingClassFieldMethodCtor(USES_REFLECTION_TO_CONSTRUCT); println(); withIndent( () -> { @@ -1958,15 +1975,15 @@ "Defines which constructor to keep by specifying the parameter list" + " types.") .addSection( - "If neither `param` nor `paramTypeNames` is specified then" - + " constructors with all parameter lists are kept."), + "If neither `parameterTypes` nor `parameterTypeNames` is specified" + + " then constructors with all parameter lists are kept."), g -> g.setDocTitle( "Defines which constructor to keep by specifying the parameter list" + " types.") .addSection( - "If neither `param` nor `paramTypeNames` is specified then" - + " constructors with all parameter lists are kept.")) + "If neither `parameterTypes` nor `parameterTypeNames` is specified" + + " then constructors with all parameter lists are kept.")) .generate(this); }); printCloseAnnotationClass(); @@ -1990,18 +2007,19 @@ .addSection("@see UsesReflectionToConstruct") .addSection("@see UsesReflectionToAccessField") .printDoc(this::println); - println("@Repeatable"); - printOpenAnnotationClassTargetingClassFieldMethodCtor( - getUnqualifiedName(USES_REFLECTION_TO_ACCESS_METHOD)); + printOpenAnnotationClassTargetingClassFieldMethodCtor(USES_REFLECTION_TO_ACCESS_METHOD); println(); withIndent( () -> { createAndroidXClassSelection( - g -> g.setDocTitle("Class containing the method accessed by reflection."), g -> - g.setDocTitle( - "Class name (or class name pattern) containing the method accessed by" - + " reflection.")) + g.setSuppressKotlinDefaultParameterOrder() + .setDocTitle("Class containing the method accessed by reflection."), + g -> + g.setSuppressKotlinDefaultParameterOrder() + .setDocTitle( + "Class name (or class name pattern) containing the method accessed" + + " by reflection.")) .generate(this); println(); createMethodNameSelection().generate(this); @@ -2012,15 +2030,15 @@ "Defines which method to keep by specifying set of parameter" + " classes passed.") .addSection( - "If neither `param` nor `paramTypeNames` is specified then" - + " methods with all parameter lists are kept."), + "If neither `parameterTypes` nor `parameterTypeNames` is specified" + + " then methods with all parameter lists are kept."), g -> g.setDocTitle( "Defines which method to keep by specifying set of parameter" + " classes passed.") .addSection( - "If neither `param` nor `paramTypeNames` is specified then" - + " methods with all parameter lists are kept.")) + "If neither `parameterTypes` nor `parameterTypeNames` is specified" + + " then methods with all parameter lists are kept.")) .generate(this); println(); createAndroidXReturnTypeSelection().generate(this); @@ -2044,18 +2062,19 @@ + " preserved if the annotated code is reachable in the final application build.") .addSection("@see UsesReflectionToConstruct", "@see UsesReflectionToAccessMethod") .printDoc(this::println); - println("@Repeatable"); - printOpenAnnotationClassTargetingClassFieldMethodCtor( - getUnqualifiedName(USES_REFLECTION_TO_ACCESS_FIELD)); + printOpenAnnotationClassTargetingClassFieldMethodCtor(USES_REFLECTION_TO_ACCESS_FIELD); println(); withIndent( () -> { createAndroidXClassSelection( - g -> g.setDocTitle("Class containing the field accessed by reflection."), g -> - g.setDocTitle( - "Class name (or class name pattern) containing the field accessed by" - + " reflection.")) + g.setSuppressKotlinDefaultParameterOrder() + .setDocTitle("Class containing the field accessed by reflection."), + g -> + g.setSuppressKotlinDefaultParameterOrder() + .setDocTitle( + "Class name (or class name pattern) containing the field accessed" + + " by reflection.")) .generate(this); println(); createAndroidXFieldNameSelection().generate(this); @@ -2090,8 +2109,7 @@ "@see UsesReflectionToAccessMethod", "@see UsesReflectionToAccessField") .printDoc(this::println); - printOpenAnnotationClassTargetingClassFieldMethodCtor( - getUnqualifiedName(UNCONDITIONALLY_KEEP)); + printOpenAnnotationClassTargetingClassFieldMethodCtor(UNCONDITIONALLY_KEEP); println(); withIndent( () -> { @@ -2108,7 +2126,7 @@ printCloseAnnotationClass(); } - private void generateUsedByX(String annotationClassName, String doc) { + private void generateUsedByX(ClassReference clazz, String doc) { printCopyRight(2023); printPackage(); printAnnotationImports(); @@ -2137,7 +2155,7 @@ "When a member is annotated, the member patterns cannot be used as the annotated" + " member itself fully defines the item to be kept (i.e., itself).") .printDoc(this::println); - printOpenAnnotationClassTargetingClassFieldMethodCtor(annotationClassName); + printOpenAnnotationClassTargetingClassFieldMethodCtor(clazz); println(); withIndent( () -> { @@ -2746,15 +2764,16 @@ private static void writeFile( String pkg, Function<Generator, ClassReference> f, - Consumer<Generator> fn, + BiConsumer<Generator, ClassReference> fn, BiConsumer<Path, String> write) throws IOException { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); PrintStream printStream = new PrintStream(byteStream); Generator generator = new Generator(printStream, pkg); - fn.accept(generator); + ClassReference clazz = f.apply(generator); + fn.accept(generator, clazz); String formatted = byteStream.toString(); - Path file = source(f.apply(generator)); + Path file = source(clazz); if (file.toString().endsWith(".kt")) { formatted = CodeGenerationBase.kotlinFormatRawOutput(formatted); } else if (file.toString().endsWith(".java")) { @@ -2783,7 +2802,7 @@ writeFile( ANDROIDX_ANNO_PKG, generator -> generator.ANNOTATION_CONSTANTS, - Generator::generateConstants, + (generator, clazz) -> generator.generateConstants(), write); // Create a copy of the non-generated classes in the androidx namespace. // This is currently disabled. Will potentially be re-introduced by converting these classes @@ -2791,61 +2810,86 @@ // copyNonGeneratedMethods(); for (String pkg : new String[] {R8_ANNO_PKG, ANDROIDX_ANNO_PKG}) { writeFile( - pkg, generator -> generator.STRING_PATTERN, Generator::generateStringPattern, write); - writeFile(pkg, generator -> generator.TYPE_PATTERN, Generator::generateTypePattern, write); + pkg, + generator -> generator.STRING_PATTERN, + (generator, clazz) -> generator.generateStringPattern(), + write); + writeFile( + pkg, + generator -> generator.TYPE_PATTERN, + (generator, clazz) -> generator.generateTypePattern(), + write); writeFile( pkg, generator -> generator.CLASS_NAME_PATTERN, - Generator::generateClassNamePattern, + (generator, clazz) -> generator.generateClassNamePattern(), write); writeFile( pkg, generator -> generator.INSTANCE_OF_PATTERN, - Generator::generateInstanceOfPattern, + (generator, clazz) -> generator.generateInstanceOfPattern(), write); writeFile( pkg, generator -> generator.ANNOTATION_PATTERN, - Generator::generateAnnotationPattern, + (generator, clazz) -> generator.generateAnnotationPattern(), write); - writeFile(pkg, generator -> generator.KEEP_BINDING, Generator::generateKeepBinding, write); - writeFile(pkg, generator -> generator.KEEP_TARGET, Generator::generateKeepTarget, write); writeFile( - pkg, generator -> generator.KEEP_CONDITION, Generator::generateKeepCondition, write); - writeFile(pkg, generator -> generator.KEEP_FOR_API, Generator::generateKeepForApi, write); + pkg, + generator -> generator.KEEP_BINDING, + (generator, clazz) -> generator.generateKeepBinding(), + write); writeFile( - pkg, generator -> generator.USES_REFLECTION, Generator::generateUsesReflection, write); + pkg, + generator -> generator.KEEP_TARGET, + (generator, clazz) -> generator.generateKeepTarget(), + write); + writeFile( + pkg, + generator -> generator.KEEP_CONDITION, + (generator, clazz) -> generator.generateKeepCondition(), + write); + writeFile( + pkg, + generator -> generator.KEEP_FOR_API, + (generator, clazz) -> generator.generateKeepForApi(), + write); + writeFile( + pkg, + generator -> generator.USES_REFLECTION, + (generator, clazz) -> generator.generateUsesReflection(), + write); writeFile( pkg, generator -> generator.USED_BY_REFLECTION, - generator -> generator.generateUsedByX("UsedByReflection", "accessed reflectively"), + (generator, clazz) -> generator.generateUsedByX(clazz, "accessed reflectively"), write); writeFile( pkg, generator -> generator.USED_BY_NATIVE, - generator -> - generator.generateUsedByX("UsedByNative", "accessed from native code via JNI"), + (generator, clazz) -> + generator.generateUsedByX(clazz, "accessed from native code via JNI"), write); } writeFile( ANDROIDX_ANNO_PKG, generator -> generator.USES_REFLECTION_TO_CONSTRUCT, - Generator::generateUsesReflectionToConstruct, + (generator, clazz) -> generator.generateUsesReflectionToConstruct(), write); writeFile( ANDROIDX_ANNO_PKG, generator -> generator.USES_REFLECTION_TO_ACCESS_METHOD, - Generator::generateUsesReflectionToAccessMethod, + (generator, clazz) -> generator.generateUsesReflectionToAccessMethod(), write); writeFile( ANDROIDX_ANNO_PKG, generator -> generator.USES_REFLECTION_TO_ACCESS_FIELD, - Generator::generateUsesReflectionToAccessField, + (generator, clazz) -> generator.generateUsesReflectionToAccessField(), write); writeFile( ANDROIDX_ANNO_PKG, generator -> generator.UNCONDITIONALLY_KEEP, - Generator::generateUnconditionallyKeep, + (generator, clazz) -> generator.generateUnconditionallyKeep(), write); } }
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java new file mode 100644 index 0000000..e87a04d --- /dev/null +++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingClasspathSplitTest.java
@@ -0,0 +1,143 @@ +// 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.memberrebinding; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestBuilder; +import com.android.tools.r8.TestCompileResult; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.ThrowableConsumer; +import com.android.tools.r8.memberrebinding.testclasses.MemberRebindingClasspathSplitTestClasses; +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; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class MemberRebindingClasspathSplitTest extends TestBase { + @Parameter(0) + public TestParameters parameters; + + private static class TestConfig { + private final String desc; + + private final ThrowableConsumer<? super TestBuilder<?, ?>> addToClasspath; + private final ThrowableConsumer<? super TestBuilder<?, ?>> addToProgrampath; + private final ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath; + + private TestConfig( + String desc, + ThrowableConsumer<? super TestBuilder<?, ?>> addToClasspath, + ThrowableConsumer<? super TestBuilder<?, ?>> addToProgrampath, + ThrowableConsumer<? super TestCompileResult<?, ?>> addToRunClasspath) { + this.desc = desc; + this.addToClasspath = addToClasspath; + this.addToProgrampath = addToProgrampath; + this.addToRunClasspath = addToRunClasspath; + } + + @Override + public String toString() { + return desc; + } + } + + @Parameter(1) + public TestConfig split; + + @Parameters(name = "{0}, classpathsplit: {1}") + public static List<Object[]> data() { + return buildParameters( + getTestParameters().withAllRuntimesAndApiLevels().build(), + ImmutableList.of( + new TestConfig( + "Both A and B on classpath", + b -> { + b.addClasspathClasses( + MemberRebindingClasspathSplitTestClasses.getA(), + MemberRebindingClasspathSplitTestClasses.getB()); + }, + b -> {}, + b -> { + b.addRunClasspathClasses( + MemberRebindingClasspathSplitTestClasses.getA(), + MemberRebindingClasspathSplitTestClasses.getB()); + }), + new TestConfig( + "Both A and B on classpath, m() bridge removed from B", + b -> { + b.addClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA()); + b.addClasspathClassFileData( + transformer(MemberRebindingClasspathSplitTestClasses.getB()) + .removeMethodsWithName("m") + .transform()); + }, + b -> {}, + b -> { + b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA()); + b.addRunClasspathClassFileData( + transformer(MemberRebindingClasspathSplitTestClasses.getB()) + .removeMethodsWithName("m") + .transform()); + }), + new TestConfig( + "A on classpath and B on programpath", + b -> { + b.addClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA()); + }, + b -> { + b.addProgramClasses(MemberRebindingClasspathSplitTestClasses.getB()); + }, + b -> { + b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA()); + }), + new TestConfig( + "A on classpath and B on programpath, m() bridge removed from B", + b -> { + b.addClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA()); + }, + b -> { + b.addProgramClassFileData( + transformer(MemberRebindingClasspathSplitTestClasses.getB()) + .removeMethodsWithName("m") + .transform()); + }, + b -> { + b.addRunClasspathClasses(MemberRebindingClasspathSplitTestClasses.getA()); + }))); + } + + @Test + public void testR8() throws Exception { + testForR8(parameters) + .addInnerClasses(getClass()) + .apply(split.addToClasspath) + .apply(split.addToProgrampath) + .allowStdoutMessages() + .addDontObfuscate() + .addDontOptimize() + .addKeepRules("-keep class " + Main.class.getTypeName() + " { void main(...); }") + .compile() + .apply(split.addToRunClasspath) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("A", "A"); + } + + public static class C extends MemberRebindingClasspathSplitTestClasses.B { + public String superm() { + return super.m(); + } + } + + public static class Main { + public static void main(String[] strArr) { + C c = new C(); + System.out.println(c.m()); + System.out.println(c.superm()); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/testclasses/MemberRebindingClasspathSplitTestClasses.java b/src/test/java/com/android/tools/r8/memberrebinding/testclasses/MemberRebindingClasspathSplitTestClasses.java new file mode 100644 index 0000000..5c0d27c --- /dev/null +++ b/src/test/java/com/android/tools/r8/memberrebinding/testclasses/MemberRebindingClasspathSplitTestClasses.java
@@ -0,0 +1,22 @@ +// 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.memberrebinding.testclasses; + +public class MemberRebindingClasspathSplitTestClasses { + public static Class<?> getA() { + return A.class; + } + + public static Class<?> getB() { + return B.class; + } + + static class A { + public String m() { + return "A"; + } + } + + public static class B extends A {} +}
diff --git a/src/test/java/com/android/tools/r8/optimize/listiteration/ListIterationRewriterTest.java b/src/test/java/com/android/tools/r8/optimize/listiteration/ListIterationRewriterTest.java index 96e2020..3caf31f 100644 --- a/src/test/java/com/android/tools/r8/optimize/listiteration/ListIterationRewriterTest.java +++ b/src/test/java/com/android/tools/r8/optimize/listiteration/ListIterationRewriterTest.java
@@ -4,12 +4,14 @@ package com.android.tools.r8.optimize.listiteration; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import com.android.tools.r8.NeverInline; import com.android.tools.r8.TestBase; import com.android.tools.r8.TestParameters; import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.utils.ListUtils; import com.android.tools.r8.utils.codeinspector.FoundClassSubject; import com.android.tools.r8.utils.codeinspector.InstructionSubject; import java.util.ArrayList; @@ -548,16 +550,34 @@ } @Test - public void testRewrites() throws Exception { + public void testRewritesD8() throws Exception { + parameters.assumeDexRuntime(); // Run all test classes in one compilation for faster tests compared to compiling each one // separately. - Class[] classes = - Arrays.stream(RewritesMain.CASES).map(x -> x.getClass()).toArray(Class[]::new); - String[] expectedOutput = getExpectedTestOutput(RewritesMain.CASES); + testForD8(parameters) + .addProgramClassesAndInnerClasses(ListUtils.map(RewritesMain.CASES, TestCase::getClass)) + .addProgramClasses(RewritesMain.class, TestCase.class) + .addOptionsModification( + o -> { + assertFalse(o.testing.listIterationRewritingRewriteCustomIterators); + o.testing.listIterationRewritingRewriteCustomIterators = true; + assertFalse(o.testing.listIterationRewritingRewriteInterfaces); + o.testing.listIterationRewritingRewriteInterfaces = true; + }) + .release() + .compile() + .run(parameters.getRuntime(), RewritesMain.class) + .assertSuccessWithOutputLines(getExpectedTestOutput(RewritesMain.CASES)) + .inspect( + inspector -> inspector.forAllClasses(ListIterationRewriterTest::checkNoIteratorInvoke)); + } - testForR8(parameters.getBackend()) - .setMinApi(parameters) - .addProgramClassesAndInnerClasses(classes) + @Test + public void testRewritesR8() throws Exception { + // Run all test classes in one compilation for faster tests compared to compiling each one + // separately. + testForR8(parameters) + .addProgramClassesAndInnerClasses(ListUtils.map(RewritesMain.CASES, TestCase::getClass)) .addProgramClasses(RewritesMain.class, TestCase.class) .addKeepMainRule(RewritesMain.class) .addDontObfuscate() @@ -565,7 +585,7 @@ .enableInliningAnnotations() .compile() .run(parameters.getRuntime(), RewritesMain.class) - .assertSuccessWithOutputLines(expectedOutput) + .assertSuccessWithOutputLines(getExpectedTestOutput(RewritesMain.CASES)) .inspect( inspector -> inspector.forAllClasses(ListIterationRewriterTest::checkNoIteratorInvoke)); } @@ -620,21 +640,39 @@ } @Test - public void testNoRewrites() throws Exception { + public void testNoRewritesD8() throws Exception { + parameters.assumeDexRuntime(); // Run all test classes in one compilation for faster tests compared to compiling each one // separately. - Class[] classes = - Arrays.stream(NoRewritesMain.CASES).map(x -> x.getClass()).toArray(Class[]::new); - String[] expectedOutput = getExpectedTestOutput(NoRewritesMain.CASES); + testForD8(parameters) + .addOptionsModification( + o -> { + // Ensure tests do not pass due to the optimization being disabled. + assertFalse(o.testing.listIterationRewritingRewriteCustomIterators); + o.testing.listIterationRewritingRewriteCustomIterators = true; + assertFalse(o.testing.listIterationRewritingRewriteInterfaces); + o.testing.listIterationRewritingRewriteInterfaces = true; + }) + .addProgramClassesAndInnerClasses(ListUtils.map(NoRewritesMain.CASES, TestCase::getClass)) + .addProgramClasses(NoRewritesMain.class, TestCase.class) + .release() + .compile() + .run(parameters.getRuntime(), NoRewritesMain.class) + .assertSuccessWithOutputLines(getExpectedTestOutput(NoRewritesMain.CASES)) + .inspect( + inspector -> + inspector.forAllClasses(ListIterationRewriterTest::checkIteratorInvokeExists)); + } - testForR8(parameters.getBackend()) - .setMinApi(parameters) + @Test + public void testNoRewritesR8() throws Exception { + testForR8(parameters) .addOptionsModification( o -> { // Ensure tests do not pass due to the optimization being disabled. o.testing.listIterationRewritingRewriteCustomIterators = true; }) - .addProgramClassesAndInnerClasses(classes) + .addProgramClassesAndInnerClasses(ListUtils.map(NoRewritesMain.CASES, TestCase::getClass)) .addProgramClasses(NoRewritesMain.class, TestCase.class) .addKeepMainRule(NoRewritesMain.class) .addDontObfuscate() @@ -642,7 +680,7 @@ .noHorizontalClassMerging() .compile() .run(parameters.getRuntime(), NoRewritesMain.class) - .assertSuccessWithOutputLines(expectedOutput) + .assertSuccessWithOutputLines(getExpectedTestOutput(NoRewritesMain.CASES)) .inspect( inspector -> inspector.forAllClasses(ListIterationRewriterTest::checkIteratorInvokeExists));
diff --git a/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java b/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java index 9da5d33..c11e2a0 100644 --- a/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java +++ b/src/test/java/com/android/tools/r8/regress/b426351560/Regress426351560Test.java
@@ -31,8 +31,6 @@ @Test public void testR8() throws Exception { - // TODO(b/427887773) - assumeFalse(parameters.isRandomPartialCompilation()); testForR8(parameters) .addInnerClasses(Regress426351560TestClasses.class, getClass()) .addKeepRules("-keep class " + Main.class.getTypeName() + " { *; }")
diff --git a/src/test/java/com/android/tools/r8/regress/b426351560/Regress427887773Test.java b/src/test/java/com/android/tools/r8/regress/b426351560/Regress427887773Test.java index 67019be..a15a998 100644 --- a/src/test/java/com/android/tools/r8/regress/b426351560/Regress427887773Test.java +++ b/src/test/java/com/android/tools/r8/regress/b426351560/Regress427887773Test.java
@@ -3,16 +3,10 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.regress.b426351560; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertTrue; - -import com.android.tools.r8.CompilationFailedException; import com.android.tools.r8.TestBase; import com.android.tools.r8.TestParameters; import com.android.tools.r8.TestParametersCollection; import com.android.tools.r8.regress.b426351560.testclasses.Regress426351560TestClasses; -import com.android.tools.r8.utils.AndroidApiLevel; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -32,29 +26,14 @@ @Test public void testR8() throws Exception { - try { - testForR8(parameters) - .addInnerClasses(getClass()) - .addClasspathClasses(Regress426351560TestClasses.getClasses()) - .addKeepRules("-keep class " + Main.class.getTypeName() + " { *; }") - .compile() - .addRunClasspathClasses(Regress426351560TestClasses.getClasses()) - .run(parameters.getRuntime(), Main.class) - .applyIf( - parameters.isDexRuntime() - && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N), - rr -> rr.assertFailureWithErrorThatThrows(IllegalAccessError.class), - // for jdk based tests: java.lang.VerifyError: Bad invokespecial instruction: - // interface method reference is in an indirect superinterface. - rr -> rr.assertFailureWithErrorThatThrows(VerifyError.class)); - } catch (CompilationFailedException e) { - if (parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.N)) { - assertTrue(e.getCause() instanceof AssertionError); - assertThat(e.getCause().getMessage(), containsString("CC was already present.")); - } else { - throw e; - } - } + testForR8(parameters) + .addInnerClasses(getClass()) + .addClasspathClasses(Regress426351560TestClasses.getClasses()) + .addKeepRules("-keep class " + Main.class.getTypeName() + " { *; }") + .compile() + .addRunClasspathClasses(Regress426351560TestClasses.getClasses()) + .run(parameters.getRuntime(), Main.class) + .assertSuccessWithOutputLines("Hello, world!"); } public static class Main extends Regress426351560TestClasses.A {
diff --git a/src/test/testbase/java/com/android/tools/r8/ProguardVersion.java b/src/test/testbase/java/com/android/tools/r8/ProguardVersion.java index 5d75149..569e22a 100644 --- a/src/test/testbase/java/com/android/tools/r8/ProguardVersion.java +++ b/src/test/testbase/java/com/android/tools/r8/ProguardVersion.java
@@ -11,8 +11,7 @@ public enum ProguardVersion { V7_0_0("7.0.0"), - V7_3_2("7.3.2"), - V7_4_1("7.4.1"); + V7_7_0("7.7.0"); private final String version; @@ -21,7 +20,7 @@ } public static ProguardVersion getLatest() { - return V7_4_1; + return V7_7_0; } public Path getProguardScript() {
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBase.java b/src/test/testbase/java/com/android/tools/r8/TestBase.java index aa40c91..f95cbc1 100644 --- a/src/test/testbase/java/com/android/tools/r8/TestBase.java +++ b/src/test/testbase/java/com/android/tools/r8/TestBase.java
@@ -2210,13 +2210,9 @@ } protected void assertListsAreEqual(List<String> expected, List<String> actual) { - int minLines = Math.min(expected.size(), actual.size()); - for (int i = 0; i < minLines; i++) { - assertEquals(expected.get(i), actual.get(i)); - } - if (expected.size() != actual.size()) { - assertEquals( - "", expected.size() > actual.size() ? expected.get(minLines) : actual.get(minLines)); + assertEquals("List sizes differ: ", expected.size(), actual.size()); + for (int i = 0; i < expected.size(); i++) { + assertEquals("Index: " + i, expected.get(i), actual.get(i)); } }
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 36968f6..101a316 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
@@ -36,7 +36,7 @@ public class KeepAnnoTestUtils { - public static ProguardVersion PG_VERSION = ProguardVersion.V7_4_1; + public static ProguardVersion PG_VERSION = ProguardVersion.V7_7_0; public static final String DESCRIPTOR_PREFIX = "Landroidx/annotation/keep/"; public static final String DESCRIPTOR_LEGACY_PREFIX =
diff --git a/src/test/testbase/java/com/android/tools/r8/profile/art/utils/ArtProfileInspector.java b/src/test/testbase/java/com/android/tools/r8/profile/art/utils/ArtProfileInspector.java index fb96468..6666333 100644 --- a/src/test/testbase/java/com/android/tools/r8/profile/art/utils/ArtProfileInspector.java +++ b/src/test/testbase/java/com/android/tools/r8/profile/art/utils/ArtProfileInspector.java
@@ -4,7 +4,6 @@ package com.android.tools.r8.profile.art.utils; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -123,6 +122,11 @@ } public ArtProfileInspector inspectMethodRule( + MethodSubject methodSubject, Consumer<ArtProfileMethodRuleInspector> inspector) { + return inspectMethodRule(methodSubject.getFinalReference(), inspector); + } + + public ArtProfileInspector inspectMethodRule( MethodReference methodReference, Consumer<ArtProfileMethodRuleInspector> inspector) { assertContainsMethodRule(methodReference); ExternalArtProfileMethodRule methodRule = artProfile.getMethodRule(methodReference);
diff --git a/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java b/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java index f73a896..753027f 100644 --- a/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java +++ b/src/test/testbase/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
@@ -52,6 +52,11 @@ InterfaceDesugaringForTesting.getCompanionClassDescriptor(clazz.getDescriptor())); } + public static ClassReference syntheticClassWithMinimalName(Class<?> clazz, int id) { + return SyntheticNaming.makeMinimalSyntheticReferenceForTest( + Reference.classFromClass(clazz), Integer.toString(id)); + } + private static ClassReference syntheticClass(Class<?> clazz, SyntheticKind kind, int id) { return syntheticClass(Reference.classFromClass(clazz), kind, id); }
diff --git a/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java index 143a44d..424e8c5 100644 --- a/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java +++ b/src/test/testbase/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -1772,4 +1772,86 @@ } }); } + + public static class AnnotationBuilder { + private final AnnotationVisitor av; + + private AnnotationBuilder(AnnotationVisitor av) { + this.av = av; + } + + public AnnotationBuilder setField(String name, Object value) { + if (value instanceof Class) { + av.visit(name, Type.getType((Class<?>) value)); + } else { + av.visit(name, value); + } + return this; + } + + public AnnotationBuilder setArray(String name, Object... values) { + AnnotationVisitor subAv = av.visitArray(name); + Arrays.stream(values).forEach(value -> subAv.visit(null, value)); + return this; + } + } + + public ClassFileTransformer setAnnotation( + Class<?> annotationClass, Consumer<AnnotationBuilder> annotationBuilderConsumer) { + return addClassTransformer( + new ClassTransformer() { + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + AnnotationVisitor av = + super.visitAnnotation( + Reference.classFromClass(annotationClass).getDescriptor(), false); + annotationBuilderConsumer.accept(new AnnotationBuilder(av)); + av.visitEnd(); + } + + @Override + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + return null; + // Remove all other annotations. + } + }); + } + + public ClassFileTransformer setAnnotation( + MethodPredicate predicate, + Class<?> annotationClass, + Consumer<AnnotationBuilder> annotationBuilderConsumer) { + return addClassTransformer( + new ClassTransformer() { + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + if (predicate.test(access, name, descriptor, signature, exceptions)) { + AnnotationVisitor av = + mv.visitAnnotation( + Reference.classFromClass(annotationClass).getDescriptor(), false); + annotationBuilderConsumer.accept(new AnnotationBuilder(av)); + av.visitEnd(); + return new MethodVisitor(ASM_VERSION, mv) { + @Override + public AnnotationVisitor visitAnnotation( + final String descriptor, final boolean visible) { + return null; + // Remove all other annotations. + } + }; + } + return mv; + } + }); + } }
diff --git a/third_party/chrome/monochrome_public_minimal_apks/chrome_200520.tar.gz.sha1 b/third_party/chrome/monochrome_public_minimal_apks/chrome_200520.tar.gz.sha1 deleted file mode 100644 index a4d4e8f..0000000 --- a/third_party/chrome/monochrome_public_minimal_apks/chrome_200520.tar.gz.sha1 +++ /dev/null
@@ -1 +0,0 @@ -4a9de4ba961c2f0c953c0a88675f29959e8602a7 \ No newline at end of file
diff --git a/third_party/proguard/README.google b/third_party/proguard/README.google index 4501b28..e510b61 100644 --- a/third_party/proguard/README.google +++ b/third_party/proguard/README.google
@@ -1,9 +1,8 @@ URL: https://github.com/Guardsquare/proguard/releases/download/v7.0.0/proguard-7.0.0.tar.gz -URL: https://github.com/Guardsquare/proguard/releases/download/v7.3.2/proguard-7.3.2.tar.gz -URL: https://github.com/Guardsquare/proguard/releases/download/v7.4.1/proguard-7.4.1.tar.gz -Version: 7.0.0, 7.3.2, 7.4.1 +URL: https://github.com/Guardsquare/proguard/releases/download/v7.4.1/proguard-7.7.0.tar.gz +Version: 7.0.0, 7.7.0 License: GPL -License File: proguard-7.0.0/docs/license.md, proguard-7.3.2/LICENSE, proguard-7.4.1/LICENSE +License File: proguard-7.0.0/docs/license.md, proguard-7.7.0/LICENSE Description: ProGuard Java Optimizer and Obfuscator
diff --git a/third_party/proguard/proguard-7.3.2.tar.gz.sha1 b/third_party/proguard/proguard-7.3.2.tar.gz.sha1 deleted file mode 100644 index 8eafeac..0000000 --- a/third_party/proguard/proguard-7.3.2.tar.gz.sha1 +++ /dev/null
@@ -1 +0,0 @@ -87ab4bba457938929d5a5cafb579a8aa7630b428 \ No newline at end of file
diff --git a/third_party/proguard/proguard-7.4.1.tar.gz.sha1 b/third_party/proguard/proguard-7.4.1.tar.gz.sha1 deleted file mode 100644 index 4c33fe2..0000000 --- a/third_party/proguard/proguard-7.4.1.tar.gz.sha1 +++ /dev/null
@@ -1 +0,0 @@ -95f547ec8e6d15338fb6c7c32cc7cf0b27e049d6 \ No newline at end of file
diff --git a/third_party/proguard/proguard-7.7.0.tar.gz.sha1 b/third_party/proguard/proguard-7.7.0.tar.gz.sha1 new file mode 100644 index 0000000..2ebfe2c --- /dev/null +++ b/third_party/proguard/proguard-7.7.0.tar.gz.sha1
@@ -0,0 +1 @@ +16e41bfab4da5ebf3e8cc612382a5a38d1770714 \ No newline at end of file
diff --git a/tools/update-androidx-keep-annotations.py b/tools/update-androidx-keep-annotations.py index 29bf464..3f5da7c 100755 --- a/tools/update-androidx-keep-annotations.py +++ b/tools/update-androidx-keep-annotations.py
@@ -15,10 +15,17 @@ def parse_options(): parser = argparse.ArgumentParser(description='Update androidx keep annotations') - parser.add_argument('--androidx', - metavar=('<path>'), - required=True, - help='Path to the androidx checkout') + parser.add_argument( + '--androidx', + metavar=('<path>'), + required=True, + help='Path to the androidx checkout') + parser.add_argument( + '--dry-run', + '--dry_run', + help="Don't copy, just print what to copy", + default=False, + action='store_true') return parser.parse_args() @@ -59,9 +66,22 @@ .format(filename=os.path.join(root, filename))) sys.exit(1) - shutil.copyfile( - os.path.join(root, filename), - os.path.join(dest_dir, filename)) + files = ( + 'UnconditionallyKeep.kt', + 'Unspecified.kt', + 'UsesReflectionToAccessField.kt', + 'UsesReflectionToAccessMethod.kt', + 'UsesReflectionToConstruct.kt', + ) + if not filename in files: + print('Skipping {filename}'.format(filename=filename)) + continue + + src = os.path.join(root, filename) + dest = os.path.join(dest_dir, filename) + print("Copying '{src}' to '{dest}'".format(src=src, dest=dest)) + if not args.dry_run: + shutil.copyfile(src, dest) if __name__ == '__main__':