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 b2e0f6d..22ad739 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -54,6 +54,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);
@@ -72,6 +73,7 @@
dumpInputFlags = DumpInputFlags.noDump();
mapIdProvider = null;
sourceFileProvider = null;
+ classConflictResolver = null;
}
BaseCompilerCommand(
@@ -90,7 +92,8 @@
int threadCount,
DumpInputFlags dumpInputFlags,
MapIdProvider mapIdProvider,
- SourceFileProvider sourceFileProvider) {
+ SourceFileProvider sourceFileProvider,
+ ClassConflictResolver classConflictResolver) {
super(app);
assert minApiLevel > 0;
assert mode != null;
@@ -109,6 +112,7 @@
this.dumpInputFlags = dumpInputFlags;
this.mapIdProvider = mapIdProvider;
this.sourceFileProvider = sourceFileProvider;
+ this.classConflictResolver = classConflictResolver;
}
/**
@@ -197,6 +201,10 @@
return threadCount;
}
+ ClassConflictResolver getClassConflictResolver() {
+ return classConflictResolver;
+ }
+
DumpInputFlags getDumpInputFlags() {
return dumpInputFlags;
}
@@ -237,6 +245,7 @@
private DumpInputFlags dumpInputFlags = DumpInputFlags.noDump();
private MapIdProvider mapIdProvider = null;
private SourceFileProvider sourceFileProvider = null;
+ private ClassConflictResolver classConflictResolver = null;
abstract CompilationMode defaultCompilationMode();
@@ -720,5 +729,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 33e984b..62d96d8 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -28,6 +28,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;
@@ -390,6 +391,7 @@
getDumpInputFlags(),
getMapIdProvider(),
enableMissingLibraryApiModeling,
+ getClassConflictResolver(),
factory);
}
}
@@ -476,6 +478,7 @@
DumpInputFlags dumpInputFlags,
MapIdProvider mapIdProvider,
boolean enableMissingLibraryApiModeling,
+ ClassConflictResolver classConflictResolver,
DexItemFactory factory) {
super(
inputApp,
@@ -493,7 +496,8 @@
threadCount,
dumpInputFlags,
mapIdProvider,
- null);
+ null,
+ classConflictResolver);
this.intermediate = intermediate;
this.globalSyntheticsConsumer = globalSyntheticsConsumer;
this.desugarGraphConsumer = desugarGraphConsumer;
@@ -610,6 +614,10 @@
horizontalClassMergerOptions.disable();
}
+ 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 ee941db..fe430fc 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -23,6 +23,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;
@@ -102,6 +103,7 @@
int threadCount,
DumpInputFlags dumpInputFlags,
MapIdProvider mapIdProvider,
+ ClassConflictResolver classConflictResolver,
DexItemFactory factory) {
super(
inputApp,
@@ -119,7 +121,8 @@
threadCount,
dumpInputFlags,
mapIdProvider,
- null);
+ null,
+ classConflictResolver);
this.d8Command = d8Command;
this.r8Command = r8Command;
this.desugaredLibrarySpecification = desugaredLibrarySpecification;
@@ -209,6 +212,10 @@
.build(),
getAssertionsConfiguration());
+ internal.programClassConflictResolver =
+ ProgramClassCollection.wrappedConflictResolver(
+ getClassConflictResolver(), internal.reporter);
+
if (!DETERMINISTIC_DEBUGGING) {
assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
internal.threadCount = getThreadCount();
@@ -421,6 +428,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 b834389..85a6a63 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -37,6 +37,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;
@@ -634,7 +635,8 @@
getDumpInputFlags(),
getMapIdProvider(),
getSourceFileProvider(),
- enableMissingLibraryApiModeling);
+ enableMissingLibraryApiModeling,
+ getClassConflictResolver());
if (inputDependencyGraphConsumer != null) {
inputDependencyGraphConsumer.finished();
@@ -802,7 +804,8 @@
DumpInputFlags dumpInputFlags,
MapIdProvider mapIdProvider,
SourceFileProvider sourceFileProvider,
- boolean enableMissingLibraryApiModeling) {
+ boolean enableMissingLibraryApiModeling,
+ ClassConflictResolver classConflictResolver) {
super(
inputApp,
mode,
@@ -819,7 +822,8 @@
threadCount,
dumpInputFlags,
mapIdProvider,
- sourceFileProvider);
+ sourceFileProvider,
+ classConflictResolver);
assert proguardConfiguration != null;
assert mainDexKeepRules != null;
this.mainDexKeepRules = mainDexKeepRules;
@@ -1014,6 +1018,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 a5bbfcb..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,13 +98,22 @@
ImmutableList.of(a.getOrigin(), b.getOrigin())));
}
- private static DexProgramClass mergeClasses(
- Reporter reporter, DexProgramClass a, DexProgramClass b) {
+ 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 905947d..a27cd51 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/CompilerApiTestCollection.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.compilerapi.assertionconfiguration.AssertionConfigurationTest;
+import com.android.tools.r8.compilerapi.classconflictresolver.ClassConflictResolverTest;
import com.android.tools.r8.compilerapi.desugardependencies.DesugarDependenciesTest;
import com.android.tools.r8.compilerapi.globalsynthetics.GlobalSyntheticsTest;
import com.android.tools.r8.compilerapi.inputdependencies.InputDependenciesTest;
@@ -42,7 +43,9 @@
private static final List<Class<? extends CompilerApiTest>> CLASSES_PENDING_BINARY_COMPATIBILITY =
ImmutableList.of(
- GlobalSyntheticsTest.ApiTest.class, EnableMissingLibraryApiModelingTest.ApiTest.class);
+ GlobalSyntheticsTest.ApiTest.class,
+ EnableMissingLibraryApiModelingTest.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 {