Add API for resolving duplicate definition conflicts.
Bug: b/241063980
Change-Id: I40294a8be3a8af70fa902d599c68a3df49004a29
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 4af2547..d9c85d3 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -52,6 +52,7 @@
private final DumpInputFlags dumpInputFlags;
private final MapIdProvider mapIdProvider;
private final SourceFileProvider sourceFileProvider;
+ private final ClassConflictResolver classConflictResolver;
BaseCompilerCommand(boolean printHelp, boolean printVersion) {
super(printHelp, printVersion);
@@ -70,6 +71,7 @@
dumpInputFlags = DumpInputFlags.noDump();
mapIdProvider = null;
sourceFileProvider = null;
+ classConflictResolver = null;
}
BaseCompilerCommand(
@@ -88,7 +90,8 @@
int threadCount,
DumpInputFlags dumpInputFlags,
MapIdProvider mapIdProvider,
- SourceFileProvider sourceFileProvider) {
+ SourceFileProvider sourceFileProvider,
+ ClassConflictResolver classConflictResolver) {
super(app);
assert minApiLevel > 0;
assert mode != null;
@@ -107,6 +110,7 @@
this.dumpInputFlags = dumpInputFlags;
this.mapIdProvider = mapIdProvider;
this.sourceFileProvider = sourceFileProvider;
+ this.classConflictResolver = classConflictResolver;
}
/**
@@ -195,6 +199,10 @@
return threadCount;
}
+ ClassConflictResolver getClassConflictResolver() {
+ return classConflictResolver;
+ }
+
DumpInputFlags getDumpInputFlags() {
return dumpInputFlags;
}
@@ -235,6 +243,7 @@
private DumpInputFlags dumpInputFlags = DumpInputFlags.noDump();
private MapIdProvider mapIdProvider = null;
private SourceFileProvider sourceFileProvider = null;
+ private ClassConflictResolver classConflictResolver = null;
abstract CompilationMode defaultCompilationMode();
@@ -716,5 +725,21 @@
List<Consumer<Inspector>> getOutputInspections() {
return outputInspections;
}
+
+ /**
+ * Set a conflict resolver to determine which class definition to use in case of duplicates.
+ *
+ * <p>If no resolver is set, the compiler will fail compilation in case of duplicates.
+ *
+ * @param resolver Resolver for choosing between duplicate classes.
+ */
+ public B setClassConflictResolver(ClassConflictResolver resolver) {
+ this.classConflictResolver = resolver;
+ return self();
+ }
+
+ ClassConflictResolver getClassConflictResolver() {
+ return classConflictResolver;
+ }
}
}
diff --git a/src/main/java/com/android/tools/r8/ClassConflictResolver.java b/src/main/java/com/android/tools/r8/ClassConflictResolver.java
new file mode 100644
index 0000000..8136f17
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ClassConflictResolver.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2022, 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;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import java.util.Collection;
+
+@Keep
+public interface ClassConflictResolver {
+
+ /**
+ * Callback called in case the compiler is provided with duplicate class definitions.
+ *
+ * <p>The callback may be called multiple times for the same type with different origins, or it
+ * may be called once with all origins. Assuming the client has provided unique origins for the
+ * various inputs, the number of origins in any call will be at least two.
+ *
+ * <p>Note that all the duplicates are in the program's compilation unit. In other words, none of
+ * them are classpath or library definitions.
+ *
+ * @param reference The type reference of the duplicated class.
+ * @param origins The multiple origins of the class.
+ * @param handler Diagnostics handler for reporting.
+ * @return Returns the origin to use or null to fail compilation.
+ */
+ Origin resolveDuplicateClass(
+ ClassReference reference, Collection<Origin> origins, DiagnosticsHandler handler);
+}
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 8d0f357..e6a6014 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -27,6 +27,7 @@
import com.android.tools.r8.utils.InternalOptions.DesugarState;
import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.ProgramClassCollection;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
@@ -319,6 +320,7 @@
getThreadCount(),
getDumpInputFlags(),
getMapIdProvider(),
+ getClassConflictResolver(),
factory);
}
}
@@ -401,6 +403,7 @@
int threadCount,
DumpInputFlags dumpInputFlags,
MapIdProvider mapIdProvider,
+ ClassConflictResolver classConflictResolver,
DexItemFactory factory) {
super(
inputApp,
@@ -418,7 +421,8 @@
threadCount,
dumpInputFlags,
mapIdProvider,
- null);
+ null,
+ classConflictResolver);
this.intermediate = intermediate;
this.desugarGraphConsumer = desugarGraphConsumer;
this.desugaredLibraryKeepRuleConsumer = desugaredLibraryKeepRuleConsumer;
@@ -517,6 +521,10 @@
internal.threadCount = getThreadCount();
}
+ internal.programClassConflictResolver =
+ ProgramClassCollection.wrappedConflictResolver(
+ getClassConflictResolver(), internal.reporter);
+
internal.setDumpInputFlags(getDumpInputFlags(), skipDump);
internal.dumpOptions = dumpOptions();
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index 51ea781..8ac11df 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -22,6 +22,7 @@
import com.android.tools.r8.utils.InternalOptions.DesugarState;
import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ProgramClassCollection;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
@@ -101,6 +102,7 @@
int threadCount,
DumpInputFlags dumpInputFlags,
MapIdProvider mapIdProvider,
+ ClassConflictResolver classConflictResolver,
DexItemFactory factory) {
super(
inputApp,
@@ -118,7 +120,8 @@
threadCount,
dumpInputFlags,
mapIdProvider,
- null);
+ null,
+ classConflictResolver);
this.d8Command = d8Command;
this.r8Command = r8Command;
this.libraryConfiguration = libraryConfiguration;
@@ -205,6 +208,10 @@
new AssertionConfigurationWithDefault(
AssertionTransformation.DISABLE, getAssertionsConfiguration());
+ internal.programClassConflictResolver =
+ ProgramClassCollection.wrappedConflictResolver(
+ getClassConflictResolver(), internal.reporter);
+
if (!DETERMINISTIC_DEBUGGING) {
assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
internal.threadCount = getThreadCount();
@@ -412,6 +419,7 @@
getThreadCount(),
getDumpInputFlags(),
getMapIdProvider(),
+ getClassConflictResolver(),
factory);
}
}
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 4b53393..eaf4ce1 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -36,6 +36,7 @@
import com.android.tools.r8.utils.InternalOptions.DesugarState;
import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.ProgramClassCollection;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
@@ -594,7 +595,8 @@
getThreadCount(),
getDumpInputFlags(),
getMapIdProvider(),
- getSourceFileProvider());
+ getSourceFileProvider(),
+ getClassConflictResolver());
return command;
}
@@ -756,7 +758,8 @@
int threadCount,
DumpInputFlags dumpInputFlags,
MapIdProvider mapIdProvider,
- SourceFileProvider sourceFileProvider) {
+ SourceFileProvider sourceFileProvider,
+ ClassConflictResolver classConflictResolver) {
super(
inputApp,
mode,
@@ -773,7 +776,8 @@
threadCount,
dumpInputFlags,
mapIdProvider,
- sourceFileProvider);
+ sourceFileProvider,
+ classConflictResolver);
assert proguardConfiguration != null;
assert mainDexKeepRules != null;
this.mainDexKeepRules = mainDexKeepRules;
@@ -953,6 +957,10 @@
SourceFileRewriter.computeSourceFileProvider(
getSourceFileProvider(), proguardConfiguration, internal);
+ internal.programClassConflictResolver =
+ ProgramClassCollection.wrappedConflictResolver(
+ getClassConflictResolver(), internal.reporter);
+
if (!DETERMINISTIC_DEBUGGING) {
assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
internal.threadCount = getThreadCount();
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
index 8c3e592..99b79e9 100644
--- a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -3,13 +3,16 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
+import com.android.tools.r8.ClassConflictResolver;
import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
import com.android.tools.r8.errors.DuplicateTypesDiagnostic;
import com.android.tools.r8.graph.ClassKind;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.references.Reference;
import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
@@ -60,10 +63,28 @@
public static ProgramClassConflictResolver defaultConflictResolver(Reporter reporter) {
// The default conflict resolver only merges synthetic classes generated by D8 correctly.
// All other conflicts are reported as a fatal error.
- return (DexProgramClass a, DexProgramClass b) -> {
- assert a.type == b.type;
- if (a.accessFlags.isSynthetic() && b.accessFlags.isSynthetic()) {
- return mergeClasses(reporter, a, b);
+ return wrappedConflictResolver(null, reporter);
+ }
+
+ public static ProgramClassConflictResolver wrappedConflictResolver(
+ ClassConflictResolver clientResolver, Reporter reporter) {
+ return (a, b) -> {
+ DexProgramClass clazz = mergeClasses(a, b);
+ if (clazz != null) {
+ return clazz;
+ }
+ if (clientResolver != null) {
+ List<Origin> origins = new ArrayList<>();
+ origins.add(a.getOrigin());
+ origins.add(b.getOrigin());
+ Origin origin =
+ clientResolver.resolveDuplicateClass(a.getClassReference(), origins, reporter);
+ if (origin == a.getOrigin()) {
+ return a;
+ }
+ if (origin == b.getOrigin()) {
+ return b;
+ }
}
throw reportDuplicateTypes(reporter, a, b);
};
@@ -77,14 +98,22 @@
ImmutableList.of(a.getOrigin(), b.getOrigin())));
}
- private static DexProgramClass mergeClasses(
- Reporter reporter, DexProgramClass a, DexProgramClass b) {
- if (a.type.isLegacySynthesizedTypeAllowedDuplication()
- || a.type.isSynthesizedTypeAllowedDuplication()) {
+ private static DexProgramClass mergeClasses(DexProgramClass a, DexProgramClass b) {
+ assert a.type == b.type;
+ boolean syntheticA = a.accessFlags.isSynthetic();
+ boolean syntheticB = b.accessFlags.isSynthetic();
+ if (syntheticA && syntheticB) {
+ return mergeIfLegacySynthetics(a, b);
+ }
+ return null;
+ }
+
+ private static DexProgramClass mergeIfLegacySynthetics(DexProgramClass a, DexProgramClass b) {
+ if (a.type.isLegacySynthesizedTypeAllowedDuplication()) {
assert assertEqualClasses(a, b);
return a;
}
- throw reportDuplicateTypes(reporter, a, b);
+ return null;
}
private static boolean assertEqualClasses(DexProgramClass a, DexProgramClass b) {
diff --git a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
index c2fab32..03c0611 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -8,6 +8,7 @@
import static com.android.tools.r8.ToolHelper.isTestingR8Lib;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.compilerapi.classconflictresolver.ClassConflictResolverTest;
import com.android.tools.r8.compilerapi.mapid.CustomMapIdTest;
import com.android.tools.r8.compilerapi.mockdata.MockClass;
import com.android.tools.r8.compilerapi.sourcefile.CustomSourceFileTest;
@@ -29,7 +30,10 @@
ImmutableList.of(ApiTestingSetUpTest.ApiTest.class);
private static final List<Class<? extends CompilerApiTest>> CLASSES_PENDING_BINARY_COMPATIBILITY =
- ImmutableList.of(CustomMapIdTest.ApiTest.class, CustomSourceFileTest.ApiTest.class);
+ ImmutableList.of(
+ CustomMapIdTest.ApiTest.class,
+ CustomSourceFileTest.ApiTest.class,
+ ClassConflictResolverTest.ApiTest.class);
private final TemporaryFolder temp;
diff --git a/src/test/java/com/android/tools/r8/compilerapi/classconflictresolver/ClassConflictResolverTest.java b/src/test/java/com/android/tools/r8/compilerapi/classconflictresolver/ClassConflictResolverTest.java
new file mode 100644
index 0000000..d328392
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/compilerapi/classconflictresolver/ClassConflictResolverTest.java
@@ -0,0 +1,116 @@
+// Copyright (c) 2022, 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.compilerapi.classconflictresolver;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ClassConflictResolver;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.compilerapi.CompilerApiTest;
+import com.android.tools.r8.compilerapi.CompilerApiTestRunner;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.BooleanBox;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.Paths;
+import org.junit.Test;
+
+public class ClassConflictResolverTest extends CompilerApiTestRunner {
+
+ public ClassConflictResolverTest(TestParameters parameters) {
+ super(parameters);
+ }
+
+ @Override
+ public Class<? extends CompilerApiTest> binaryTestClass() {
+ return ApiTest.class;
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ Origin originA = new PathOrigin(Paths.get("SourceA"));
+ Origin originB = new PathOrigin(Paths.get("SourceB"));
+ BooleanBox called = new BooleanBox(false);
+ new ApiTest(ApiTest.PARAMETERS)
+ .runD8(
+ originA,
+ originB,
+ (reference, origins, handler) -> {
+ called.set(true);
+ assertEquals(ImmutableSet.of(originA, originB), ImmutableSet.copyOf(origins));
+ return originA;
+ });
+ assertTrue(called.get());
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ Origin originA = new PathOrigin(Paths.get("SourceA"));
+ Origin originB = new PathOrigin(Paths.get("SourceB"));
+ BooleanBox called = new BooleanBox(false);
+ new ApiTest(ApiTest.PARAMETERS)
+ .runR8(
+ originA,
+ originB,
+ (reference, origins, handler) -> {
+ called.set(true);
+ assertEquals(ImmutableSet.of(originA, originB), ImmutableSet.copyOf(origins));
+ return originA;
+ });
+ assertTrue(called.get());
+ }
+
+ public static class ApiTest extends CompilerApiTest {
+
+ public ApiTest(Object parameters) {
+ super(parameters);
+ }
+
+ public void runD8(Origin originA, Origin originB, ClassConflictResolver resolver)
+ throws Exception {
+ D8.run(
+ D8Command.builder()
+ .addClassProgramData(getBytesForClass(getMockClass()), originA)
+ .addClassProgramData(getBytesForClass(getMockClass()), originB)
+ .addLibraryFiles(getJava8RuntimeJar())
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+ .setClassConflictResolver(resolver)
+ .build());
+ }
+
+ public void runR8(Origin originA, Origin originB, ClassConflictResolver resolver)
+ throws Exception {
+ R8.run(
+ R8Command.builder()
+ .addClassProgramData(getBytesForClass(getMockClass()), originA)
+ .addClassProgramData(getBytesForClass(getMockClass()), originB)
+ .addLibraryFiles(getJava8RuntimeJar())
+ .setDisableTreeShaking(true)
+ .setDisableMinification(true)
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+ .setClassConflictResolver(resolver)
+ .build());
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ Origin originA = new PathOrigin(Paths.get("SourceA"));
+ Origin originB = new PathOrigin(Paths.get("SourceB"));
+ runD8(originA, originB, (reference, origins, handler) -> originA);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ Origin originA = new PathOrigin(Paths.get("SourceA"));
+ Origin originB = new PathOrigin(Paths.get("SourceB"));
+ runR8(originA, originB, (reference, origins, handler) -> originA);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/invalid/DuplicateDefinitionsTest.java b/src/test/java/com/android/tools/r8/invalid/DuplicateDefinitionsTest.java
index 90771b4..a52c762 100644
--- a/src/test/java/com/android/tools/r8/invalid/DuplicateDefinitionsTest.java
+++ b/src/test/java/com/android/tools/r8/invalid/DuplicateDefinitionsTest.java
@@ -4,25 +4,39 @@
package com.android.tools.r8.invalid;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.jasmin.JasminBuilder;
import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
import com.android.tools.r8.jasmin.JasminTestBase;
-import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.google.common.collect.ImmutableList;
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.nio.charset.Charset;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+@RunWith(Parameterized.class)
public class DuplicateDefinitionsTest extends JasminTestBase {
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDefaultDexRuntime().withApiLevel(AndroidApiLevel.B).build();
+ }
+
+ private final TestParameters parameters;
+
+ public DuplicateDefinitionsTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
@Test
public void testDuplicateMethods() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
@@ -32,38 +46,37 @@
classBuilder.addVirtualMethod("method", "V", ".limit locals 1", ".limit stack 0", "return");
classBuilder.addVirtualMethod("method", "V", ".limit locals 1", ".limit stack 0", "return");
- // Run D8 and intercept warnings.
- PrintStream stderr = System.err;
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- System.setErr(new PrintStream(baos));
+ testForD8(parameters.getBackend())
+ .addProgramClassFileData(jasminBuilder.buildClasses())
+ .setMinApi(parameters.getApiLevel())
+ .compileWithExpectedDiagnostics(
+ diagnostics ->
+ diagnostics
+ .assertOnlyWarnings()
+ .assertWarningsMatch(
+ diagnosticMessage(
+ containsString(
+ "Ignoring an implementation of the method `void"
+ + " C.main(java.lang.String[])` because it has multiple"
+ + " definitions")),
+ diagnosticMessage(
+ containsString(
+ "Ignoring an implementation of the method `void C.method()` because"
+ + " it has multiple definitions"))))
+ .inspect(
+ inspector -> {
+ ClassSubject clazz = inspector.clazz("C");
+ assertThat(clazz, isPresent());
- AndroidApp app = compileWithD8(jasminBuilder.build());
+ // There are two direct methods, but only because one is <init>.
+ assertEquals(
+ 2, clazz.getDexProgramClass().getMethodCollection().numberOfDirectMethods());
+ assertThat(clazz.method("void", "<init>", ImmutableList.of()), isPresent());
- String output = new String(baos.toByteArray(), Charset.defaultCharset());
- System.setOut(stderr);
-
- // Check that warnings were emitted.
- assertThat(
- output,
- containsString(
- "Ignoring an implementation of the method `void C.main(java.lang.String[])` because "
- + "it has multiple definitions"));
- assertThat(
- output,
- containsString(
- "Ignoring an implementation of the method `void C.method()` because "
- + "it has multiple definitions"));
-
- CodeInspector inspector = new CodeInspector(app);
- ClassSubject clazz = inspector.clazz("C");
- assertThat(clazz, isPresent());
-
- // There are two direct methods, but only because one is <init>.
- assertEquals(2, clazz.getDexProgramClass().getMethodCollection().numberOfDirectMethods());
- assertThat(clazz.method("void", "<init>", ImmutableList.of()), isPresent());
-
- // There is only one virtual method.
- assertEquals(1, clazz.getDexProgramClass().getMethodCollection().numberOfVirtualMethods());
+ // There is only one virtual method.
+ assertEquals(
+ 1, clazz.getDexProgramClass().getMethodCollection().numberOfVirtualMethods());
+ });
}
@Test
@@ -75,26 +88,26 @@
classBuilder.addStaticField("staticFld", "LC;", null);
classBuilder.addStaticField("staticFld", "LC;", null);
- // Run D8 and intercept warnings.
- PrintStream stderr = System.err;
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- System.setErr(new PrintStream(baos));
+ testForD8(parameters.getBackend())
+ .addProgramClassFileData(jasminBuilder.buildClasses())
+ .setMinApi(parameters.getApiLevel())
+ .compileWithExpectedDiagnostics(
+ diagnostics ->
+ diagnostics
+ .assertOnlyWarnings()
+ .assertWarningsMatch(
+ diagnosticMessage(
+ containsString("Field `C C.fld` has multiple definitions")),
+ diagnosticMessage(
+ containsString("Field `C C.staticFld` has multiple definitions"))))
+ .inspect(
+ inspector -> {
+ ClassSubject clazz = inspector.clazz("C");
+ assertThat(clazz, isPresent());
- AndroidApp app = compileWithD8(jasminBuilder.build());
-
- String output = new String(baos.toByteArray(), Charset.defaultCharset());
- System.setOut(stderr);
-
- // Check that warnings were emitted.
- assertThat(output, containsString("Field `C C.fld` has multiple definitions"));
- assertThat(output, containsString("Field `C C.staticFld` has multiple definitions"));
-
- CodeInspector inspector = new CodeInspector(app);
- ClassSubject clazz = inspector.clazz("C");
- assertThat(clazz, isPresent());
-
- // Redundant fields have been removed.
- assertEquals(1, clazz.getDexProgramClass().instanceFields().size());
- assertEquals(1, clazz.getDexProgramClass().staticFields().size());
+ // Redundant fields have been removed.
+ assertEquals(1, clazz.getDexProgramClass().instanceFields().size());
+ assertEquals(1, clazz.getDexProgramClass().staticFields().size());
+ });
}
}
diff --git a/src/test/java/com/android/tools/r8/invalid/DuplicateProgramTypesTest.java b/src/test/java/com/android/tools/r8/invalid/DuplicateProgramTypesTest.java
index e8c8d39..1efe38f 100644
--- a/src/test/java/com/android/tools/r8/invalid/DuplicateProgramTypesTest.java
+++ b/src/test/java/com/android/tools/r8/invalid/DuplicateProgramTypesTest.java
@@ -10,10 +10,12 @@
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
+import com.android.tools.r8.BaseCompilerCommand;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.ToolHelper;
@@ -21,6 +23,8 @@
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.position.Position;
import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.SetUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -32,7 +36,7 @@
@Parameterized.Parameters(name = "{0}")
public static TestParametersCollection data() {
- return getTestParameters().withNoneRuntime().build();
+ return getTestParameters().withDefaultDexRuntime().withApiLevel(AndroidApiLevel.B).build();
}
public DuplicateProgramTypesTest(TestParameters parameters) {
@@ -55,39 +59,113 @@
}
};
+ private void addDuplicateDefinitions(BaseCompilerCommand.Builder<?, ?> builder) throws Exception {
+ byte[] bytes = ToolHelper.getClassAsBytes(TestClass.class);
+ builder.addClassProgramData(bytes, originA);
+ builder.addClassProgramData(bytes, originB);
+ }
+
+ private void addResolvingHandler(BaseCompilerCommand.Builder<?, ?> builder) throws Exception {
+ builder.setClassConflictResolver(
+ (reference, origins, handler) -> {
+ assertEquals(
+ SetUtils.newIdentityHashSet(originA, originB), SetUtils.newIdentityHashSet(origins));
+ return originA;
+ });
+ }
+
+ private void addNonResolvingHandler(BaseCompilerCommand.Builder<?, ?> builder) throws Exception {
+ builder.setClassConflictResolver(
+ (reference, origins, handler) -> {
+ assertEquals(
+ SetUtils.newIdentityHashSet(originA, originB), SetUtils.newIdentityHashSet(origins));
+ return null;
+ });
+ }
+
+ private void checkErrorDiagnostic(TestDiagnosticMessages diagnostics) {
+ diagnostics.assertOnlyErrors();
+ diagnostics.assertErrorsCount(1);
+ DuplicateTypesDiagnostic diagnostic = (DuplicateTypesDiagnostic) diagnostics.getErrors().get(0);
+ assertEquals(Position.UNKNOWN, diagnostic.getPosition());
+ assertThat(diagnostic.getType(), equalTo(Reference.classFromClass(TestClass.class)));
+ assertThat(diagnostic.getOrigin(), anyOf(equalTo(originA), equalTo(originB)));
+ assertThat(diagnostic.getOrigins(), hasItems(originA, originB));
+ assertThat(
+ diagnostic.getDiagnosticMessage(),
+ allOf(
+ containsString("defined multiple"),
+ containsString("SourceA"),
+ containsString("SourceB")));
+ }
+
@Test
- public void test() throws Exception {
- try {
- byte[] bytes = ToolHelper.getClassAsBytes(TestClass.class);
- testForD8()
- .setMinApi(parameters.getRuntime())
- .apply(
- b -> {
- b.getBuilder().addClassProgramData(bytes, originA);
- b.getBuilder().addClassProgramData(bytes, originB);
- })
- .compileWithExpectedDiagnostics(
- diagnostics -> {
- diagnostics.assertOnlyErrors();
- diagnostics.assertErrorsCount(1);
- DuplicateTypesDiagnostic diagnostic =
- (DuplicateTypesDiagnostic) diagnostics.getErrors().get(0);
- assertEquals(Position.UNKNOWN, diagnostic.getPosition());
- assertThat(
- diagnostic.getType(), equalTo(Reference.classFromClass(TestClass.class)));
- assertThat(diagnostic.getOrigin(), anyOf(equalTo(originA), equalTo(originB)));
- assertThat(diagnostic.getOrigins(), hasItems(originA, originB));
- assertThat(
- diagnostic.getDiagnosticMessage(),
- allOf(
- containsString("defined multiple"),
- containsString("SourceA"),
- containsString("SourceB")));
- });
- } catch (CompilationFailedException e) {
- return; // Success.
- }
- fail("Expected test to fail with CompilationFailedException");
+ public void testDefaultError() throws Exception {
+ assertThrows(
+ CompilationFailedException.class,
+ () ->
+ testForD8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .apply(b -> addDuplicateDefinitions(b.getBuilder()))
+ .compileWithExpectedDiagnostics(this::checkErrorDiagnostic));
+ }
+
+ @Test
+ public void testResolvedConflictD8() throws Exception {
+ testForD8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .apply(
+ b -> {
+ addDuplicateDefinitions(b.getBuilder());
+ addResolvingHandler(b.getBuilder());
+ })
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello, world");
+ }
+
+ @Test
+ public void testResolvedConflictR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .addKeepMainRule(TestClass.class)
+ .apply(
+ b -> {
+ addDuplicateDefinitions(b.getBuilder());
+ addResolvingHandler(b.getBuilder());
+ })
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutputLines("Hello, world");
+ }
+
+ @Test
+ public void testNonResolvedConflictD8() throws Exception {
+ assertThrows(
+ CompilationFailedException.class,
+ () ->
+ testForD8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .apply(
+ b -> {
+ addDuplicateDefinitions(b.getBuilder());
+ addNonResolvingHandler(b.getBuilder());
+ })
+ .compileWithExpectedDiagnostics(this::checkErrorDiagnostic));
+ }
+
+ @Test
+ public void testNonResolvedConflictR8() throws Exception {
+ assertThrows(
+ CompilationFailedException.class,
+ () ->
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .addKeepMainRule(TestClass.class)
+ .apply(
+ b -> {
+ addDuplicateDefinitions(b.getBuilder());
+ addNonResolvingHandler(b.getBuilder());
+ })
+ .compileWithExpectedDiagnostics(this::checkErrorDiagnostic));
}
static class TestClass {