Merge commit 'ca83a4d86859229a3c1e5fe938125342931d5e77' into dev-release
Change-Id: Idf053ded2dd8d9b3b363a251b4439a1cd1ff1135
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index 4f7665a..c41aa8b 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -31,7 +31,7 @@
-## Introduction<a id="introduction"></a>
+## Introduction<a name="introduction"></a>
When using a Java/Kotlin shrinker such as R8 or Proguard, developers must inform
the shrinker about parts of the program that are used either externally from the
@@ -50,7 +50,7 @@
independent from keep rules and have a hopefully more clear and direct meaning.
-## Build configuration<a id="build-configuration"></a>
+## Build configuration<a name="build-configuration"></a>
To use the keep annotations your build must include the library of
annotations. It is currently built as part of each R8 build and if used with R8,
@@ -77,7 +77,7 @@
```
-## Annotating code using reflection<a id="using-reflection"></a>
+## Annotating code using reflection<a name="using-reflection"></a>
The keep annotation library defines a family of annotations depending on your
use case. You should generally prefer [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) where applicable.
@@ -85,7 +85,7 @@
of such use cases are detailed below.
-### Invoking methods<a id="using-reflection-methods"></a>
+### Invoking methods<a name="using-reflection-methods"></a>
For example, if your program is reflectively invoking a method, you
should annotate the method that is doing the reflection. The annotation must describe the
@@ -117,7 +117,7 @@
-### Accessing fields<a id="using-reflection-fields"></a>
+### Accessing fields<a name="using-reflection-fields"></a>
For example, if your program is reflectively accessing the fields on a class, you should
annotate the method that is doing the reflection.
@@ -152,17 +152,153 @@
-## Annotating code used by reflection (or via JNI)<a id="used-by-reflection"></a>
+## Annotating code used by reflection (or via JNI)<a name="used-by-reflection"></a>
-TODO
+Sometimes reflecting code cannot be annotated. For example, the reflection can
+be done in native code or in a library outside your control. In such cases you
+can annotate the code that is being used by reflection with either
+[@UsedByReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html) or [@UsedByNative](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByNative.html). These two annotations are equivalent.
+Use the one that best matches why the annotation is needed.
+
+Let's consider some code with reflection outside our control.
+For example, the same field printing as in the above example might be part of a library.
+
+In this example, the `MyClassWithFields` is a class you are passing to the
+field-printing utility of the library. Since the library is reflectively accessing each field
+we annotate them with the [@UsedByReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html) annotation.
+
+We could additionally add the [@UsedByReflection.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#constraints()) property as we did previously.
+We elide it here for brevity.
-## Annotating APIs<a id="apis"></a>
+```
+public class MyClassWithFields implements PrintableFieldInterface {
+ @UsedByReflection final int intField = 42;
-TODO
+ @UsedByReflection String stringField = "Hello!";
+}
+
+public static void run() throws Exception {
+ new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
+}
+```
-## Migrating rules to annotations<a id="migrating-rules"></a>
+Rather than annotate the individual fields we can annotate the holder and add a specification
+similar to the [@KeepTarget](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepTarget.html). The [@UsedByReflection.kind](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#kind()) specifies that only the fields are
+used reflectively. In particular, the "field printer" example we are considering here does not
+make reflective assumptions about the holder class, so we should not constrain it.
+
+To be more precise let's add the [@UsedByReflection.constraints](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#constraints()) property now. This specifies
+that the fields are looked up, their names are used/assumed and their values are read.
+
+
+```
+@UsedByReflection(
+ kind = KeepItemKind.ONLY_FIELDS,
+ constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+public class MyClassWithFields implements PrintableFieldInterface {
+ final int intField = 42;
+ String stringField = "Hello!";
+}
+```
+
+
+Our use of [@UsedByReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html) is still not as flexible as the original [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html). In
+particular, if we change our code to no longer have any call to the library method
+`printFieldValues` the shrinker will still keep all of the fields on our annotated class.
+
+This is because the [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) implicitly encodes as a precondition that the annotated
+method is actually used in the program. If not, the [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) annotation is not
+"active".
+
+Luckily we can specify the same precondition using [@UsedByReflection.preconditions](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsedByReflection.html#preconditions()).
+
+
+```
+@UsedByReflection(
+ preconditions = {
+ @KeepCondition(
+ classConstant = FieldValuePrinterLibrary.class,
+ methodName = "printFieldValues")
+ },
+ kind = KeepItemKind.ONLY_FIELDS,
+ constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+public class MyClassWithFields implements PrintableFieldInterface {
+ final int intField = 42;
+ String stringField = "Hello!";
+}
+```
+
+
+
+## Annotating APIs<a name="apis"></a>
+
+If your code is being shrunk before release as a library, or if you have an API
+surface that is used via dynamic loading at runtime, then you need to keep the
+API surface. For that you should use the [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) annotation.
+
+When annotating a class the default for [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) is to keep the class as well as all of its
+public and protected members:
+
+
+```
+@KeepForApi
+public class MyApi {
+ public void thisPublicMethodIsKept() {
+ /* ... */
+ }
+
+ protected void thisProtectedMethodIsKept() {
+ /* ... */
+ }
+
+ void thisPackagePrivateMethodIsNotKept() {
+ /* ... */
+ }
+
+ private void thisPrivateMethodIsNotKept() {
+ /* ... */
+ }
+}
+```
+
+
+The default can be changed using the [@KeepForApi.memberAccess](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html#memberAccess()) property:
+
+
+```
+@KeepForApi(
+ memberAccess = {
+ MemberAccessFlags.PUBLIC,
+ MemberAccessFlags.PROTECTED,
+ MemberAccessFlags.PACKAGE_PRIVATE
+ })
+```
+
+
+The [@KeepForApi](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/KeepForApi.html) annotation can also be placed directly on members and avoid keeping
+unannotated members. The holder class is implicitly kept. When annotating the members
+directly, the access does not matter as illustrated here by annotating a package private method:
+
+
+```
+public class MyOtherApi {
+
+ public void notKept() {
+ /* ... */
+ }
+
+ @KeepForApi
+ void isKept() {
+ /* ... */
+ }
+}
+```
+
+
+
+## Migrating rules to annotations<a name="migrating-rules"></a>
There is no automatic migration of keep rules. Keep annotations often invert the
direction and rules have no indication of where the reflection is taking
@@ -202,7 +338,7 @@
-## My use case is not covered!<a id="other-uses"></a>
+## My use case is not covered!<a name="other-uses"></a>
The annotation library is in active development and not all use cases are
described here or supported. Reach out to the R8 team by
@@ -210,7 +346,7 @@
Describe your use case and we will look at how best to support it.
-## Troubleshooting<a id="troubleshooting"></a>
+## Troubleshooting<a name="troubleshooting"></a>
If an annotation is not working as expected it may be helpful to inspect the
rules that have been extracted for the annotation. This can be done by
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index 6e3efde..104bf02 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -86,12 +86,43 @@
## [Annotating code used by reflection (or via JNI)](used-by-reflection)
-TODO
+Sometimes reflecting code cannot be annotated. For example, the reflection can
+be done in native code or in a library outside your control. In such cases you
+can annotate the code that is being used by reflection with either
+`@UsedByReflection` or `@UsedByNative`. These two annotations are equivalent.
+Use the one that best matches why the annotation is needed.
+
+Let's consider some code with reflection outside our control.
+[[[INCLUDE DOC:UsedByReflectionFieldPrinterOnFields]]]
+
+[[[INCLUDE CODE:UsedByReflectionFieldPrinterOnFields]]]
+
+[[[INCLUDE DOC:UsedByReflectionFieldPrinterOnClass]]]
+
+[[[INCLUDE CODE:UsedByReflectionFieldPrinterOnClass]]]
+
+[[[INCLUDE DOC:UsedByReflectionFieldPrinterConditional]]]
+
+[[[INCLUDE CODE:UsedByReflectionFieldPrinterConditional]]]
## [Annotating APIs](apis)
-TODO
+If your code is being shrunk before release as a library, or if you have an API
+surface that is used via dynamic loading at runtime, then you need to keep the
+API surface. For that you should use the `@KeepForApi` annotation.
+
+[[[INCLUDE DOC:ApiClass]]]
+
+[[[INCLUDE CODE:ApiClass]]]
+
+[[[INCLUDE DOC:ApiClassMemberAccess]]]
+
+[[[INCLUDE CODE:ApiClassMemberAccess]]]
+
+[[[INCLUDE DOC:ApiMember]]]
+
+[[[INCLUDE CODE:ApiMember]]]
## [Migrating rules to annotations](migrating-rules)
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
index b98f4ab..177990e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepBinding.java
@@ -431,11 +431,27 @@
* <p>If none, and other properties define this item as a field, the default matches any field
* name.
*
+ * <p>Mutually exclusive with the property `fieldNamePattern` also defining field-name.
+ *
* @return The exact field name of the field.
*/
String fieldName() default "";
/**
+ * Define the field-name pattern by a string pattern.
+ *
+ * <p>Mutually exclusive with all method properties.
+ *
+ * <p>If none, and other properties define this item as a field, the default matches any field
+ * name.
+ *
+ * <p>Mutually exclusive with the property `fieldName` also defining field-name.
+ *
+ * @return The string pattern of the field name.
+ */
+ StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+ /**
* Define the field-type pattern by a fully qualified type.
*
* <p>Mutually exclusive with all method properties.
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
index bab5ca7..a35d08b 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepCondition.java
@@ -432,13 +432,37 @@
* <p>If none, and other properties define this item as a field, the default matches any field
* name.
*
- * <p>Mutually exclusive with the property `memberFromBinding` also defining field-name.
+ * <p>Mutually exclusive with the following other properties defining field-name:
+ *
+ * <ul>
+ * <li>fieldNamePattern
+ * <li>memberFromBinding
+ * </ul>
*
* @return The exact field name of the field.
*/
String fieldName() default "";
/**
+ * Define the field-name pattern by a string pattern.
+ *
+ * <p>Mutually exclusive with all method properties.
+ *
+ * <p>If none, and other properties define this item as a field, the default matches any field
+ * name.
+ *
+ * <p>Mutually exclusive with the following other properties defining field-name:
+ *
+ * <ul>
+ * <li>fieldName
+ * <li>memberFromBinding
+ * </ul>
+ *
+ * @return The string pattern of the field name.
+ */
+ StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+ /**
* Define the field-type pattern by a fully qualified type.
*
* <p>Mutually exclusive with all method properties.
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
index 9d097f0..4b56b2e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepForApi.java
@@ -210,11 +210,27 @@
* <p>If none, and other properties define this item as a field, the default matches any field
* name.
*
+ * <p>Mutually exclusive with the property `fieldNamePattern` also defining field-name.
+ *
* @return The exact field name of the field.
*/
String fieldName() default "";
/**
+ * Define the field-name pattern by a string pattern.
+ *
+ * <p>Mutually exclusive with all method properties.
+ *
+ * <p>If none, and other properties define this item as a field, the default matches any field
+ * name.
+ *
+ * <p>Mutually exclusive with the property `fieldName` also defining field-name.
+ *
+ * @return The string pattern of the field name.
+ */
+ StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+ /**
* Define the field-type pattern by a fully qualified type.
*
* <p>Mutually exclusive with all method properties.
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
index d601a6a..e2ebc3b 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/KeepTarget.java
@@ -529,13 +529,37 @@
* <p>If none, and other properties define this item as a field, the default matches any field
* name.
*
- * <p>Mutually exclusive with the property `memberFromBinding` also defining field-name.
+ * <p>Mutually exclusive with the following other properties defining field-name:
+ *
+ * <ul>
+ * <li>fieldNamePattern
+ * <li>memberFromBinding
+ * </ul>
*
* @return The exact field name of the field.
*/
String fieldName() default "";
/**
+ * Define the field-name pattern by a string pattern.
+ *
+ * <p>Mutually exclusive with all method properties.
+ *
+ * <p>If none, and other properties define this item as a field, the default matches any field
+ * name.
+ *
+ * <p>Mutually exclusive with the following other properties defining field-name:
+ *
+ * <ul>
+ * <li>fieldName
+ * <li>memberFromBinding
+ * </ul>
+ *
+ * @return The string pattern of the field name.
+ */
+ StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+ /**
* Define the field-type pattern by a fully qualified type.
*
* <p>Mutually exclusive with all method properties.
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
index 229d0f1..f025c0e 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByNative.java
@@ -253,11 +253,27 @@
* <p>If none, and other properties define this item as a field, the default matches any field
* name.
*
+ * <p>Mutually exclusive with the property `fieldNamePattern` also defining field-name.
+ *
* @return The exact field name of the field.
*/
String fieldName() default "";
/**
+ * Define the field-name pattern by a string pattern.
+ *
+ * <p>Mutually exclusive with all method properties.
+ *
+ * <p>If none, and other properties define this item as a field, the default matches any field
+ * name.
+ *
+ * <p>Mutually exclusive with the property `fieldName` also defining field-name.
+ *
+ * @return The string pattern of the field name.
+ */
+ StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+ /**
* Define the field-type pattern by a fully qualified type.
*
* <p>Mutually exclusive with all method properties.
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
index 7b8fa8e..8076e51 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/annotations/UsedByReflection.java
@@ -253,11 +253,27 @@
* <p>If none, and other properties define this item as a field, the default matches any field
* name.
*
+ * <p>Mutually exclusive with the property `fieldNamePattern` also defining field-name.
+ *
* @return The exact field name of the field.
*/
String fieldName() default "";
/**
+ * Define the field-name pattern by a string pattern.
+ *
+ * <p>Mutually exclusive with all method properties.
+ *
+ * <p>If none, and other properties define this item as a field, the default matches any field
+ * name.
+ *
+ * <p>Mutually exclusive with the property `fieldName` also defining field-name.
+ *
+ * @return The string pattern of the field name.
+ */
+ StringPattern fieldNamePattern() default @StringPattern(exact = "");
+
+ /**
* Define the field-type pattern by a fully qualified type.
*
* <p>Mutually exclusive with all method properties.
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 7f03de2..91751e6 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
@@ -829,9 +829,6 @@
if (!descriptor.equals(itemDescriptor)) {
throw parsingContext.error("must reference its class context " + className);
}
- if (itemPattern.isMemberItemPattern() && items.size() == 1) {
- throw parsingContext.error("kind must include its class");
- }
if (!holderPattern.getInstanceOfPattern().isAny()) {
throw parsingContext.error("cannot define an 'extends' pattern.");
}
@@ -1567,6 +1564,7 @@
private static class FieldDeclaration extends Declaration<KeepFieldPattern> {
private final ParsingContext parsingContext;
+ private final StringPatternParser nameParser;
private final FieldTypeParser typeParser;
private KeepFieldAccessPattern.Builder accessBuilder = null;
private KeepFieldPattern.Builder builder = null;
@@ -1574,12 +1572,16 @@
public FieldDeclaration(ParsingContext parsingContext) {
this.parsingContext = parsingContext;
+ nameParser = new StringPatternParser(parsingContext.group(Item.fieldNameGroup));
+ nameParser.setProperty(Item.fieldName, StringProperty.EXACT);
+ nameParser.setProperty(Item.fieldNamePattern, StringProperty.PATTERN);
+
typeParser = new FieldTypeParser(parsingContext.group(Item.fieldTypeGroup));
typeParser.setProperty(Item.fieldTypePattern, TypeProperty.TYPE_PATTERN);
typeParser.setProperty(Item.fieldType, TypeProperty.TYPE_NAME);
typeParser.setProperty(Item.fieldTypeConstant, TypeProperty.TYPE_CONSTANT);
- parsers = Collections.singletonList(typeParser);
+ parsers = ImmutableList.of(nameParser, typeParser);
}
@Override
@@ -1603,6 +1605,9 @@
if (accessBuilder != null) {
getBuilder().setAccessPattern(accessBuilder.build());
}
+ if (!nameParser.isDefault()) {
+ getBuilder().setNamePattern(KeepFieldNamePattern.fromStringPattern(nameParser.getValue()));
+ }
if (!typeParser.isDefault()) {
getBuilder().setTypePattern(typeParser.getValue());
}
@@ -1610,15 +1615,6 @@
}
@Override
- boolean tryParse(String name, Object value) {
- if (name.equals(Item.fieldName) && value instanceof String) {
- getBuilder().setNamePattern(KeepFieldNamePattern.exact((String) value));
- return true;
- }
- return super.tryParse(name, value);
- }
-
- @Override
AnnotationVisitor tryParseArray(String name) {
if (name.equals(Item.fieldAccess)) {
accessBuilder = KeepFieldAccessPattern.builder();
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
index c714960..332e101 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/asm/KeepEdgeWriter.java
@@ -12,7 +12,6 @@
import com.android.tools.r8.keepanno.ast.KeepConsequences;
import com.android.tools.r8.keepanno.ast.KeepEdge;
import com.android.tools.r8.keepanno.ast.KeepEdgeException;
-import com.android.tools.r8.keepanno.ast.KeepFieldNamePattern.KeepFieldNameExactPattern;
import com.android.tools.r8.keepanno.ast.KeepFieldPattern;
import com.android.tools.r8.keepanno.ast.KeepItemPattern;
import com.android.tools.r8.keepanno.ast.KeepMemberItemPattern;
@@ -187,9 +186,9 @@
}
private void writeField(KeepFieldPattern field, AnnotationVisitor targetVisitor) {
- KeepFieldNameExactPattern exactFieldName = field.getNamePattern().asExact();
+ String exactFieldName = field.getNamePattern().asExactString();
if (exactFieldName != null) {
- targetVisitor.visit(Item.fieldName, exactFieldName.getName());
+ targetVisitor.visit(Item.fieldName, exactFieldName);
} else {
throw new Unimplemented();
}
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 c489701..f0bdccf 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
@@ -94,7 +94,9 @@
public static final String methodParameters = "methodParameters";
public static final String methodParameterTypePatterns = "methodParameterTypePatterns";
public static final String fieldAccess = "fieldAccess";
+ public static final String fieldNameGroup = "field-name";
public static final String fieldName = "fieldName";
+ public static final String fieldNamePattern = "fieldNamePattern";
public static final String fieldTypeGroup = "field-type";
public static final String fieldType = "fieldType";
public static final String fieldTypeConstant = "fieldTypeConstant";
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java
index 0945764..5223e60 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepFieldNamePattern.java
@@ -3,96 +3,44 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.keepanno.ast;
-public abstract class KeepFieldNamePattern {
+public class KeepFieldNamePattern {
+
+ private static final KeepFieldNamePattern ANY = new KeepFieldNamePattern(KeepStringPattern.any());
public static KeepFieldNamePattern any() {
- return Any.getInstance();
+ return ANY;
}
public static KeepFieldNamePattern exact(String methodName) {
- return new KeepFieldNameExactPattern(methodName);
+ return fromStringPattern(KeepStringPattern.exact(methodName));
}
- private KeepFieldNamePattern() {}
+ public static KeepFieldNamePattern fromStringPattern(KeepStringPattern pattern) {
+ if (pattern.isAny()) {
+ return ANY;
+ }
+ return new KeepFieldNamePattern(pattern);
+ }
+
+ private final KeepStringPattern pattern;
+
+ private KeepFieldNamePattern(KeepStringPattern pattern) {
+ this.pattern = pattern;
+ }
+
+ public KeepStringPattern asStringPattern() {
+ return pattern;
+ }
public boolean isAny() {
- return false;
+ return ANY == this;
}
public final boolean isExact() {
- return asExact() != null;
+ return pattern.isExact();
}
- public KeepFieldNameExactPattern asExact() {
- return null;
- }
-
- private static class Any extends KeepFieldNamePattern {
- private static final Any INSTANCE = new Any();
-
- public static Any getInstance() {
- return INSTANCE;
- }
-
- @Override
- public boolean isAny() {
- return true;
- }
-
- @Override
- public boolean equals(Object obj) {
- return this == obj;
- }
-
- @Override
- public int hashCode() {
- return System.identityHashCode(this);
- }
-
- @Override
- public String toString() {
- return "*";
- }
- }
-
- public static class KeepFieldNameExactPattern extends KeepFieldNamePattern {
- private final String name;
-
- public KeepFieldNameExactPattern(String name) {
- assert name != null;
- this.name = name;
- }
-
- @Override
- public KeepFieldNameExactPattern asExact() {
- return this;
- }
-
- public String getName() {
- return name;
- }
-
- @Override
- @SuppressWarnings("EqualsGetClass")
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- KeepFieldNameExactPattern that = (KeepFieldNameExactPattern) o;
- return name.equals(that.name);
- }
-
- @Override
- public int hashCode() {
- return name.hashCode();
- }
-
- @Override
- public String toString() {
- return name;
- }
+ public String asExactString() {
+ return pattern.asExactString();
}
}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
index 6843e61..b79b616 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/ast/KeepMethodNamePattern.java
@@ -3,17 +3,27 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.keepanno.ast;
+public class KeepMethodNamePattern {
-public abstract class KeepMethodNamePattern {
private static final String INIT_STRING = "<init>";
private static final String CLINIT_STRING = "<clinit>";
+ private static final KeepMethodNamePattern ANY =
+ new KeepMethodNamePattern(KeepStringPattern.any());
+ private static final KeepMethodNamePattern INSTANCE_INIT =
+ new KeepMethodNamePattern(KeepStringPattern.exact(INIT_STRING));
+ private static final KeepMethodNamePattern CLASS_INIT =
+ new KeepMethodNamePattern(KeepStringPattern.exact(CLINIT_STRING));
public static KeepMethodNamePattern any() {
- return SomePattern.ANY;
+ return KeepMethodNamePattern.ANY;
}
- public static KeepMethodNamePattern initializer() {
- return SomePattern.INSTANCE_INIT;
+ public static KeepMethodNamePattern instanceInitializer() {
+ return KeepMethodNamePattern.INSTANCE_INIT;
+ }
+
+ public static KeepMethodNamePattern classInitializer() {
+ return KeepMethodNamePattern.CLASS_INIT;
}
public static KeepMethodNamePattern exact(String methodName) {
@@ -22,98 +32,70 @@
public static KeepMethodNamePattern fromStringPattern(KeepStringPattern pattern) {
if (pattern.isAny()) {
- return SomePattern.ANY;
+ return KeepMethodNamePattern.ANY;
}
if (pattern.isExact()) {
String exact = pattern.asExactString();
if (INIT_STRING.equals(exact)) {
- return SomePattern.INSTANCE_INIT;
+ return KeepMethodNamePattern.INSTANCE_INIT;
}
if (CLINIT_STRING.equals(exact)) {
- return SomePattern.CLASS_INIT;
+ return KeepMethodNamePattern.CLASS_INIT;
}
}
- return new SomePattern(pattern);
+ return new KeepMethodNamePattern(pattern);
}
- private KeepMethodNamePattern() {}
+ private final KeepStringPattern pattern;
- public abstract boolean isAny();
+ private KeepMethodNamePattern(KeepStringPattern pattern) {
+ assert pattern != null;
+ this.pattern = pattern;
+ }
- public abstract boolean isInstanceInitializer();
+ public KeepStringPattern asStringPattern() {
+ return pattern;
+ }
- public abstract boolean isClassInitializer();
-
- public abstract boolean isExact();
-
- public abstract String asExactString();
-
- public abstract KeepStringPattern asStringPattern();
-
- private static class SomePattern extends KeepMethodNamePattern {
- private static final SomePattern ANY = new SomePattern(KeepStringPattern.any());
- private static final KeepMethodNamePattern INSTANCE_INIT =
- new SomePattern(KeepStringPattern.exact("<init>"));
- private static final KeepMethodNamePattern CLASS_INIT =
- new SomePattern(KeepStringPattern.exact("<clinit>"));
-
- private final KeepStringPattern pattern;
-
- public SomePattern(KeepStringPattern pattern) {
- assert pattern != null;
- this.pattern = pattern;
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
}
-
- @Override
- public KeepStringPattern asStringPattern() {
- return pattern;
+ if (!(o instanceof KeepMethodNamePattern)) {
+ return false;
}
+ KeepMethodNamePattern that = (KeepMethodNamePattern) o;
+ return pattern.equals(that.pattern);
+ }
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (!(o instanceof SomePattern)) {
- return false;
- }
- SomePattern that = (SomePattern) o;
- return pattern.equals(that.pattern);
- }
+ @Override
+ public int hashCode() {
+ return pattern.hashCode();
+ }
- @Override
- public int hashCode() {
- return pattern.hashCode();
- }
+ @Override
+ public String toString() {
+ return pattern.toString();
+ }
- @Override
- public String toString() {
- return pattern.toString();
- }
+ public boolean isAny() {
+ return ANY == this;
+ }
- @Override
- public boolean isAny() {
- return ANY == this;
- }
+ public boolean isClassInitializer() {
+ return CLASS_INIT == this;
+ }
- @Override
- public boolean isClassInitializer() {
- return CLASS_INIT == this;
- }
+ public boolean isInstanceInitializer() {
+ return INSTANCE_INIT == this;
+ }
- @Override
- public boolean isInstanceInitializer() {
- return INSTANCE_INIT == this;
- }
+ public boolean isExact() {
+ return pattern.isExact();
+ }
- @Override
- public boolean isExact() {
- return pattern.isExact();
- }
-
- @Override
- public String asExactString() {
- return pattern.asExactString();
- }
+ public String asExactString() {
+ return pattern.asExactString();
}
}
diff --git a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
index f16d9dd..7737744 100644
--- a/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
+++ b/src/keepanno/java/com/android/tools/r8/keepanno/keeprules/RulePrintingUtils.java
@@ -176,9 +176,7 @@
}
private static RulePrinter printFieldName(RulePrinter builder, KeepFieldNamePattern namePattern) {
- return namePattern.isAny()
- ? builder.appendStar()
- : builder.append(namePattern.asExact().getName());
+ return printStringPattern(builder, namePattern.asStringPattern());
}
private static RulePrinter printMethodName(
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index ad074ed..32d16c3 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -907,7 +907,9 @@
}
}
}
-
+ if (options.androidResourceProguardMapStrings != null) {
+ resourceShrinkerBuilder.setProguardMapStrings(options.androidResourceProguardMapStrings);
+ }
LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
ShrinkerResult shrinkerResult;
if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 405f214..2b20068 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -1177,13 +1177,21 @@
MapConsumer mapConsumer =
wrapExistingMapConsumerIfNotNull(
internal.mapConsumer, partitionMapConsumer, MapConsumerToPartitionMapConsumer::new);
- internal.mapConsumer =
+ mapConsumer =
wrapExistingMapConsumerIfNotNull(
mapConsumer,
stringConsumer,
nonNullStringConsumer ->
ProguardMapStringConsumer.builder().setStringConsumer(stringConsumer).build());
+ internal.mapConsumer =
+ wrapExistingMapConsumerIfNotNull(
+ mapConsumer,
+ androidResourceConsumer,
+ nonNulStringConsumer ->
+ ProguardMapStringConsumer.builder()
+ .setStringConsumer(new ResourceShrinkerMapStringConsumer(internal))
+ .build());
// Amend the usage information consumer with options from the proguard configuration.
internal.usageInformationConsumer =
wrapStringConsumer(
@@ -1308,6 +1316,27 @@
return optionConsumer;
}
+ private static class ResourceShrinkerMapStringConsumer implements StringConsumer {
+
+ private final InternalOptions internal;
+ private StringBuilder resultBuilder = new StringBuilder();
+
+ public ResourceShrinkerMapStringConsumer(InternalOptions internal) {
+ this.internal = internal;
+ }
+
+ @Override
+ public void accept(String string, DiagnosticsHandler handler) {
+ resultBuilder.append(string);
+ }
+
+ @Override
+ public void finished(DiagnosticsHandler handler) {
+ internal.androidResourceProguardMapStrings = StringUtils.splitLines(resultBuilder.toString());
+ resultBuilder = null;
+ }
+ }
+
private static class StandardOutConsumer extends StringConsumer.ForwardingConsumer {
public StandardOutConsumer(StringConsumer consumer) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index d26c44c..2d8cc3e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -244,6 +244,10 @@
return get(Opcodes.MUL);
}
+ public boolean mayHaveNewArrayEmpty() {
+ return get(Opcodes.NEW_ARRAY_EMPTY);
+ }
+
public boolean mayHaveNewArrayFilled() {
return get(Opcodes.NEW_ARRAY_FILLED);
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
index 2a93788..df8d38a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
@@ -96,15 +96,12 @@
BasicBlock block = worklist.next();
hasChanged |= simplifyArrayConstructionBlock(block, worklist, code, appView.options());
}
- if (hasChanged) {
- code.removeRedundantBlocks();
- }
return CodeRewriterResult.hasChanged(hasChanged);
}
@Override
protected boolean shouldRewriteCode(IRCode code) {
- return true;
+ return code.metadata().mayHaveNewArrayEmpty();
}
private boolean simplifyArrayConstructionBlock(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
index e269dc1..28a40a5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
@@ -31,11 +31,14 @@
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.utils.BooleanBox;
import com.android.tools.r8.utils.InternalOptions.RewriteArrayOptions;
import com.android.tools.r8.utils.SetUtils;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import java.util.ArrayList;
import java.util.IdentityHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -61,31 +64,63 @@
protected CodeRewriterResult rewriteCode(IRCode code) {
assert !mayHaveRedundantBlocks;
assert toRemove == NOTHING;
- BasicBlockIterator blockIterator = code.listIterator();
CodeRewriterResult result = noChange();
- while (blockIterator.hasNext()) {
- BasicBlock block = blockIterator.next();
- BasicBlockInstructionListIterator instructionIterator = block.listIterator(code);
- while (instructionIterator.hasNext()) {
- Instruction instruction = instructionIterator.next();
- if (instruction.isNewArrayFilled()) {
- result =
- processInstruction(
- code, blockIterator, instructionIterator, instruction.asNewArrayFilled(), result);
+ BooleanBox pendingRewrites = new BooleanBox(true);
+ while (pendingRewrites.get()) {
+ pendingRewrites.set(false);
+ BasicBlockIterator blockIterator = code.listIterator();
+ while (blockIterator.hasNext()) {
+ BasicBlock block = blockIterator.next();
+ BasicBlockInstructionListIterator instructionIterator = block.listIterator(code);
+ while (instructionIterator.hasNext()) {
+ Instruction instruction = instructionIterator.next();
+ if (instruction.isNewArrayFilled()) {
+ result =
+ processInstruction(
+ code,
+ blockIterator,
+ instructionIterator,
+ instruction.asNewArrayFilled(),
+ result,
+ pendingRewrites);
+ }
}
}
- }
- if (!toRemove.isEmpty()) {
- InstructionListIterator it = code.instructionListIterator();
- while (it.hasNext()) {
- if (toRemove.contains(it.next())) {
- it.remove();
- mayHaveRedundantBlocks = true;
+ if (!toRemove.isEmpty()) {
+ Set<Instruction> additionalToRemove = SetUtils.newIdentityHashSet();
+ InstructionListIterator it = code.instructionListIterator();
+ while (it.hasNext()) {
+ Instruction next = it.next();
+ if (toRemove.contains(next)) {
+ // Also remove constants used by the removed NewArrayFilled.
+ if (next.isNewArrayFilled()) {
+ next.inValues()
+ .forEach(
+ value -> {
+ if (value.hasSingleUniqueUser()) {
+ additionalToRemove.add(value.getDefinition());
+ }
+ });
+ }
+ it.remove();
+ mayHaveRedundantBlocks = true;
+ }
+ }
+ if (!additionalToRemove.isEmpty()) {
+ InstructionListIterator itAdditional = code.instructionListIterator();
+ while (itAdditional.hasNext()) {
+ Instruction next = itAdditional.next();
+ if (additionalToRemove.contains(next)) {
+ itAdditional.remove();
+ mayHaveRedundantBlocks = true;
+ }
+ }
}
}
- }
- if (mayHaveRedundantBlocks) {
- code.removeRedundantBlocks();
+ toRemove = NOTHING;
+ if (mayHaveRedundantBlocks) {
+ code.removeRedundantBlocks();
+ }
}
return result;
}
@@ -95,12 +130,49 @@
return code.metadata().mayHaveNewArrayFilled();
}
+ private boolean isNewArrayFilledOfConstants(NewArrayFilled newArrayFilled) {
+ for (Value inValue : newArrayFilled.inValues()) {
+ if (!inValue.isConstNumber() && !inValue.isConstString() && !inValue.isConstClass()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isDefinedByNewArrayFilledOfConstants(Value value) {
+ if (!value.isDefinedByInstructionSatisfying(Instruction::isNewArrayFilled)) {
+ return false;
+ }
+ return isNewArrayFilledOfConstants(value.definition.asNewArrayFilled());
+ }
+
+ public NewArrayFilled copyConstantsNewArrayFilled(IRCode code, NewArrayFilled original) {
+ assert isNewArrayFilledOfConstants(original);
+ Value newValue = code.createValue(original.getOutType(), original.getLocalInfo());
+ List<Value> newArguments = new ArrayList<>(original.inValues().size());
+ for (Value value : original.inValues()) {
+ if (value.isConstNumber()) {
+ newArguments.add(
+ ConstNumber.copyOf(code, value.getDefinition().asConstNumber()).outValue());
+ } else if (value.isConstString()) {
+ newArguments.add(
+ ConstString.copyOf(code, value.getDefinition().asConstString()).outValue());
+ } else if (value.isConstClass()) {
+ newArguments.add(ConstClass.copyOf(code, value.getDefinition().asConstClass()).outValue());
+ } else {
+ assert false;
+ }
+ }
+ return new NewArrayFilled(original.getArrayType(), newValue, newArguments);
+ }
+
private CodeRewriterResult processInstruction(
IRCode code,
BasicBlockIterator blockIterator,
BasicBlockInstructionListIterator instructionIterator,
NewArrayFilled newArrayFilled,
- CodeRewriterResult result) {
+ CodeRewriterResult result,
+ BooleanBox pendingRewrites) {
if (canUseNewArrayFilled(newArrayFilled)) {
return result;
}
@@ -108,13 +180,59 @@
instructionIterator.removeOrReplaceByDebugLocalRead();
} else if (canUseNewArrayFilledData(newArrayFilled)) {
rewriteToNewArrayFilledData(code, blockIterator, instructionIterator, newArrayFilled);
+ } else if (newArrayFilled.outValue().hasSingleUniqueUser()
+ && newArrayFilled.outValue().singleUniqueUser().isNewArrayFilled()
+ && isNewArrayFilledOfConstants(newArrayFilled)) {
+ if (canUseNewArrayFilled(newArrayFilled.outValue().singleUniqueUser().asNewArrayFilled())) {
+ // The NewArrayFilled user is supported, so rewrite here.
+ rewriteToArrayPuts(code, blockIterator, instructionIterator, newArrayFilled);
+ } else {
+ // The NewArrayFilled user is not supported so leave for rewriting after that.
+ //
+ // The effect of this is that when the user of this NewArrayFilled is rewritten to puts,
+ // the NewArrayFilled construction is copied to the use site
+ //
+ // Input:
+ //
+ // v0 <- Const X
+ // v1 <- NewArrayFilled(v0)
+ // v2 <- Const Y
+ // v3 <- NewArrayFilled(v2)
+ // v4 <- NewArrayFilled(v1, v3)
+ //
+ // After rewriting the user (v0 - v3 are unused and removed):
+ //
+ // v4 <- NewArrayEmpty(...)
+ // v5 <- Const X
+ // v6 <- NewArrayFilled(v5)
+ // APut v4, <Const 0>, v6
+ // v7 <- Const Y
+ // v8 <- NewArrayFilled(v7)
+ // APut v4, <Const 1>, v8
+ //
+ // Setting pending rewrites cause the copied NewArrayFilled to be rewritten in their new
+ // location in the fixpoint:
+ //
+ // v4 <- NewArrayEmpty(...)
+ // v9 <- NewArrayEmpty(...)
+ // v10 <- Const X
+ // APut v9, <Const 0>, v10
+ // APut v4, <Const 0>, v9
+ // v11 <- NewArrayEmpty(...)
+ // v12 <- Const Y
+ // APut v11, <Const 0>, v12
+ // APut v4, <Const 1>, v11
+ //
+ // If the NewArrayFilled which gets moved is supported then the second rewriting in the
+ // fixpoint does not happen.
+ pendingRewrites.set(true);
+ }
} else {
rewriteToArrayPuts(code, blockIterator, instructionIterator, newArrayFilled);
}
return CodeRewriterResult.HAS_CHANGED;
}
- @SuppressWarnings("ReferenceEquality")
private boolean canUseNewArrayFilled(NewArrayFilled newArrayFilled) {
if (!options.isGeneratingDex()) {
return false;
@@ -125,7 +243,7 @@
}
// filled-new-array is implemented only for int[] and Object[].
DexType arrayType = newArrayFilled.getArrayType();
- if (arrayType == dexItemFactory.intArrayType) {
+ if (arrayType.isIdenticalTo(dexItemFactory.intArrayType)) {
// For int[], using filled-new-array is usually smaller than filled-array-data.
// filled-new-array supports up to 5 registers before it's filled-new-array/range.
if (size > rewriteArrayOptions.maxSizeForFilledNewArrayOfInts) {
@@ -143,7 +261,7 @@
if (size > rewriteArrayOptions.maxSizeForFilledNewArrayOfReferences) {
return false;
}
- if (arrayType == dexItemFactory.stringArrayType) {
+ if (arrayType.isIdenticalTo(dexItemFactory.stringArrayType)) {
return rewriteArrayOptions.canUseFilledNewArrayOfStrings();
}
if (!rewriteArrayOptions.canUseFilledNewArrayOfNonStringObjects()) {
@@ -155,7 +273,7 @@
}
// Check that all arguments to the array is the array type or that the array is type Object[].
if (rewriteArrayOptions.canHaveSubTypesInFilledNewArrayBug()
- && arrayType != dexItemFactory.objectArrayType
+ && arrayType.isNotIdenticalTo(dexItemFactory.objectArrayType)
&& !arrayType.isPrimitiveArrayType()) {
DexType arrayElementType = arrayType.toArrayElementType(dexItemFactory);
for (Value elementValue : newArrayFilled.inValues()) {
@@ -169,9 +287,8 @@
return false;
}
- @SuppressWarnings("ReferenceEquality")
private boolean canStoreElementInNewArrayFilled(TypeElement valueType, DexType elementType) {
- if (elementType == dexItemFactory.objectType) {
+ if (elementType.isIdenticalTo(dexItemFactory.objectType)) {
return true;
}
if (valueType.isNullType() && !elementType.isPrimitiveType()) {
@@ -512,7 +629,9 @@
|| !(elementValue.isConstString()
|| elementValue.isConstNumber()
|| elementValue.isConstClass()
- || elementValue.isDefinedByInstructionSatisfying(Instruction::isStaticGet))) {
+ || elementValue.isDefinedByInstructionSatisfying(Instruction::isStaticGet)
+ || (isDefinedByNewArrayFilledOfConstants(elementValue)
+ && !instructionIterator.getBlock().hasCatchHandlers()))) {
return elementValue;
}
@@ -534,6 +653,14 @@
} else if (elementValue.isDefinedByInstructionSatisfying(Instruction::isStaticGet)) {
copy = StaticGet.copyOf(code, elementValue.getDefinition().asStaticGet());
constantMaterializingInstructionCache.putNewValue(copy.asStaticGet().outValue());
+ } else if (isDefinedByNewArrayFilledOfConstants(elementValue)) {
+ copy = copyConstantsNewArrayFilled(code, elementValue.getDefinition().asNewArrayFilled());
+ assert !instructionIterator.getBlock().hasCatchHandlers();
+ for (Value inValue : copy.asNewArrayFilled().inValues()) {
+ instructionIterator.add(inValue.getDefinition());
+ inValue.getDefinition().setBlock(instructionIterator.getBlock());
+ inValue.getDefinition().setPosition(newArrayEmpty.getPosition());
+ }
} else {
assert false;
return elementValue;
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index 26d9e87..8100e4e 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -5,6 +5,7 @@
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+import static com.google.common.base.Predicates.alwaysFalse;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -16,6 +17,7 @@
import com.android.tools.r8.graph.DexEncodedMember;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItem;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexReference;
@@ -28,15 +30,22 @@
import com.android.tools.r8.graph.lens.NonIdentityGraphLens;
import com.android.tools.r8.shaking.KeepFieldInfo.Joiner;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.MapUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.Streams;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.IdentityHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.function.Supplier;
// Non-mutable collection of keep information pertaining to a program.
@@ -253,6 +262,8 @@
public abstract KeepInfoCollection mutate(Consumer<MutableKeepInfoCollection> mutator);
+ public abstract void writeToDirectory(Path directory) throws IOException;
+
// Mutation interface for building up the keep info.
public static class MutableKeepInfoCollection extends KeepInfoCollection {
@@ -611,5 +622,53 @@
}
});
}
+
+ @Override
+ public void writeToDirectory(Path directory) throws IOException {
+ writePropertyToFile(
+ directory.resolve("no-horizontal-class-merging.txt"),
+ classInfo -> !classInfo.internalIsHorizontalClassMergingAllowed(),
+ alwaysFalse(),
+ alwaysFalse());
+ writePropertyToFile(
+ directory.resolve("no-optimization.txt"),
+ classInfo -> !classInfo.internalIsOptimizationAllowed(),
+ fieldInfo -> !fieldInfo.internalIsOptimizationAllowed(),
+ methodInfo -> !methodInfo.internalIsOptimizationAllowed());
+ writePropertyToFile(
+ directory.resolve("no-shrinking.txt"),
+ classInfo -> !classInfo.internalIsShrinkingAllowed(),
+ fieldInfo -> !fieldInfo.internalIsShrinkingAllowed(),
+ methodInfo -> !methodInfo.internalIsShrinkingAllowed());
+ }
+
+ private void writePropertyToFile(
+ Path path,
+ Predicate<KeepClassInfo> classInfoPredicate,
+ Predicate<KeepFieldInfo> fieldInfoPredicate,
+ Predicate<KeepMethodInfo> methodInfoPredicate)
+ throws IOException {
+ List<DexReference> lines = new ArrayList<>();
+ keepClassInfo.forEach(
+ (clazz, info) -> {
+ if (classInfoPredicate.test(info)) {
+ lines.add(clazz);
+ }
+ });
+ keepFieldInfo.forEach(
+ (field, info) -> {
+ if (fieldInfoPredicate.test(info)) {
+ lines.add(field);
+ }
+ });
+ keepMethodInfo.forEach(
+ (method, info) -> {
+ if (methodInfoPredicate.test(info)) {
+ lines.add(method);
+ }
+ });
+ lines.sort(DexReference::compareTo);
+ Files.write(path, ListUtils.map(lines, DexItem::toSourceString));
+ }
}
}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
index 79b6f5f..2279a8e 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
@@ -253,7 +253,6 @@
if (getMemberRules() != null) {
memberRules =
getMemberRules().stream()
- .filter(rule -> rule.getRuleType().includesMethods())
.map(memberRule -> memberRule.materialize(dexItemFactory))
.collect(Collectors.toList());
}
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 7dc92f2..d13b3d0 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.shaking;
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import static com.google.common.base.Predicates.alwaysFalse;
import com.android.tools.r8.androidapi.ComputedApiLevel;
@@ -154,8 +155,9 @@
retainReachableInterfacesFrom(type, reachableInterfaces);
}
if (!reachableInterfaces.isEmpty()) {
+ boolean pinnedHolder = !appView.getKeepInfo(clazz).isOptimizationAllowed(appView.options());
removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
- clazz.superType, reachableInterfaces);
+ pinnedHolder, clazz.superType, reachableInterfaces, clazz);
}
if (reachableInterfaces.isEmpty()) {
clazz.interfaces = DexTypeList.empty();
@@ -165,7 +167,7 @@
}
private void removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
- DexType type, Set<DexType> interfaces) {
+ boolean pinnedRoot, DexType type, Set<DexType> interfaces, DexProgramClass context) {
DexClass clazz = appView.definitionFor(type);
if (clazz == null) {
return;
@@ -176,13 +178,22 @@
return;
}
for (DexType itf : clazz.interfaces) {
+ if (pinnedRoot) {
+ DexProgramClass itfClass = asProgramClassOrNull(appView.definitionFor(itf, context));
+ if (itfClass == null
+ || !appView.getKeepInfo(itfClass).isOptimizationAllowed(appView.options())) {
+ // If root-holder and interface are pinned then retain the interface on the root-holder.
+ continue;
+ }
+ }
if (interfaces.remove(itf) && interfaces.isEmpty()) {
return;
}
}
if (clazz.superType != null) {
assert !interfaces.isEmpty();
- removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(clazz.superType, interfaces);
+ removeInterfacesImplementedDirectlyAndIndirectlyByClassFromSet(
+ pinnedRoot, clazz.superType, interfaces, context);
}
}
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 26a4087..011fbfb 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -201,6 +201,8 @@
public CancelCompilationChecker cancelCompilationChecker = null;
public AndroidResourceProvider androidResourceProvider = null;
public AndroidResourceConsumer androidResourceConsumer = null;
+ public List<String> androidResourceProguardMapStrings = null;
+
public ResourceShrinkerConfiguration resourceShrinkerConfiguration =
ResourceShrinkerConfiguration.DEFAULT_CONFIGURATION;
public ResourceAccessAnalysis resourceAccessAnalysis = null;
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
index dd538c7..2e2aaca 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/obfuscation/ProguardMappingsRecorder.kt
@@ -24,9 +24,12 @@
/**
* Records obfuscation mappings from single file in proguard format.
*
- * @param mappingsFile path to proguard.map file.
+ * @param mapping file lines or the mapping file path.
*/
-class ProguardMappingsRecorder(private val mappingsFile: Path) : ObfuscationMappingsRecorder {
+class ProguardMappingsRecorder(private val mappingLines: List<String>) : ObfuscationMappingsRecorder {
+
+ constructor(mappingsFile: Path)
+ : this(Files.readAllLines(mappingsFile, StandardCharsets.UTF_8));
override fun recordObfuscationMappings(model: ResourceShrinkerModel) {
model.obfuscatedClasses = extractObfuscatedResourceClasses()
@@ -41,9 +44,8 @@
// com.package.R -> a.b:
// com.package.R$style -> a.b.a:
// int Toolbar_android_gravity -> i1
-
val builder = ObfuscatedClasses.Builder()
- Files.readAllLines(mappingsFile, StandardCharsets.UTF_8).forEach { line ->
+ mappingLines.forEach { line ->
when {
isMethodMapping(line) -> builder.addMethodMapping(extractMethodMapping(line))
isClassMapping(line) -> builder.addClassMapping(extractClassMapping(line))
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
index ad7ddfb..1c0a875 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/LegacyResourceShrinker.java
@@ -15,6 +15,7 @@
import com.android.build.shrinker.ResourceTableUtilKt;
import com.android.build.shrinker.graph.ProtoResourcesGraphBuilder;
import com.android.build.shrinker.graph.ResFolderFileTree;
+import com.android.build.shrinker.obfuscation.ProguardMappingsRecorder;
import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
import com.android.build.shrinker.usages.DexFileAnalysisCallback;
import com.android.build.shrinker.usages.ProtoAndroidManifestUsageRecorderKt;
@@ -48,6 +49,7 @@
private final Map<String, byte[]> dexInputs;
private final List<PathAndBytes> resFolderInputs;
private final List<PathAndBytes> xmlInputs;
+ private List<String> proguardMapStrings;
private final List<PathAndBytes> manifest;
private final Map<PathAndBytes, FeatureSplit> resourceTables;
@@ -59,6 +61,7 @@
private final List<PathAndBytes> manifests = new ArrayList<>();
private final Map<PathAndBytes, FeatureSplit> resourceTables = new HashMap<>();
+ private List<String> proguardMapStrings;
private Builder() {}
@@ -96,7 +99,11 @@
public LegacyResourceShrinker build() {
assert manifests != null && resourceTables != null;
return new LegacyResourceShrinker(
- dexInputs, resFolderInputs, manifests, resourceTables, xmlInputs);
+ dexInputs, resFolderInputs, manifests, resourceTables, xmlInputs, proguardMapStrings);
+ }
+
+ public void setProguardMapStrings(List<String> proguardMapStrings) {
+ this.proguardMapStrings = proguardMapStrings;
}
}
@@ -105,12 +112,14 @@
List<PathAndBytes> resFolderInputs,
List<PathAndBytes> manifests,
Map<PathAndBytes, FeatureSplit> resourceTables,
- List<PathAndBytes> xmlInputs) {
+ List<PathAndBytes> xmlInputs,
+ List<String> proguardMapStrings) {
this.dexInputs = dexInputs;
this.resFolderInputs = resFolderInputs;
this.manifest = manifests;
this.resourceTables = resourceTables;
this.xmlInputs = xmlInputs;
+ this.proguardMapStrings = proguardMapStrings;
}
public static Builder builder() {
@@ -127,6 +136,10 @@
}
public ShrinkerResult shrinkModel(R8ResourceShrinkerModel model) throws IOException {
+ if (proguardMapStrings != null) {
+ new ProguardMappingsRecorder(proguardMapStrings).recordObfuscationMappings(model);
+ proguardMapStrings = null;
+ }
for (Entry<String, byte[]> entry : dexInputs.entrySet()) {
// The analysis needs an origin for the dex files, synthesize an easy recognizable one.
Path inMemoryR8 = Paths.get("in_memory_r8_" + entry.getKey() + ".dex");
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 9d47152..729f5e7 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1787,6 +1787,20 @@
ToolHelper.getDexVersionForApiLevel(apiLevelWithJavaTime()));
}
+ public boolean canUseFilledNewArrayOfInteger(TestParameters parameters) {
+ return parameters.isDexRuntime();
+ }
+
+ public boolean canUseFilledNewArrayOfStringObjects(TestParameters parameters) {
+ return parameters.isDexRuntime()
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
+ }
+
+ public boolean canUseFilledNewArrayOfNonStringObjects(TestParameters parameters) {
+ return parameters.isDexRuntime()
+ && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
+ }
+
// TODO(b/131130038): Do not allow accessmodification when kept.
public boolean isForceAccessModifyingPackagePrivateAndProtectedMethods() {
return true;
diff --git a/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java b/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java
new file mode 100644
index 0000000..fdc8a87
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/RClassStaticValuesIgnoreTest.java
@@ -0,0 +1,91 @@
+// Copyright (c) 2023, 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.androidresources;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+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 RClassStaticValuesIgnoreTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ private static final String RClassDescriptor =
+ descriptor(RClassStaticValuesIgnoreTest.class)
+ .replace(RClassStaticValuesIgnoreTest.class.getSimpleName(), "R$string");
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection parameters() {
+ return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+ }
+
+ public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .addRClassInitializeWithDefaultValues(R.string.class)
+ .build(temp);
+ }
+
+ private byte[] getRClassWithReferenceToUnused() throws IOException {
+ return transformer(ToBeRenamedToRDollarString.class)
+ .setClassDescriptor(RClassDescriptor)
+ .transform();
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ AndroidTestResource testResources = getTestResources(temp);
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addProgramClasses(FooBar.class)
+ .addProgramClassFileData(getRClassWithReferenceToUnused())
+ .addKeepClassAndMembersRulesWithAllowObfuscation(
+ DescriptorUtils.descriptorToJavaType(RClassDescriptor))
+ .addAndroidResources(testResources)
+ .addKeepMainRule(FooBar.class)
+ .compile()
+ .inspectShrunkenResources(
+ resourceTableInspector -> {
+ resourceTableInspector.assertContainsResourceWithName("string", "bar");
+ resourceTableInspector.assertDoesNotContainResourceWithName(
+ "string", "unused_string");
+ })
+ .run(parameters.getRuntime(), FooBar.class)
+ .assertSuccess();
+ }
+
+ public static class FooBar {
+
+ public static void main(String[] args) {
+ if (System.currentTimeMillis() == 0) {
+ System.out.println(R.string.bar);
+ }
+ }
+ }
+
+ // Simulate R class usage of unused_string
+ public static class ToBeRenamedToRDollarString {
+ public static int use_the_unused = RClassStaticValuesIgnoreTest.R.string.unused_string;
+ }
+
+ public static class R {
+ public static class string {
+
+ public static int bar;
+ public static int unused_string;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java
index 6d6b915..7cbbdf2 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java
@@ -39,7 +39,8 @@
testForR8(parameters.getBackend())
.addInnerClasses(getClass())
.setMinApi(parameters)
- .addKeepAllClassesRule()
+ .addKeepMainRule(Main.class)
+ .addKeepClassRules(J.class, A.class, B.class)
.addKeepAttributeSignature()
.addKeepAttributeInnerClassesAndEnclosingMethod()
.run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java
new file mode 100644
index 0000000..21f61ce
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedButKeptInterfaceTest.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.unusedinterfaces;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/* Regression test for b/318787479 */
+@RunWith(Parameterized.class)
+public class UnusedButKeptInterfaceTest extends TestBase {
+
+ static final String EXPECTED = StringUtils.lines("A: I", "B:", "C: I");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public UnusedButKeptInterfaceTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testReference() throws Exception {
+ testForRuntime(parameters)
+ .addInnerClasses(UnusedButKeptInterfaceTest.class)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addInnerClasses(UnusedButKeptInterfaceTest.class)
+ .addKeepMainRule(TestClass.class)
+ .addKeepClassRules(I.class, A.class, B.class, C.class)
+ .setMinApi(parameters)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ interface I {}
+
+ static class A implements I {}
+
+ static class B extends A {}
+
+ static class C extends A implements I {}
+
+ static class TestClass {
+
+ static String innerName(Class<?> clazz) {
+ String typeName = clazz.getName();
+ return typeName.substring(typeName.lastIndexOf('$') + 1);
+ }
+
+ public static void main(String[] args) {
+ for (Class<?> c : Arrays.asList(A.class, B.class, C.class)) {
+ System.out.print(innerName(c) + ":");
+ for (Class<?> iface : c.getInterfaces()) {
+ System.out.print(" " + innerName(iface));
+ }
+ System.out.println();
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java b/src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
similarity index 88%
rename from src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java
rename to src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
index ee1748d..8bf2e6a 100644
--- a/src/test/java/com/android/tools/r8/keepanno/doctests/ConditionalMethodRulesAndHorizontalMergingTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ConditionalMethodRulesAndHorizontalMergingTest.java
@@ -1,15 +1,15 @@
-// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.keepanno.doctests;
+package com.android.tools.r8.keepanno;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example1.BaseClass;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example1.MyHiddenMethodCaller;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example2.MyFieldValuePrinter;
-import com.android.tools.r8.keepanno.doctests.ConditionalMethodRulesAndHorizontalMergingTest.Example2.PrintableFieldInterface;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example1.BaseClass;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example1.MyHiddenMethodCaller;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example2.MyFieldValuePrinter;
+import com.android.tools.r8.keepanno.ConditionalMethodRulesAndHorizontalMergingTest.Example2.PrintableFieldInterface;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.ImmutableList;
diff --git a/src/test/java/com/android/tools/r8/keepanno/FieldNameStringPatternsTest.java b/src/test/java/com/android/tools/r8/keepanno/FieldNameStringPatternsTest.java
new file mode 100644
index 0000000..520c65c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/FieldNameStringPatternsTest.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.keepanno;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.StringPattern;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Field;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class FieldNameStringPatternsTest extends TestBase {
+
+ static final String EXPECTED = StringUtils.lines("1", "2");
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+ }
+
+ public FieldNameStringPatternsTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testReference() throws Exception {
+ testForRuntime(parameters)
+ .addProgramClasses(getInputClasses())
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .enableExperimentalKeepAnnotations()
+ .addProgramClasses(getInputClasses())
+ .setMinApi(parameters)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED)
+ .inspect(this::checkOutput);
+ }
+
+ public List<Class<?>> getInputClasses() {
+ return ImmutableList.of(TestClass.class, A.class, B.class);
+ }
+
+ private void checkOutput(CodeInspector inspector) {
+ assertThat(inspector.clazz(B.class), isPresentAndRenamed());
+ assertThat(inspector.clazz(B.class).field("int", "bar"), isAbsent());
+ assertThat(inspector.clazz(B.class).field("int", "hiddenOneI"), isPresent());
+ assertThat(inspector.clazz(B.class).field("int", "hiddenTwoI"), isPresent());
+ assertThat(inspector.clazz(B.class).field("java.lang.String", "hiddenSomeS"), isAbsent());
+ }
+
+ static class A {
+
+ @UsesReflection(
+ @KeepTarget(
+ classConstant = B.class,
+ fieldNamePattern = @StringPattern(startsWith = "hidden", endsWith = "I")))
+ public void foo() throws Exception {
+ int counter = 1;
+ for (Field field : B.class.getDeclaredFields()) {
+ String name = field.getName();
+ if (name.startsWith("hidden")) {
+ if (name.endsWith("I")) {
+ field.set(null, counter++);
+ }
+ }
+ }
+ for (Field field : B.class.getDeclaredFields()) {
+ String name = field.getName();
+ if (name.startsWith("hidden")) {
+ if (name.endsWith("I")) {
+ System.out.println(field.get(null));
+ }
+ }
+ }
+ }
+ }
+
+ static class B {
+ static int hiddenOneI = 9;
+ static int hiddenTwoI = 18;
+ static String hiddenSomeS = "foo";
+ static int bar = 7;
+ }
+
+ static class TestClass {
+
+ @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
+ public static void main(String[] args) throws Exception {
+ new A().foo();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
index 8a83cf9..dae2c6b 100644
--- a/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/ast/KeepEdgeAstTest.java
@@ -250,7 +250,7 @@
private KeepMemberPattern defaultInitializerPattern() {
return KeepMethodPattern.builder()
- .setNamePattern(KeepMethodNamePattern.initializer())
+ .setNamePattern(KeepMethodNamePattern.instanceInitializer())
.setParametersPattern(KeepMethodParametersPattern.none())
.setReturnTypeVoid()
.build();
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.java
new file mode 100644
index 0000000..7df53d6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/ForApiDocumentationTest.java
@@ -0,0 +1,210 @@
+// Copyright (c) 2023, 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.doctests;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.keepanno.annotations.MemberAccessFlags;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ForApiDocumentationTest extends TestBase {
+
+ static final String EXPECTED_1_REF =
+ StringUtils.joinLines(
+ "thisPackagePrivateMethodIsNotKept",
+ "thisPrivateMethodIsNotKept",
+ "thisProtectedMethodIsKept",
+ "thisPublicMethodIsKept");
+
+ static final String EXPECTED_1_R8 =
+ StringUtils.joinLines("thisProtectedMethodIsKept", "thisPublicMethodIsKept");
+
+ static final String EXPECTED_2_REF =
+ StringUtils.joinLines(
+ "thisPackagePrivateMethodIsKept",
+ "thisPrivateMethodIsNotKept",
+ "thisProtectedMethodIsKept",
+ "thisPublicMethodIsKept");
+
+ static final String EXPECTED_2_R8 =
+ StringUtils.joinLines(
+ "thisPackagePrivateMethodIsKept", "thisProtectedMethodIsKept", "thisPublicMethodIsKept");
+
+ static final String EXPECTED_3_REF = StringUtils.joinLines("isKept", "notKept");
+
+ static final String EXPECTED_3_R8 = StringUtils.joinLines("isKept");
+
+ static final String EXPECTED_REF =
+ StringUtils.lines(EXPECTED_1_REF, EXPECTED_2_REF, EXPECTED_3_REF);
+
+ static final String EXPECTED_R8 = StringUtils.lines(EXPECTED_1_R8, EXPECTED_2_R8, EXPECTED_3_R8);
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDefaultRuntimes().withApiLevel(AndroidApiLevel.B).build();
+ }
+
+ public ForApiDocumentationTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testReference() throws Exception {
+ testForRuntime(parameters)
+ .addProgramClasses(TestClass.class)
+ .addProgramClassesAndInnerClasses(getExampleClasses())
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED_REF);
+ }
+
+ @Test
+ public void testWithRuleExtraction() throws Exception {
+ testForR8(parameters.getBackend())
+ .enableExperimentalKeepAnnotations()
+ .addProgramClasses(TestClass.class)
+ .addProgramClassesAndInnerClasses(getExampleClasses())
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED_R8);
+ }
+
+ public List<Class<?>> getExampleClasses() {
+ return ImmutableList.of(Example1.class, Example2.class, Example3.class);
+ }
+
+ static class Example1 {
+
+ /* INCLUDE DOC: ApiClass
+ When annotating a class the default for `@KeepForApi` is to keep the class as well as all of its
+ public and protected members:
+ INCLUDE END */
+
+ static
+ // INCLUDE CODE: ApiClass
+ @KeepForApi
+ public class MyApi {
+ public void thisPublicMethodIsKept() {
+ /* ... */
+ }
+
+ protected void thisProtectedMethodIsKept() {
+ /* ... */
+ }
+
+ void thisPackagePrivateMethodIsNotKept() {
+ /* ... */
+ }
+
+ private void thisPrivateMethodIsNotKept() {
+ /* ... */
+ }
+ }
+
+ // INCLUDE END
+
+ static void run() throws Exception {
+ TestClass.printMethods(MyApi.class);
+ }
+ }
+
+ static class Example2 {
+
+ /* INCLUDE DOC: ApiClassMemberAccess
+ The default can be changed using the `@KeepForApi#memberAccess` property:
+ INCLUDE END */
+
+ // INCLUDE CODE: ApiClassMemberAccess
+ @KeepForApi(
+ memberAccess = {
+ MemberAccessFlags.PUBLIC,
+ MemberAccessFlags.PROTECTED,
+ MemberAccessFlags.PACKAGE_PRIVATE
+ })
+ // INCLUDE END
+ public static class MyApi {
+ public void thisPublicMethodIsKept() {
+ /* ... */
+ }
+
+ protected void thisProtectedMethodIsKept() {
+ /* ... */
+ }
+
+ void thisPackagePrivateMethodIsKept() {
+ /* ... */
+ }
+
+ private void thisPrivateMethodIsNotKept() {
+ /* ... */
+ }
+ }
+
+ static void run() throws Exception {
+ TestClass.printMethods(MyApi.class);
+ }
+ }
+
+ static class Example3 {
+
+ /* INCLUDE DOC: ApiMember
+ The `@KeepForApi` annotation can also be placed directly on members and avoid keeping
+ unannotated members. The holder class is implicitly kept. When annotating the members
+ directly, the access does not matter as illustrated here by annotating a package private method:
+ INCLUDE END */
+
+ static
+ // INCLUDE CODE: ApiMember
+ public class MyOtherApi {
+
+ public void notKept() {
+ /* ... */
+ }
+
+ @KeepForApi
+ void isKept() {
+ /* ... */
+ }
+ }
+
+ // INCLUDE END
+
+ static void run() throws Exception {
+ TestClass.printMethods(MyOtherApi.class);
+ }
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) throws Exception {
+ Example1.run();
+ Example2.run();
+ Example3.run();
+ }
+
+ static void printMethods(Class<?> clazz) {
+ List<String> names = new ArrayList<>();
+ for (Method m : clazz.getDeclaredMethods()) {
+ names.add(m.getName());
+ }
+ names.sort(String::compareTo);
+ for (String name : names) {
+ System.out.println(name);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
index 0cf7639..98a2f0d 100644
--- a/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
+++ b/src/test/java/com/android/tools/r8/keepanno/doctests/UsesReflectionDocumentationTest.java
@@ -6,9 +6,11 @@
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.keepanno.annotations.KeepCondition;
import com.android.tools.r8.keepanno.annotations.KeepConstraint;
import com.android.tools.r8.keepanno.annotations.KeepItemKind;
import com.android.tools.r8.keepanno.annotations.KeepTarget;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
import com.android.tools.r8.keepanno.annotations.UsesReflection;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
@@ -22,8 +24,17 @@
@RunWith(Parameterized.class)
public class UsesReflectionDocumentationTest extends TestBase {
+ static final String EXPECTED_METHOD_EXAMPLE = StringUtils.joinLines("on Base", "on Sub");
+ static final String EXPECTED_FIELD_EXAMPLE =
+ StringUtils.joinLines("intField = 42", "stringField = Hello!");
+
static final String EXPECTED =
- StringUtils.lines("on Base", "on Sub", "intField = 42", "stringField = Hello!");
+ StringUtils.lines(
+ EXPECTED_METHOD_EXAMPLE,
+ EXPECTED_FIELD_EXAMPLE,
+ EXPECTED_FIELD_EXAMPLE,
+ EXPECTED_FIELD_EXAMPLE,
+ EXPECTED_FIELD_EXAMPLE);
private final TestParameters parameters;
@@ -58,7 +69,8 @@
}
public List<Class<?>> getExampleClasses() {
- return ImmutableList.of(Example1.class, Example2.class);
+ return ImmutableList.of(
+ Example1.class, Example2.class, Example3.class, Example4.class, Example5.class);
}
static class Example1 {
@@ -160,11 +172,140 @@
}
}
+ static class Example3 {
+
+ interface PrintableFieldInterface {}
+
+ /* INCLUDE DOC: UsedByReflectionFieldPrinterOnFields
+ For example, the same field printing as in the above example might be part of a library.
+
+ In this example, the `MyClassWithFields` is a class you are passing to the
+ field-printing utility of the library. Since the library is reflectively accessing each field
+ we annotate them with the `@UsedByReflection` annotation.
+
+ We could additionally add the `@UsedByReflection#constraints` property as we did previously.
+ We elide it here for brevity.
+ INCLUDE END */
+
+ static
+ // INCLUDE CODE: UsedByReflectionFieldPrinterOnFields
+ public class MyClassWithFields implements PrintableFieldInterface {
+ @UsedByReflection final int intField = 42;
+
+ @UsedByReflection String stringField = "Hello!";
+ }
+
+ public static void run() throws Exception {
+ new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
+ }
+
+ // INCLUDE END
+
+ public static class FieldValuePrinterLibrary {
+
+ public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+ for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+ System.out.println(field.getName() + " = " + field.get(objectWithFields));
+ }
+ }
+ }
+ }
+
+ static class Example4 {
+
+ interface PrintableFieldInterface {}
+
+ /* INCLUDE DOC: UsedByReflectionFieldPrinterOnClass
+ Rather than annotate the individual fields we can annotate the holder and add a specification
+ similar to the `@KeepTarget`. The `@UsedByReflection#kind` specifies that only the fields are
+ used reflectively. In particular, the "field printer" example we are considering here does not
+ make reflective assumptions about the holder class, so we should not constrain it.
+
+ To be more precise let's add the `@UsedByReflection#constraints` property now. This specifies
+ that the fields are looked up, their names are used/assumed and their values are read.
+ INCLUDE END */
+
+ static
+ // INCLUDE CODE: UsedByReflectionFieldPrinterOnClass
+ @UsedByReflection(
+ kind = KeepItemKind.ONLY_FIELDS,
+ constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+ public class MyClassWithFields implements PrintableFieldInterface {
+ final int intField = 42;
+ String stringField = "Hello!";
+ }
+
+ // INCLUDE END
+
+ public static void run() throws Exception {
+ new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
+ }
+
+ public static class FieldValuePrinterLibrary {
+
+ public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+ for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+ System.out.println(field.getName() + " = " + field.get(objectWithFields));
+ }
+ }
+ }
+ }
+
+ static class Example5 {
+
+ interface PrintableFieldInterface {}
+
+ /* INCLUDE DOC: UsedByReflectionFieldPrinterConditional
+ Our use of `@UsedByReflection` is still not as flexible as the original `@UsesReflection`. In
+ particular, if we change our code to no longer have any call to the library method
+ `printFieldValues` the shrinker will still keep all of the fields on our annotated class.
+
+ This is because the `@UsesReflection` implicitly encodes as a precondition that the annotated
+ method is actually used in the program. If not, the `@UsesReflection` annotation is not
+ "active".
+
+ Luckily we can specify the same precondition using `@UsedByReflection#preconditions`.
+ INCLUDE END */
+
+ static
+ // INCLUDE CODE: UsedByReflectionFieldPrinterConditional
+ @UsedByReflection(
+ preconditions = {
+ @KeepCondition(
+ classConstant = FieldValuePrinterLibrary.class,
+ methodName = "printFieldValues")
+ },
+ kind = KeepItemKind.ONLY_FIELDS,
+ constraints = {KeepConstraint.LOOKUP, KeepConstraint.NAME, KeepConstraint.FIELD_GET})
+ public class MyClassWithFields implements PrintableFieldInterface {
+ final int intField = 42;
+ String stringField = "Hello!";
+ }
+
+ // INCLUDE END
+
+ public static void run() throws Exception {
+ new FieldValuePrinterLibrary().printFieldValues(new MyClassWithFields());
+ }
+
+ public static class FieldValuePrinterLibrary {
+
+ public void printFieldValues(PrintableFieldInterface objectWithFields) throws Exception {
+ for (Field field : objectWithFields.getClass().getDeclaredFields()) {
+ System.out.println(field.getName() + " = " + field.get(objectWithFields));
+ }
+ }
+ }
+ }
+
static class TestClass {
public static void main(String[] args) throws Exception {
Example1.run();
Example2.run();
+ Example3.run();
+ Example4.run();
+ Example5.run();
}
}
}
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
index 9088752..9376fa3 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
@@ -20,6 +20,7 @@
import com.android.tools.r8.keepanno.annotations.UsedByNative;
import com.android.tools.r8.keepanno.annotations.UsedByReflection;
import com.android.tools.r8.keepanno.annotations.UsesReflection;
+import com.android.tools.r8.keepanno.doctests.ForApiDocumentationTest;
import com.android.tools.r8.keepanno.doctests.MainMethodsDocumentationTest;
import com.android.tools.r8.keepanno.doctests.UsesReflectionDocumentationTest;
import com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.Generator;
@@ -92,7 +93,9 @@
MethodAccessFlags.class,
FieldAccessFlags.class);
populateCodeAndDocReplacements(
- UsesReflectionDocumentationTest.class, MainMethodsDocumentationTest.class);
+ UsesReflectionDocumentationTest.class,
+ ForApiDocumentationTest.class,
+ MainMethodsDocumentationTest.class);
}
private Map<String, String> getTypeLinkReplacements(Class<?>... classes) {
@@ -418,7 +421,7 @@
}
public String getIdAnchor() {
- return name + "<a id=" + quote(id) + "></a>";
+ return name + "<a name=" + quote(id) + "></a>";
}
}
}
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 0ab89fa..61265cb 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
@@ -821,7 +821,14 @@
.addParagraph(getMutuallyExclusiveForFieldProperties())
.addParagraph(getFieldDefaultDoc("any field name"))
.setDocReturn("The exact field name of the field.")
- .defaultEmptyString());
+ .defaultEmptyString())
+ .addMember(
+ new GroupMember("fieldNamePattern")
+ .setDocTitle("Define the field-name pattern by a string pattern.")
+ .addParagraph(getMutuallyExclusiveForFieldProperties())
+ .addParagraph(getFieldDefaultDoc("any field name"))
+ .setDocReturn("The string pattern of the field name.")
+ .defaultValue(StringPattern.class, DEFAULT_INVALID_STRING_PATTERN));
}
private Group createFieldTypeGroup() {
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index 12b507d..ba27997 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -257,6 +257,7 @@
testBuilder.addKeepRules(
"-neverinline class kotlin.jvm.internal.Intrinsics {",
" *** checkNotNullParameter(...);",
+ " *** checkParameterIsNotNull(...);",
"}"))
.inspect(
inspector -> {
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java
new file mode 100644
index 0000000..82ae38f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfArraysTest.java
@@ -0,0 +1,185 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ArrayOfArraysTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public CompilationMode compilationMode;
+
+ @Parameters(name = "{0}, mode = {1}")
+ public static Iterable<?> data() {
+ return buildParameters(
+ getTestParameters()
+ .withDefaultCfRuntime()
+ .withDexRuntimesAndAllApiLevels()
+ .withAllApiLevelsAlsoForCf()
+ .build(),
+ CompilationMode.values());
+ }
+
+ private static final String EXPECTED_OUTPUT =
+ StringUtils.lines(
+ "[class [B, class [S, class [I, class [J, class [C, class [F, class [D, class"
+ + " [Ljava.lang.String;, [class [B, class [S, class [I, class [J, class [C, class [F,"
+ + " class [D, class [Ljava.lang.String;]]",
+ "[class [B, class [S, class [I, class [J, class [C, class [F, class [D, class"
+ + " [Ljava.lang.String;, [class [B, class [S, class [I, class [J, class [C, class [F,"
+ + " class [D, class [Ljava.lang.String;]]");
+
+ private void inspect(MethodSubject method, int dexFilledNewArray, int cfMaxLocals) {
+ if (parameters.isDexRuntime()) {
+ assertEquals(
+ dexFilledNewArray,
+ method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+
+ } else {
+ assertEquals(
+ 0, method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+ assertEquals(cfMaxLocals, method.getMethod().getCode().asCfCode().getMaxLocals());
+ }
+ }
+
+ private void inspect(CodeInspector inspector) {
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
+ 2
+ + (canUseFilledNewArrayOfStringObjects(parameters) ? 2 : 0)
+ + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 2 : 0),
+ 2);
+ inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 2, 2);
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ parameters.assumeDexRuntime();
+ testForD8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .setMinApi(parameters)
+ .setMode(compilationMode)
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::inspect)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ parameters.assumeR8TestParameters();
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters)
+ .setMode(compilationMode)
+ .enableInliningAnnotations()
+ .addDontObfuscate()
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::inspect)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ public static final class TestClass {
+
+ @NeverInline
+ public static void m1() {
+ Object[] array = {
+ new byte[] {(byte) 1},
+ new short[] {(short) 1},
+ new int[] {1},
+ new long[] {1L},
+ new char[] {(char) 1},
+ new float[] {1.0f},
+ new double[] {1.0d},
+ new String[] {"one"},
+ new Object[] {
+ new byte[] {(byte) 2},
+ new short[] {(short) 2},
+ new int[] {2},
+ new long[] {2L},
+ new char[] {(char) 2},
+ new float[] {2.0f},
+ new double[] {2.0d},
+ new String[] {"two"},
+ }
+ };
+ printArray(array);
+ System.out.println();
+ }
+
+ @NeverInline
+ public static void m2() {
+ try {
+ Object[] array = {
+ new byte[] {(byte) 1},
+ new short[] {(short) 1},
+ new int[] {1},
+ new long[] {1L},
+ new char[] {(char) 1},
+ new float[] {1.0f},
+ new double[] {1.0d},
+ new String[] {"one"},
+ new Object[] {
+ new byte[] {(byte) 2},
+ new short[] {(short) 2},
+ new int[] {2},
+ new long[] {2L},
+ new char[] {(char) 2},
+ new float[] {2.0f},
+ new double[] {2.0d},
+ new String[] {"two"},
+ }
+ };
+ printArray(array);
+ System.out.println();
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+ }
+
+ @NeverInline
+ public static void printArray(Object[] array) {
+ System.out.print("[");
+ for (int i = 0; i < array.length; i++) {
+ if (i != 0) {
+ System.out.print(", ");
+ }
+ if (array[i].getClass().isArray()) {
+ if (array[i] instanceof Object[] && !(array[i] instanceof String[])) {
+ printArray((Object[]) array[i]);
+ } else {
+ System.out.print(array[i].getClass());
+ }
+ } else {
+ System.out.print("Unexpected " + array[i].getClass());
+ }
+ }
+ System.out.print("]");
+ }
+
+ public static void main(String[] args) {
+ m1();
+ m2();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java
new file mode 100644
index 0000000..63fec32
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfConstClassArraysTest.java
@@ -0,0 +1,179 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.rewrite.arrays.ConstClassArrayWithUniqueValuesTest.TestClass;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ArrayOfConstClassArraysTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public CompilationMode compilationMode;
+
+ @Parameters(name = "{0}, mode = {1}")
+ public static Iterable<?> data() {
+ return buildParameters(
+ getTestParameters()
+ .withDefaultCfRuntime()
+ .withDexRuntimesAndAllApiLevels()
+ .withAllApiLevelsAlsoForCf()
+ .build(),
+ CompilationMode.values());
+ }
+
+ private static final String EXPECTED_OUTPUT =
+ StringUtils.lines(
+ "[[A, B], [C, D], [E, F, G], [H, I, J]]", "[[J, I, H], [G, F, E], [D, C], [B, A]]");
+
+ private void inspect(MethodSubject method, int dexFilledNewArray, int cfMaxLocals) {
+ if (canUseFilledNewArrayOfNonStringObjects(parameters)) {
+ assertEquals(
+ dexFilledNewArray,
+ method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+
+ } else {
+ assertEquals(
+ 0, method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+ if (parameters.isCfRuntime()) {
+ assertEquals(cfMaxLocals, method.getMethod().getCode().asCfCode().getMaxLocals());
+ }
+ }
+ }
+
+ private void inspect(CodeInspector inspector) {
+ inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5, 1);
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
+ 0,
+ compilationMode.isDebug() ? 1 : 2);
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ parameters.assumeDexRuntime();
+ testForD8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .setMinApi(parameters)
+ .setMode(compilationMode)
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::inspect)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ parameters.assumeR8TestParameters();
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters)
+ .setMode(compilationMode)
+ .enableInliningAnnotations()
+ .addDontObfuscate()
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::inspect)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ public static final class TestClass {
+
+ @NeverInline
+ public static void m1() {
+ Class<?>[][] array = {
+ new Class<?>[] {A.class, B.class},
+ new Class<?>[] {C.class, D.class},
+ new Class<?>[] {E.class, F.class, G.class},
+ new Class<?>[] {H.class, I.class, J.class}
+ };
+ printArray(array);
+ }
+
+ @NeverInline
+ public static void m2() {
+ try {
+ Class<?>[][] array = {
+ new Class<?>[] {J.class, I.class, H.class},
+ new Class<?>[] {G.class, F.class, E.class},
+ new Class<?>[] {D.class, C.class},
+ new Class<?>[] {B.class, A.class}
+ };
+ printArray(array);
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+ }
+
+ @NeverInline
+ public static void printArray(Class<?>[][] array) {
+ System.out.print("[");
+ for (int i = 0; i < array.length; i++) {
+ if (i != 0) {
+ System.out.print(", ");
+ }
+ printArray(array[i]);
+ }
+ System.out.println("]");
+ }
+
+ @NeverInline
+ public static void printArray(Class<?>[] array) {
+ System.out.print("[");
+ for (int i = 0; i < array.length; i++) {
+ if (i != 0) {
+ System.out.print(", ");
+ }
+ String simpleName = array[i].getName();
+ if (simpleName.lastIndexOf("$") > 0) {
+ simpleName = simpleName.substring(simpleName.lastIndexOf("$") + 1);
+ }
+ System.out.print(simpleName);
+ }
+ System.out.print("]");
+ }
+
+ public static void main(String[] args) {
+ m1();
+ m2();
+ }
+ }
+
+ class A {}
+
+ class B {}
+
+ class C {}
+
+ class D {}
+
+ class E {}
+
+ class F {}
+
+ class G {}
+
+ class H {}
+
+ class I {}
+
+ class J {}
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java
new file mode 100644
index 0000000..edc54de
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfIntArraysTest.java
@@ -0,0 +1,151 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ArrayOfIntArraysTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public CompilationMode compilationMode;
+
+ @Parameters(name = "{0}, mode = {1}")
+ public static Iterable<?> data() {
+ return buildParameters(
+ getTestParameters()
+ .withDefaultCfRuntime()
+ .withDexRuntimesAndAllApiLevels()
+ .withAllApiLevelsAlsoForCf()
+ .build(),
+ CompilationMode.values());
+ }
+
+ private static final String EXPECTED_OUTPUT =
+ StringUtils.lines(
+ "[[1, 2], [3, 4], [5, 6, 7], [8, 9, 10]]", "[[10, 9, 8], [7, 6, 5], [4, 3], [2, 1]]");
+
+ private void inspect(MethodSubject method, int dexFilledNewArray, int cfMaxLocals) {
+ if (parameters.isDexRuntime()) {
+ assertEquals(
+ dexFilledNewArray,
+ method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+
+ } else {
+ assertEquals(
+ 0, method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+ assertEquals(cfMaxLocals, method.getMethod().getCode().asCfCode().getMaxLocals());
+ }
+ }
+
+ private void inspect(CodeInspector inspector) {
+ // This test use smaller int arrays, where filled-new-array is preferred over filled-array-data.
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
+ 4 + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+ 1);
+ // With catch handler the int[][] creation is not converted to filled-new-array.
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
+ 4,
+ compilationMode.isDebug() ? 1 : 2);
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ parameters.assumeDexRuntime();
+ testForD8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .setMinApi(parameters)
+ .setMode(compilationMode)
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::inspect)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ parameters.assumeR8TestParameters();
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters)
+ .setMode(compilationMode)
+ .enableInliningAnnotations()
+ .addDontObfuscate()
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::inspect)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ public static final class TestClass {
+
+ @NeverInline
+ public static void m1() {
+ int[][] array = {
+ new int[] {1, 2}, new int[] {3, 4}, new int[] {5, 6, 7}, new int[] {8, 9, 10}
+ };
+ printArray(array);
+ }
+
+ @NeverInline
+ public static void m2() {
+ try {
+ int[][] array = {
+ new int[] {10, 9, 8}, new int[] {7, 6, 5}, new int[] {4, 3}, new int[] {2, 1}
+ };
+ printArray(array);
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+ }
+
+ @NeverInline
+ public static void printArray(int[][] array) {
+ System.out.print("[");
+ for (int i = 0; i < array.length; i++) {
+ if (i != 0) {
+ System.out.print(", ");
+ }
+ printArray(array[i]);
+ }
+ System.out.println("]");
+ }
+
+ @NeverInline
+ public static void printArray(int[] array) {
+ System.out.print("[");
+ for (int i = 0; i < array.length; i++) {
+ if (i != 0) {
+ System.out.print(", ");
+ }
+ System.out.print(array[i]);
+ }
+ System.out.print("]");
+ }
+
+ public static void main(String[] args) {
+ m1();
+ m2();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java
new file mode 100644
index 0000000..1934f24
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayOfStringArraysTest.java
@@ -0,0 +1,235 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.rewrite.arrays.ArrayOfConstClassArraysTest.TestClass;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 ArrayOfStringArraysTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public CompilationMode compilationMode;
+
+ @Parameters(name = "{0}, mode = {1}")
+ public static Iterable<?> data() {
+ return buildParameters(
+ getTestParameters()
+ .withDefaultCfRuntime()
+ .withDexRuntimesAndAllApiLevels()
+ .withAllApiLevelsAlsoForCf()
+ .build(),
+ CompilationMode.values());
+ }
+
+ private static final String EXPECTED_OUTPUT =
+ StringUtils.lines(
+ "[[A, B], [C, D], [E, F, G], [H, I, J]]",
+ "[[J, I, H], [G, F, E], [D, C], [B, A]]",
+ "[[[A, B, C]], [[D, E, F]], [[G, H]], [[I, J]]]",
+ "[[[J, I, H]], [[G, F, E]], [[D, C]], [[B, A]]]",
+ "[[J, I, H], [G, F, E], [D, C], [B, A]]");
+
+ private void inspect(MethodSubject method, int dexFilledNewArray, int cfMaxLocals) {
+ if (parameters.isDexRuntime()) {
+ assertEquals(
+ dexFilledNewArray,
+ method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+
+ } else {
+ assertEquals(
+ 0, method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+ assertEquals(cfMaxLocals, method.getMethod().getCode().asCfCode().getMaxLocals());
+ }
+ }
+
+ private void inspect(CodeInspector inspector) {
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
+ (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
+ + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+ 1);
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
+ 0,
+ compilationMode.isDebug() ? 2 : 1);
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"),
+ (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
+ + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 5 : 0),
+ 5);
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m4"),
+ 0,
+ compilationMode.isDebug() ? 2 : 1);
+ inspect(
+ inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m5"),
+ (canUseFilledNewArrayOfStringObjects(parameters) ? 4 : 0)
+ + (canUseFilledNewArrayOfNonStringObjects(parameters) ? 1 : 0),
+ compilationMode.isDebug() ? 6 : 4);
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ parameters.assumeDexRuntime();
+ testForD8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .setMinApi(parameters)
+ .setMode(compilationMode)
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::inspect)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ parameters.assumeR8TestParameters();
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters)
+ .setMode(compilationMode)
+ .enableInliningAnnotations()
+ .addDontObfuscate()
+ .run(parameters.getRuntime(), TestClass.class)
+ .inspect(this::inspect)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ public static final class TestClass {
+
+ @NeverInline
+ public static void m1() {
+ String[][] array = {
+ new String[] {"A", "B"},
+ new String[] {"C", "D"},
+ new String[] {"E", "F", "G"},
+ new String[] {"H", "I", "J"}
+ };
+ printArray(array);
+ System.out.println();
+ }
+
+ @NeverInline
+ public static void m2() {
+ try {
+ String[][] array = {
+ new String[] {"J", "I", "H"},
+ new String[] {"G", "F", "E"},
+ new String[] {"D", "C"},
+ new String[] {"B", "A"}
+ };
+ printArray(array);
+ System.out.println();
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+ }
+
+ @NeverInline
+ public static void m3() {
+ String[][][] array = {
+ new String[][] {new String[] {"A", "B", "C"}},
+ new String[][] {new String[] {"D", "E", "F"}},
+ new String[][] {new String[] {"G", "H"}},
+ new String[][] {new String[] {"I", "J"}}
+ };
+ printArray(array);
+ System.out.println();
+ }
+
+ @NeverInline
+ public static void m4() {
+ try {
+ String[][][] array = {
+ new String[][] {new String[] {"J", "I", "H"}},
+ new String[][] {new String[] {"G", "F", "E"}},
+ new String[][] {new String[] {"D", "C"}},
+ new String[][] {new String[] {"B", "A"}}
+ };
+ printArray(array);
+ System.out.println();
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+ }
+
+ @NeverInline
+ public static void m5() {
+ String[] a1 = new String[] {"J", "I", "H"};
+ String[] a2 = new String[] {"G", "F", "E"};
+ String[] a3 = new String[] {"D", "C"};
+ String[] a4 = new String[] {"B", "A"};
+ try {
+ String[][] array = {a1, a2, a3, a4};
+ printArray(array);
+ System.out.println();
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+ }
+
+ @NeverInline
+ public static void printArray(String[][][] array) {
+ System.out.print("[");
+ for (int i = 0; i < array.length; i++) {
+ if (i != 0) {
+ System.out.print(", ");
+ }
+ printArray(array[i]);
+ }
+ System.out.print("]");
+ }
+
+ @NeverInline
+ public static void printArray(String[][] array) {
+ System.out.print("[");
+ for (int i = 0; i < array.length; i++) {
+ if (i != 0) {
+ System.out.print(", ");
+ }
+ printArray(array[i]);
+ }
+ System.out.print("]");
+ }
+
+ @NeverInline
+ public static void printArray(String[] array) {
+ System.out.print("[");
+ for (int i = 0; i < array.length; i++) {
+ if (i != 0) {
+ System.out.print(", ");
+ }
+ System.out.print(array[i]);
+ }
+ System.out.print("]");
+ }
+
+ public static void main(String[] args) {
+ m1();
+ m2();
+ m3();
+ m4();
+ m5();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
index 180c412..4b6e64c 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayTest.java
@@ -41,11 +41,6 @@
private static final String EXPECTED_OUTPUT =
StringUtils.lines("[A, B, C, D, E]", "[E, D, C, B, A]");
- public boolean canUseFilledNewArrayOfNonStringObjects(TestParameters parameters) {
- return parameters.isDexRuntime()
- && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
- }
-
private enum State {
EXPECTING_CONSTCLASS,
EXPECTING_APUTOBJECT
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
index 213e3e4..3a364dd 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
@@ -12,7 +12,6 @@
import com.android.tools.r8.TestCompilerBuilder;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -47,15 +46,10 @@
private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "104");
- public boolean canUseFilledNewArrayOfClass(TestParameters parameters) {
- return parameters.isDexRuntime()
- && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
- }
-
private void inspect(
MethodSubject method, int constClasses, int puts, boolean insideCatchHandler) {
boolean expectingFilledNewArray =
- canUseFilledNewArrayOfClass(parameters) && !insideCatchHandler;
+ canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
assertEquals(
expectingFilledNewArray ? 0 : puts,
method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
index d8227dc..c7f847b 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
@@ -11,7 +11,6 @@
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -48,11 +47,6 @@
private static final String EXPECTED_OUTPUT =
StringUtils.lines("[A00, A01, A02, A03, A04]", "[A00, A01, A02, A03, A04]", "100");
- public boolean canUseFilledNewArrayOfClass(TestParameters parameters) {
- return parameters.isDexRuntime()
- && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
- }
-
private enum State {
EXPECTING_CONSTCLASS,
EXPECTING_APUTOBJECT
@@ -60,7 +54,7 @@
private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
boolean expectingFilledNewArray =
- canUseFilledNewArrayOfClass(parameters) && !insideCatchHandler;
+ canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
assertEquals(
expectingFilledNewArray ? 0 : puts,
method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/IntegerArrayTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/IntegerArrayTest.java
index 54abbcd..1c159da 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/IntegerArrayTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/IntegerArrayTest.java
@@ -41,10 +41,6 @@
StringUtils.lines(
"[-2147483648, -1, 0, 1, 2147483647]", "[-2147483647, -2, 0, 2, 2147483646]");
- public boolean canUseFilledNewArrayOfInteger(TestParameters parameters) {
- return parameters.isDexRuntime();
- }
-
private void inspect(MethodSubject main) {
assertEquals(
canUseFilledNewArrayOfInteger(parameters) ? 0 : 5,
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
index 3f75ac1..c80436c 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
@@ -212,14 +212,12 @@
assertArrayTypes(reversedArray, DexFilledNewArray.class);
}
- // Cannot use filled-new-array of String before K.
- if (parameters.getApiLevel().isLessThan(AndroidApiLevel.K)) {
+ if (!canUseFilledNewArrayOfStringObjects(parameters)) {
assertArrayTypes(stringArrays, DexNewArray.class);
} else {
assertArrayTypes(stringArrays, DexFilledNewArray.class);
}
- // Cannot use filled-new-array of Object before L.
- if (parameters.getApiLevel().isLessThan(AndroidApiLevel.N)) {
+ if (!canUseFilledNewArrayOfNonStringObjects(parameters)) {
assertArrayTypes(referenceArraysNoCasts, DexNewArray.class);
assertArrayTypes(referenceArraysWithSubclasses, DexNewArray.class);
assertArrayTypes(referenceArraysWithInterfaceImplementations, DexNewArray.class);
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
index 8623e55..be54d34 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithNonUniqueValuesTest.java
@@ -12,7 +12,6 @@
import com.android.tools.r8.TestCompilerBuilder;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -47,14 +46,9 @@
private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "50");
- public boolean canUseFilledNewArrayOfObject(TestParameters parameters) {
- return parameters.isDexRuntime()
- && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
- }
-
private void inspect(MethodSubject method, int staticGets, int puts, boolean insideCatchHandler) {
boolean expectingFilledNewArray =
- canUseFilledNewArrayOfObject(parameters) && !insideCatchHandler;
+ canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
assertEquals(
expectingFilledNewArray ? 0 : puts,
method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
@@ -74,12 +68,14 @@
private void inspectD8(CodeInspector inspector) {
inspect(
inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"),
- canUseFilledNewArrayOfObject(parameters) ? 100 : 1,
+ canUseFilledNewArrayOfNonStringObjects(parameters) ? 100 : 1,
100,
false);
inspect(
inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"),
- canUseFilledNewArrayOfObject(parameters) ? 50 : (maxMaterializingConstants == 2 ? 42 : 10),
+ canUseFilledNewArrayOfNonStringObjects(parameters)
+ ? 50
+ : (maxMaterializingConstants == 2 ? 42 : 10),
50,
false);
}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
index b5fe270..fce1e79 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StaticGetArrayWithUniqueValuesTest.java
@@ -11,7 +11,6 @@
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -48,11 +47,6 @@
private static final String EXPECTED_OUTPUT =
StringUtils.lines("[A00, A01, A02, A03, A04]", "[A00, A01, A02, A03, A04]", "100");
- public boolean canUseFilledNewArrayOfObject(TestParameters parameters) {
- return parameters.isDexRuntime()
- && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
- }
-
private enum State {
EXPECTING_GETSTATIC,
EXPECTING_APUTOBJECT
@@ -60,7 +54,7 @@
private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
boolean expectingFilledNewArray =
- canUseFilledNewArrayOfObject(parameters) && !insideCatchHandler;
+ canUseFilledNewArrayOfNonStringObjects(parameters) && !insideCatchHandler;
assertEquals(
expectingFilledNewArray ? 0 : puts,
method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
index 9100e39..c433e6d 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
@@ -12,7 +12,6 @@
import com.android.tools.r8.TestCompilerBuilder;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -47,15 +46,10 @@
private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "104");
- public boolean canUseFilledNewArrayOfStrings(TestParameters parameters) {
- return parameters.isDexRuntime()
- && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
- }
-
private void inspect(
MethodSubject method, int constStrings, int puts, boolean insideCatchHandler) {
boolean expectingFilledNewArray =
- canUseFilledNewArrayOfStrings(parameters) && !insideCatchHandler;
+ canUseFilledNewArrayOfStringObjects(parameters) && !insideCatchHandler;
assertEquals(
expectingFilledNewArray ? 0 : puts,
method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
index 2613ca5..837bea2 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
@@ -11,7 +11,6 @@
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -48,11 +47,6 @@
private static final String EXPECTED_OUTPUT =
StringUtils.lines("[A, B, C, D, E]", "[F, G, H, I, J]", "100");
- public boolean canUseFilledNewArrayOfStrings(TestParameters parameters) {
- return parameters.isDexRuntime()
- && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
- }
-
private enum State {
EXPECTING_CONSTSTRING,
EXPECTING_APUTOBJECT
@@ -60,7 +54,7 @@
private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
boolean expectingFilledNewArray =
- canUseFilledNewArrayOfStrings(parameters) && !insideCatchHandler;
+ canUseFilledNewArrayOfStringObjects(parameters) && !insideCatchHandler;
assertEquals(
expectingFilledNewArray ? 0 : puts,
method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
diff --git a/tools/archive.py b/tools/archive.py
index a906a82..d9f8335 100755
--- a/tools/archive.py
+++ b/tools/archive.py
@@ -122,6 +122,20 @@
print('INFO: Open files hard limit: %s' % hard)
+def RSyncDir(src_dir, version_or_path, dst_dir, is_main, options):
+ destination = GetUploadDestination(version_or_path, dst_dir, is_main)
+ print(f'RSyncing {src_dir} to {destination}')
+ if options.dry_run:
+ if options.dry_run_output:
+ dry_run_destination = os.path.join(options.dry_run_output, version_or_path, dst_dir)
+ print(f'Dry run, not actually syncing. Copying to {dry_run_destination}')
+ shutil.copytree(src_dir, dry_run_destination)
+ else:
+ print('Dry run, not actually uploading')
+ else:
+ utils.rsync_directory_to_cloud_storage(src_dir, destination)
+ print(f'Directory available at: {GetUrl(version_or_path, dst_dir, is_main)}')
+
def UploadDir(src_dir, version_or_path, dst_dir, is_main, options):
destination = GetUploadDestination(version_or_path, dst_dir, is_main)
print(f'Uploading {src_dir} to {destination}')
@@ -250,7 +264,7 @@
if is_main:
version_or_path = 'docs'
dst_dir = 'keepanno/javadoc'
- UploadDir(utils.KEEPANNO_ANNOTATIONS_DOC, version_or_path, dst_dir, is_main, options)
+ RSyncDir(utils.KEEPANNO_ANNOTATIONS_DOC, version_or_path, dst_dir, is_main, options)
# Upload directories.
dirs_for_archiving = [
diff --git a/tools/utils.py b/tools/utils.py
index e859748..1df6d83 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -393,11 +393,36 @@
PrintCmd(cmd)
subprocess.check_call(cmd)
+def check_dir_args(source, destination):
+ # We require that the dirname of the paths coincide, e.g., src/dirname and dst/dirname
+ # The target is then stripped so the upload command will be: cp -R src/dirname dst/
+ (destination_parent, destination_file) = os.path.split(destination)
+ if os.path.basename(source) != destination_file:
+ raise Exception(
+ 'Attempt to upload directory with non-matching directory name: ' +
+ f'{source} and {destination}')
+ if len(destination_parent.strip()) == 0:
+ raise Exception(
+ 'Attempt to upload directory to empty destination directory: '
+ + destination)
+ return destination_parent
+
def upload_directory_to_cloud_storage(source, destination, parallel=True):
+ destination_parent = check_dir_args(source, destination)
cmd = [get_gsutil()]
if parallel:
cmd += ['-m']
cmd += ['cp', '-R']
+ cmd += [source, destination_parent + '/']
+ PrintCmd(cmd)
+ subprocess.check_call(cmd)
+
+def rsync_directory_to_cloud_storage(source, destination, parallel=True):
+ check_dir_args(source, destination)
+ cmd = [get_gsutil()]
+ if parallel:
+ cmd += ['-m']
+ cmd += ['rsync', '-R']
cmd += [source, destination]
PrintCmd(cmd)
subprocess.check_call(cmd)