Version 2.0.63
Cherry-pick: Rewrite invoke-direct to default methods in parent
interfaces
CL: https://r8-review.googlesource.com/50262
Cherry-pick: Desugar default call to looked up virtual method
CL: https://r8-review.googlesource.com/50290
Cherry-pick: Add a test for renaming of lambda methods to mapped names
CL: https://r8-review.googlesource.com/50147
Cherry-pick: Reserve mapped to names to prevent clashes at runtime
CL: https://r8-review.googlesource.com/50181
Cherry-pick: Rewrite invoke-static getter users to use staticized methods
CL: https://r8-review.googlesource.com/50300
Cherry-pick: Reland "Do not write zip output to invalid file."
CL: https://r8-review.googlesource.com/50294
Cherry-pick: Update CompileToInvalidFileTest to use both cf and dex
consumers
CL: https://r8-review.googlesource.com/50302
Bug: 153042496
Bug: 152715309
Bug: 152800551
Bug: 152973695
Change-Id: I4048107a167c30585c9da6a4e74d09ace9f012cf
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index e53349e..b9b47c0 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
// This field is accessed from release scripts using simple pattern matching.
// Therefore, changing this field could break our release scripts.
- public static final String LABEL = "2.0.62";
+ public static final String LABEL = "2.0.63";
private Version() {
}
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index ad19ed4..b830c3e 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -436,6 +436,16 @@
return builder.resolve();
}
+ // Non-private lookup (ie, not resolution) to find interface targets.
+ public DexEncodedMethod lookupMaximallySpecificTarget(DexClass clazz, DexMethod method) {
+ MaximallySpecificMethodsBuilder builder = new MaximallySpecificMethodsBuilder(clazz);
+ resolveMethodStep3Helper(clazz, method, builder);
+ ResolutionResult resolution = builder.resolve();
+ return resolution.isSingleResolution()
+ ? resolution.asSingleResolution().getResolvedMethod()
+ : null;
+ }
+
/** Helper method that builds the set of maximally specific methods. */
private void resolveMethodStep3Helper(
DexClass clazz, DexMethod method, MaximallySpecificMethodsBuilder builder) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 5ff8e57..7ca1e54 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -327,38 +327,46 @@
if (clazz == null) {
// Report missing class since we don't know if it is an interface.
warnMissingType(encodedMethod.method, method.holder);
-
} else if (clazz.isInterface()) {
if (clazz.isLibraryClass()) {
throw new CompilationError("Unexpected call to a private method " +
"defined in library class " + clazz.toSourceString(),
getMethodOrigin(encodedMethod.method));
}
-
- // This might be either private method call, or a call to default
- // interface method made via invoke-direct.
- DexEncodedMethod virtualTarget = null;
- for (DexEncodedMethod candidate : clazz.virtualMethods()) {
- if (candidate.method == method) {
- virtualTarget = candidate;
- break;
- }
- }
-
- if (virtualTarget != null) {
- // This is a invoke-direct call to a virtual method.
- instructions.replaceCurrentInstruction(
- new InvokeStatic(defaultAsMethodOfCompanionClass(method),
- invokeDirect.outValue(), invokeDirect.arguments()));
-
- } else {
- // Otherwise this must be a private instance method call. Note that the referenced
+ DexEncodedMethod directTarget = appView.definitionFor(method);
+ if (directTarget != null) {
+ // This can be a private instance method call. Note that the referenced
// method is expected to be in the current class since it is private, but desugaring
// may move some methods or their code into other classes.
-
- instructions.replaceCurrentInstruction(
- new InvokeStatic(privateAsMethodOfCompanionClass(method),
- invokeDirect.outValue(), invokeDirect.arguments()));
+ if (directTarget.isPrivateMethod()) {
+ instructions.replaceCurrentInstruction(
+ new InvokeStatic(
+ privateAsMethodOfCompanionClass(method),
+ invokeDirect.outValue(),
+ invokeDirect.arguments()));
+ } else {
+ instructions.replaceCurrentInstruction(
+ new InvokeStatic(
+ defaultAsMethodOfCompanionClass(method),
+ invokeDirect.outValue(),
+ invokeDirect.arguments()));
+ }
+ } else {
+ // The method can be a default method in the interface hierarchy.
+ DexEncodedMethod virtualTarget =
+ appView.appInfo().lookupMaximallySpecificTarget(clazz, method);
+ if (virtualTarget != null) {
+ // This is a invoke-direct call to a virtual method.
+ instructions.replaceCurrentInstruction(
+ new InvokeStatic(
+ defaultAsMethodOfCompanionClass(virtualTarget.method),
+ invokeDirect.outValue(),
+ invokeDirect.arguments()));
+ } else {
+ // The below assert is here because a well-type program should have a target, but we
+ // cannot throw a compilation error, since we have no knowledge about the input.
+ assert false;
+ }
}
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
index e019acd..3bc9825 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.graph.Descriptor;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
@@ -65,6 +66,7 @@
private final Map<DexEncodedMethod, CandidateInfo> hostClassInits = new IdentityHashMap<>();
private final Set<DexEncodedMethod> methodsToBeStaticized = Sets.newIdentityHashSet();
private final Map<DexField, CandidateInfo> singletonFields = new IdentityHashMap<>();
+ private final Map<DexMethod, CandidateInfo> singletonGetters = new IdentityHashMap<>();
private final Map<DexType, DexType> candidateToHostMapping = new IdentityHashMap<>();
StaticizingProcessor(
@@ -94,7 +96,7 @@
.add(collectOptimizationInfo(feedback)));
// Enqueue instance methods to be staticized (only remove references to 'this'). Intentionally
- // not collection optimization info for these methods, since they will be reprocessed again
+ // not collecting optimization info for these methods, since they will be reprocessed again
// below once staticized.
enqueueMethodsWithCodeOptimizations(
methodsToBeStaticized, optimizations -> optimizations.add(this::removeReferencesToThis));
@@ -203,19 +205,36 @@
for (DexEncodedMethod method : info.referencedFrom) {
IRCode code = method.buildIR(appView, appView.appInfo().originFor(method.method.holder));
assert code != null;
- List<StaticGet> singletonFieldReads =
+ List<Instruction> singletonUsers =
Streams.stream(code.instructionIterator())
- .filter(Instruction::isStaticGet)
- .map(Instruction::asStaticGet)
- .filter(get -> get.getField() == info.singletonField.field)
+ .filter(
+ instruction -> {
+ if (instruction.isStaticGet()
+ && instruction.asStaticGet().getField() == info.singletonField.field) {
+ return true;
+ }
+ DexEncodedMethod getter = info.getter.get();
+ return getter != null
+ && instruction.isInvokeStatic()
+ && instruction.asInvokeStatic().getInvokedMethod() == getter.method;
+ })
.collect(Collectors.toList());
boolean fixableFieldReadsPerUsage = true;
- for (StaticGet read : singletonFieldReads) {
- Value dest = read.dest();
+ for (Instruction user : singletonUsers) {
+ if (user.outValue() == null) {
+ continue;
+ }
+ Value dest = user.outValue();
visited.clear();
trivialPhis.clear();
- boolean onlyHasTrivialPhis = testAndCollectPhisComposedOfSameFieldRead(
- visited, dest.uniquePhiUsers(), read.getField(), trivialPhis);
+ assert user.isInvokeStatic() || user.isStaticGet();
+ Descriptor member =
+ user.isStaticGet()
+ ? user.asStaticGet().getField()
+ : user.asInvokeStatic().getInvokedMethod();
+ boolean onlyHasTrivialPhis =
+ testAndCollectPhisComposedOfSameMember(
+ visited, dest.uniquePhiUsers(), member, trivialPhis);
if (dest.hasPhiUsers() && !onlyHasTrivialPhis) {
fixableFieldReadsPerUsage = false;
break;
@@ -255,6 +274,10 @@
}
}
singletonFields.put(candidate.singletonField.field, candidate);
+ DexEncodedMethod getter = candidate.getter.get();
+ if (getter != null) {
+ singletonGetters.put(getter.method, candidate);
+ }
referencingExtraMethods.addAll(candidate.referencedFrom);
}
@@ -341,28 +364,38 @@
}
private void rewriteReferences(IRCode code) {
- // Process all singleton field reads and rewrite their users.
- List<StaticGet> singletonFieldReads =
+ // Fetch all instructions that reference singletons to avoid concurrent modifications to the
+ // instruction list that can arise from doing it directly in the iterator.
+ List<Instruction> singletonUsers =
Streams.stream(code.instructionIterator())
- .filter(Instruction::isStaticGet)
- .map(Instruction::asStaticGet)
- .filter(get -> singletonFields.containsKey(get.getField()))
+ .filter(
+ instruction ->
+ (instruction.isStaticGet()
+ && singletonFields.containsKey(
+ instruction.asFieldInstruction().getField()))
+ || (instruction.isInvokeStatic()
+ && singletonGetters.containsKey(
+ instruction.asInvokeStatic().getInvokedMethod())))
.collect(Collectors.toList());
-
- singletonFieldReads.forEach(
- read -> {
- DexField field = read.getField();
- CandidateInfo candidateInfo = singletonFields.get(field);
- assert candidateInfo != null;
- Value value = read.dest();
- if (value != null) {
- fixupStaticizedFieldReadUsers(code, value, field);
- }
- if (!candidateInfo.preserveRead.get()) {
- read.removeOrReplaceByDebugLocalRead(code);
- }
- });
-
+ for (Instruction singletonUser : singletonUsers) {
+ CandidateInfo candidateInfo;
+ Descriptor member;
+ if (singletonUser.isStaticGet()) {
+ candidateInfo = singletonFields.get(singletonUser.asStaticGet().getField());
+ member = singletonUser.asStaticGet().getField();
+ } else {
+ assert singletonUser.isInvokeStatic();
+ candidateInfo = singletonGetters.get(singletonUser.asInvokeStatic().getInvokedMethod());
+ member = singletonUser.asInvokeStatic().getInvokedMethod();
+ }
+ Value value = singletonUser.outValue();
+ if (value != null) {
+ fixupStaticizedFieldUsers(code, value, member);
+ }
+ if (!candidateInfo.preserveRead.get()) {
+ singletonUser.removeOrReplaceByDebugLocalRead(code);
+ }
+ }
if (!candidateToHostMapping.isEmpty()) {
remapMovedCandidates(code);
}
@@ -429,7 +462,7 @@
// invoke-virtual { s1, ... } mtd1
// goto Exit
// b2:
- // s2 <- static-get singleton
+ // s2 <- invoke-static getter()
// ...
// invoke-virtual { s2, ... } mtd1
// goto Exit
@@ -443,7 +476,7 @@
// ...
// goto Exit
// b2:
- // s2 <- static-get singleton
+ // s2 <- invoke-static getter()
// ...
// goto Exit
// Exit:
@@ -454,8 +487,8 @@
// From staticizer's viewpoint, `sp` is trivial in the sense that it is composed of values that
// refer to the same singleton field. If so, we can safely relax the assertion; remove uses of
// field reads; remove quasi-trivial phis; and then remove original field reads.
- private boolean testAndCollectPhisComposedOfSameFieldRead(
- Set<Phi> visited, Set<Phi> phisToCheck, DexField field, Set<Phi> trivialPhis) {
+ private boolean testAndCollectPhisComposedOfSameMember(
+ Set<Phi> visited, Set<Phi> phisToCheck, Descriptor dexMember, Set<Phi> trivialPhis) {
for (Phi phi : phisToCheck) {
if (!visited.add(phi)) {
continue;
@@ -466,16 +499,20 @@
if (v.isPhi()) {
chainedPhis.add(operand.asPhi());
} else {
- if (!v.definition.isStaticGet()) {
+ Instruction definition = v.definition;
+ if (!definition.isStaticGet() && !definition.isInvokeStatic()) {
return false;
}
- if (v.definition.asStaticGet().getField() != field) {
+ if (definition.isStaticGet() && definition.asStaticGet().getField() != dexMember) {
+ return false;
+ } else if (definition.isInvokeStatic()
+ && definition.asInvokeStatic().getInvokedMethod() != dexMember) {
return false;
}
}
}
if (!chainedPhis.isEmpty()) {
- if (!testAndCollectPhisComposedOfSameFieldRead(visited, chainedPhis, field, trivialPhis)) {
+ if (!testAndCollectPhisComposedOfSameMember(visited, chainedPhis, dexMember, trivialPhis)) {
return false;
}
}
@@ -486,13 +523,14 @@
// Fixup field read usages. Same as {@link #fixupStaticizedThisUsers} except this one determines
// quasi-trivial phis, based on the original field.
- private void fixupStaticizedFieldReadUsers(IRCode code, Value dest, DexField field) {
+ private void fixupStaticizedFieldUsers(IRCode code, Value dest, Descriptor member) {
assert dest != null;
// During the examine phase, field reads with any phi users have been invalidated, hence zero.
// However, it may be not true if re-processing introduces phis after optimizing common suffix.
Set<Phi> trivialPhis = Sets.newIdentityHashSet();
- boolean onlyHasTrivialPhis = testAndCollectPhisComposedOfSameFieldRead(
- Sets.newIdentityHashSet(), dest.uniquePhiUsers(), field, trivialPhis);
+ boolean onlyHasTrivialPhis =
+ testAndCollectPhisComposedOfSameMember(
+ Sets.newIdentityHashSet(), dest.uniquePhiUsers(), member, trivialPhis);
assert !dest.hasPhiUsers() || onlyHasTrivialPhis;
assert trivialPhis.isEmpty() || onlyHasTrivialPhis;
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
index 1a07b28..09aaea5 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
@@ -133,7 +133,8 @@
ClassNameMinifier classNameMinifier =
new ClassNameMinifier(
appView,
- new ApplyMappingClassNamingStrategy(appView, mappedNames),
+ new ApplyMappingClassNamingStrategy(
+ appView, mappedNames, seedMapper.getMappedToDescriptorNames()),
// The package naming strategy will actually not be used since all classes and methods
// will be output with identity name if not found in mapping. However, there is a check
// in the ClassNameMinifier that the strategy should produce a "fresh" name so we just
@@ -391,10 +392,13 @@
static class ApplyMappingClassNamingStrategy extends MinificationClassNamingStrategy {
private final Map<DexType, DexString> mappings;
+ private final Set<String> mappedNames;
- ApplyMappingClassNamingStrategy(AppView<?> appView, Map<DexType, DexString> mappings) {
+ ApplyMappingClassNamingStrategy(
+ AppView<?> appView, Map<DexType, DexString> mappings, Set<String> mappedNames) {
super(appView);
this.mappings = mappings;
+ this.mappedNames = mappedNames;
}
@Override
@@ -405,7 +409,16 @@
Predicate<DexString> isUsed) {
assert !mappings.containsKey(type);
assert appView.rootSet().mayBeMinified(type, appView);
- return super.next(type, packagePrefix, state, isUsed);
+ return super.next(
+ type,
+ packagePrefix,
+ state,
+ candidate -> {
+ if (mappedNames.contains(candidate.toString())) {
+ return true;
+ }
+ return isUsed.test(candidate);
+ });
}
@Override
diff --git a/src/main/java/com/android/tools/r8/naming/SeedMapper.java b/src/main/java/com/android/tools/r8/naming/SeedMapper.java
index d7f36da..be69416 100644
--- a/src/main/java/com/android/tools/r8/naming/SeedMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/SeedMapper.java
@@ -20,6 +20,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -37,6 +38,7 @@
static class Builder extends ProguardMap.Builder {
final Map<String, ClassNamingForMapApplier.Builder> map = new HashMap<>();
+ final Set<String> mappedToDescriptorNames = new HashSet<>();
private final Reporter reporter;
private Builder(Reporter reporter) {
@@ -47,9 +49,11 @@
ClassNamingForMapApplier.Builder classNamingBuilder(
String renamedName, String originalName, Position position) {
String originalDescriptor = javaTypeToDescriptor(originalName);
+ String renamedDescriptorName = javaTypeToDescriptor(renamedName);
+ mappedToDescriptorNames.add(renamedDescriptorName);
ClassNamingForMapApplier.Builder classNamingBuilder =
ClassNamingForMapApplier.builder(
- javaTypeToDescriptor(renamedName), originalDescriptor, position, reporter);
+ renamedDescriptorName, originalDescriptor, position, reporter);
if (map.put(originalDescriptor, classNamingBuilder) != null) {
reporter.error(ProguardMapError.duplicateSourceClass(originalName, position));
}
@@ -59,7 +63,7 @@
@Override
SeedMapper build() {
reporter.failIfPendingErrors();
- return new SeedMapper(ImmutableMap.copyOf(map), reporter);
+ return new SeedMapper(ImmutableMap.copyOf(map), mappedToDescriptorNames, reporter);
}
}
@@ -82,15 +86,20 @@
}
private final ImmutableMap<String, ClassNamingForMapApplier> mappings;
+ private final Set<String> mappedToDescriptorNames;
private final Reporter reporter;
- private SeedMapper(Map<String, ClassNamingForMapApplier.Builder> mappings, Reporter reporter) {
+ private SeedMapper(
+ Map<String, ClassNamingForMapApplier.Builder> mappings,
+ Set<String> mappedToDescriptorNames,
+ Reporter reporter) {
this.reporter = reporter;
ImmutableMap.Builder<String, ClassNamingForMapApplier> builder = ImmutableMap.builder();
for(Map.Entry<String, ClassNamingForMapApplier.Builder> entry : mappings.entrySet()) {
builder.put(entry.getKey(), entry.getValue().build());
}
this.mappings = builder.build();
+ this.mappedToDescriptorNames = mappedToDescriptorNames;
verifyMappingsAreConflictFree();
}
@@ -140,6 +149,10 @@
return mappings.keySet();
}
+ public Set<String> getMappedToDescriptorNames() {
+ return mappedToDescriptorNames;
+ }
+
public ClassNamingForMapApplier getMapping(String key) {
return mappings.get(key);
}
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
index 2276790..a47684f 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
@@ -86,14 +86,9 @@
}
/** Get or open the zip output stream. */
- private synchronized ZipOutputStream getStream(DiagnosticsHandler handler) {
+ private synchronized ZipOutputStream getStream() throws IOException {
assert !closed;
- try {
- getStreamRaw();
- } catch (IOException e) {
- handler.error(new ExceptionDiagnostic(e, origin));
- }
- return stream;
+ return getStreamRaw();
}
private void handleIOException(IOException e, DiagnosticsHandler handler) {
@@ -117,9 +112,9 @@
}
ZipEntry entry = new ZipEntry(name);
entry.setTime(0);
- ZipOutputStream zip = getStream(handler);
synchronized (this) {
try {
+ ZipOutputStream zip = getStream();
zip.putNextEntry(entry);
zip.closeEntry();
} catch (IOException e) {
@@ -150,7 +145,7 @@
private void writeFileNow(String name, ByteDataView content, DiagnosticsHandler handler) {
try {
- ZipUtils.writeToZipStream(getStream(handler), name, content, ZipEntry.DEFLATED);
+ ZipUtils.writeToZipStream(getStream(), name, content, ZipEntry.DEFLATED);
} catch (IOException e) {
handleIOException(e, handler);
}
diff --git a/src/test/java/com/android/tools/r8/desugar/DefaultMethodWithAccessTest.java b/src/test/java/com/android/tools/r8/desugar/DefaultMethodWithAccessTest.java
new file mode 100644
index 0000000..0c778d0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/DefaultMethodWithAccessTest.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Opcodes;
+// This is a reproduction of b/153042496 in a java-only setting.
+
+@RunWith(Parameterized.class)
+public class DefaultMethodWithAccessTest extends TestBase {
+
+ private final TestParameters parameters;
+ private final boolean implementI0I1;
+
+ @Parameters(name = "{0}, implementI0I1: {1}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
+ }
+
+ public DefaultMethodWithAccessTest(TestParameters parameters, boolean implementI0I1) {
+ this.parameters = parameters;
+ this.implementI0I1 = implementI0I1;
+ }
+
+ @Test
+ public void testRuntime() throws ExecutionException, CompilationFailedException, IOException {
+ testForRuntime(parameters)
+ .addProgramClasses(I0.class, I1.class, Main.class, Impl.class)
+ .addProgramClassFileData(transformI2AccessToInvokeSpecial())
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("Hello World!");
+ }
+
+ private byte[] transformI2AccessToInvokeSpecial() throws IOException {
+ ClassFileTransformer classFileTransformer =
+ transformer(I2.class)
+ .transformMethodInsnInMethod(
+ "access",
+ (opcode, owner, name, descriptor, isInterface, continuation) -> {
+ continuation.apply(
+ name.equals("print") ? Opcodes.INVOKESPECIAL : opcode,
+ owner,
+ name,
+ descriptor,
+ isInterface);
+ });
+ if (implementI0I1) {
+ classFileTransformer.setImplements(I0.class, I1.class);
+ }
+ return classFileTransformer.transform();
+ }
+
+ public interface I0 {
+ void print();
+ }
+
+ public interface I1 {
+ default void print() {
+ System.out.println("Hello World!");
+ }
+ }
+
+ public interface I2 extends /* I0, */ I1 {
+
+ static void access(I2 i2) {
+ /* invoke-special */ i2.print();
+ }
+ }
+
+ public static class Impl implements I2 {}
+
+ public static class Main {
+
+ public static void main(String[] args) {
+ testPrint(new Impl());
+ }
+
+ public static void testPrint(I2 i) {
+ I2.access(i);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/PrivateMethodsInInterfaceTest.java b/src/test/java/com/android/tools/r8/desugar/PrivateMethodsInInterfaceTest.java
new file mode 100644
index 0000000..564f831
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/PrivateMethodsInInterfaceTest.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class PrivateMethodsInInterfaceTest extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public PrivateMethodsInInterfaceTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testRuntime()
+ throws NoSuchMethodException, IOException, CompilationFailedException, ExecutionException {
+ testForRuntime(parameters)
+ .addProgramClasses(SubI.class, Impl.class, Main.class)
+ .addProgramClassFileData(transformIToPrivate())
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines("Hello World!", "Hello World!", "Hello World!");
+ }
+
+ private byte[] transformIToPrivate() throws NoSuchMethodException, IOException {
+ return transformer(I.class)
+ .setPrivate(I.class.getDeclaredMethod("bar"))
+ .setPrivate(I.class.getDeclaredMethod("baz", I.class))
+ .transformMethodInsnInMethod(
+ "foo",
+ ((opcode, owner, name, descriptor, isInterface, continuation) -> {
+ continuation.apply(
+ name.equals("bar") ? Opcodes.INVOKESPECIAL : opcode,
+ owner,
+ name,
+ descriptor,
+ isInterface);
+ }))
+ .transformMethodInsnInMethod(
+ "baz",
+ ((opcode, owner, name, descriptor, isInterface, continuation) -> {
+ continuation.apply(
+ name.equals("bar") ? Opcodes.INVOKESPECIAL : opcode,
+ owner,
+ name,
+ descriptor,
+ isInterface);
+ }))
+ .transform();
+ }
+
+ public interface I {
+
+ default void foo() {
+ bar();
+ I.qux(this);
+ }
+
+ /* private */ default void bar() {
+ System.out.println("Hello World!");
+ }
+
+ /* private */ static void baz(I i) {
+ i.bar();
+ }
+
+ static void qux(I i) {
+ baz(i);
+ }
+ }
+
+ public interface SubI extends I {}
+
+ public static class Impl implements SubI {}
+
+ public static class Main {
+
+ public static void main(String[] args) {
+ Impl impl = new Impl();
+ impl.foo();
+ I.qux(impl);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index d0a4064..5cd6a9a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -12,6 +12,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.NeverInline;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
@@ -50,11 +51,14 @@
import com.android.tools.r8.ir.optimize.staticizer.trivial.TrivialTestClass;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.google.common.collect.Lists;
import com.google.common.collect.Streams;
+import java.io.IOException;
import java.util.List;
+import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -64,6 +68,37 @@
public class ClassStaticizerTest extends TestBase {
private final TestParameters parameters;
+ private static final String EXPECTED =
+ StringUtils.lines(
+ "Simple::bar(Simple::foo())",
+ "Simple::bar(0)",
+ "SimpleWithPhi$Companion::bar(SimpleWithPhi$Companion::foo()) true",
+ "SimpleWithSideEffects::<clinit>()",
+ "SimpleWithSideEffects::bar(SimpleWithSideEffects::foo())",
+ "SimpleWithSideEffects::bar(1)",
+ "SimpleWithParams::bar(SimpleWithParams::foo())",
+ "SimpleWithParams::bar(2)",
+ "SimpleWithGetter::bar(SimpleWithGetter::foo())",
+ "SimpleWithGetter::bar(3)",
+ "Simple::bar(Simple::foo())",
+ "Simple::bar(4)",
+ "Simple::bar(Simple::foo())",
+ "Simple::bar(5)");
+
+ private static final Class<?> main = TrivialTestClass.class;
+ private static final Class<?>[] classes = {
+ NeverInline.class,
+ TrivialTestClass.class,
+ Simple.class,
+ SimpleWithGetter.class,
+ SimpleWithLazyInit.class,
+ SimpleWithParams.class,
+ SimpleWithPhi.class,
+ SimpleWithPhi.Companion.class,
+ SimpleWithSideEffects.class,
+ SimpleWithThrowingGetter.class
+ };
+
@Parameterized.Parameters(name = "{0}")
public static TestParametersCollection data() {
// TODO(b/112831361): support for class staticizer in CF backend.
@@ -75,21 +110,20 @@
}
@Test
+ public void testWithoutAccessModification()
+ throws ExecutionException, CompilationFailedException, IOException {
+ testForR8(parameters.getBackend())
+ .addProgramClasses(classes)
+ .addKeepMainRule(main)
+ .addKeepAttributes("InnerClasses", "EnclosingMethod")
+ .addOptionsModification(this::configure)
+ .setMinApi(parameters.getApiLevel())
+ .run(parameters.getRuntime(), main)
+ .assertSuccessWithOutput(EXPECTED);
+ }
+
+ @Test
public void testTrivial() throws Exception {
- Class<?> main = TrivialTestClass.class;
- Class<?>[] classes = {
- NeverInline.class,
- TrivialTestClass.class,
- Simple.class,
- SimpleWithGetter.class,
- SimpleWithLazyInit.class,
- SimpleWithParams.class,
- SimpleWithPhi.class,
- SimpleWithPhi.Companion.class,
- SimpleWithSideEffects.class,
- SimpleWithThrowingGetter.class
- };
- String javaOutput = runOnJava(main);
TestRunResult result =
testForR8(parameters.getBackend())
.addProgramClasses(classes)
@@ -101,7 +135,7 @@
.allowAccessModification()
.setMinApi(parameters.getApiLevel())
.run(parameters.getRuntime(), main)
- .assertSuccessWithOutput(javaOutput);
+ .assertSuccessWithOutput(EXPECTED);
CodeInspector inspector = result.inspector();
ClassSubject clazz = inspector.clazz(main);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
index 55487ac..6bc91b4 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
@@ -15,11 +15,11 @@
@NeverInline
String foo() {
- return bar("Simple::foo()");
+ return bar("SimpleWithGetter::foo()");
}
@NeverInline
String bar(String other) {
- return "Simple::bar(" + other + ")";
+ return "SimpleWithGetter::bar(" + other + ")";
}
}
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingDesugarLambdaTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingDesugarLambdaTest.java
new file mode 100644
index 0000000..63fe00f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingDesugarLambdaTest.java
@@ -0,0 +1,120 @@
+package com.android.tools.r8.naming.applymapping;
+
+import static com.android.tools.r8.Collectors.toSingle;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotSame;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+// This is a reproduction of b/152715309.
+@RunWith(Parameterized.class)
+public class ApplyMappingDesugarLambdaTest extends TestBase {
+
+ private static final String EXPECTED = "FOO";
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+ }
+
+ public ApplyMappingDesugarLambdaTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testR8() throws CompilationFailedException, IOException, ExecutionException {
+ // Create a dictionary to control the naming.
+ Path dictionary = temp.getRoot().toPath().resolve("dictionary.txt");
+ FileUtils.writeTextFile(dictionary, "e");
+
+ final String finalName = "com.android.tools.r8.naming.applymapping.e";
+
+ R8TestCompileResult libraryResult =
+ testForR8(parameters.getBackend())
+ .addProgramClasses(A.class)
+ .addKeepClassAndMembersRulesWithAllowObfuscation(A.class)
+ .setMinApi(parameters.getApiLevel())
+ .addKeepRules(
+ "-keeppackagenames", "-classobfuscationdictionary " + dictionary.toString())
+ .compile()
+ .inspect(
+ inspector -> {
+ assertThat(inspector.clazz(A.class), isRenamed());
+ assertEquals(finalName, inspector.clazz(A.class).getFinalName());
+ });
+
+ Path libraryPath = libraryResult.writeToZip();
+
+ // Ensure that the library works as supposed.
+ testForD8()
+ .addProgramClasses(I.class, Main.class)
+ .addClasspathFiles(libraryPath)
+ .setMinApi(parameters.getApiLevel())
+ .run(parameters.getRuntime(), Main.class, EXPECTED)
+ .assertSuccessWithOutputLines(EXPECTED);
+
+ testForR8(parameters.getBackend())
+ .addClasspathClasses(A.class)
+ .addProgramClasses(I.class, Main.class)
+ .addKeepMainRule(Main.class)
+ .addKeepClassAndMembersRules(I.class)
+ .setMinApi(parameters.getApiLevel())
+ .addApplyMapping(libraryResult.getProguardMap())
+ .addOptionsModification(internalOptions -> internalOptions.enableClassInlining = false)
+ .addKeepRules("-classobfuscationdictionary " + dictionary.toString())
+ .compile()
+ .inspect(
+ inspector -> {
+ // Assert that there is a lambda class created.
+ assertEquals(3, inspector.allClasses().size());
+ FoundClassSubject lambdaClass =
+ inspector.allClasses().stream()
+ .filter(FoundClassSubject::isSynthetic)
+ .collect(toSingle());
+ assertNotSame(finalName, lambdaClass.getFinalName());
+ })
+ .addRunClasspathFiles(libraryPath)
+ .run(parameters.getRuntime(), Main.class, EXPECTED)
+ .assertSuccessWithOutputLines(EXPECTED);
+ }
+
+ public static class A {
+
+ A(int bar) {
+ System.out.println(bar);
+ }
+ }
+
+ @FunctionalInterface
+ public interface I {
+
+ void doStuff();
+ }
+
+ public static class Main {
+
+ public static void main(String[] args) {
+ processI(() -> System.out.println(args[0]));
+ }
+
+ public static void processI(I i) {
+ i.doStuff();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b152973695/CompileToInvalidFileTest.java b/src/test/java/com/android/tools/r8/regress/b152973695/CompileToInvalidFileTest.java
new file mode 100644
index 0000000..d635af8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b152973695/CompileToInvalidFileTest.java
@@ -0,0 +1,104 @@
+// Copyright (c) 2020, 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.regress.b152973695;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class CompileToInvalidFileTest extends TestBase {
+
+ private static final Path INVALID_FILE = Paths.get("!@#/\\INVALID_FILE");
+
+ private final boolean classFileConsumer;
+
+ @Parameterized.Parameters(name = "{0}, classfileConsumer: {1}")
+ public static List<Object[]> data() {
+ return buildParameters(getTestParameters().withNoneRuntime().build(), BooleanUtils.values());
+ }
+
+ public CompileToInvalidFileTest(TestParameters parameters, boolean classFileConsumer) {
+ this.classFileConsumer = classFileConsumer;
+ }
+
+ @Test
+ public void testCompileToInvalidFileD8() {
+ assumeFalse(classFileConsumer);
+ ensureInvalidFileIsInvalid();
+ ProgramConsumer programConsumer =
+ classFileConsumer
+ ? new ClassFileConsumer.ArchiveConsumer(INVALID_FILE)
+ : new ArchiveConsumer(INVALID_FILE);
+ try {
+ testForD8().addProgramClasses(Main.class).setProgramConsumer(programConsumer).compile();
+ fail("Expected a CompilationFailedException but the code succeeded");
+ } catch (CompilationFailedException ex) {
+ assertInvalidFileNotFound(ex);
+ } catch (Throwable t) {
+ fail("Expected a CompilationFailedException but got instead " + t);
+ }
+ }
+
+ @Test
+ public void testCompileToInvalidFileR8() {
+ ensureInvalidFileIsInvalid();
+ ProgramConsumer programConsumer =
+ classFileConsumer
+ ? new ClassFileConsumer.ArchiveConsumer(INVALID_FILE)
+ : new ArchiveConsumer(INVALID_FILE);
+ try {
+ testForR8(classFileConsumer ? Backend.CF : Backend.DEX)
+ .addProgramClasses(Main.class)
+ .addKeepMainRule(Main.class)
+ .setProgramConsumer(programConsumer)
+ .compile();
+ fail("Expected a CompilationFailedException but the code succeeded");
+ } catch (CompilationFailedException ex) {
+ assertInvalidFileNotFound(ex);
+ } catch (Throwable t) {
+ fail("Expected a CompilationFailedException but got instead " + t);
+ }
+ }
+
+ private void assertInvalidFileNotFound(CompilationFailedException ex) {
+ assertTrue(ex.getCause().getMessage().contains("File not found"));
+ assertTrue(ex.getCause().getMessage().contains(INVALID_FILE.toString()));
+ }
+
+ private void ensureInvalidFileIsInvalid() {
+ try {
+ Files.newOutputStream(
+ INVALID_FILE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ fail("Expected an IOException but the code succeeded");
+ } catch (IOException ignored) {
+ } catch (Throwable t) {
+ fail("Expected an IOException but got instead " + t);
+ }
+ }
+
+ static class Main {
+ public static void main(String[] args) {
+ System.out.println("Hello world!");
+ }
+ }
+}