Merge "Prevent class merging from hiding IncompatibleClassChangeErrors"
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index c737055..a8fd776 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.shaking;
import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.AppInfo.ResolutionResult;
import com.android.tools.r8.graph.DefaultUseRegistry;
import com.android.tools.r8.graph.DexAnnotationSet;
import com.android.tools.r8.graph.DexApplication;
@@ -345,6 +346,9 @@
}
continue;
}
+ if (methodResolutionMayChange(clazz, targetClass)) {
+ continue;
+ }
// Field resolution first considers the direct interfaces of [targetClass] before it proceeds
// to the super class.
if (fieldResolutionMayChange(clazz, targetClass)) {
@@ -395,6 +399,49 @@
return renamedMembersLense.build(graphLense);
}
+ private boolean methodResolutionMayChange(DexClass source, DexClass target) {
+ // When merging an interface into a class, all instructions on the form "invoke-interface
+ // [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
+ // transformation could hide IncompatibleClassChangeErrors.
+ if (source.isInterface() && !target.isInterface()) {
+ List<DexEncodedMethod> defaultMethods = new ArrayList<>();
+ for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
+ if (!virtualMethod.accessFlags.isAbstract()) {
+ defaultMethods.add(virtualMethod);
+ }
+ }
+
+ // For each of the default methods, the subclass [target] could inherit another default method
+ // with the same signature from another interface (i.e., there is a conflict). In such cases,
+ // instructions on the form "invoke-interface [source].foo()" will fail with an Incompatible-
+ // ClassChangeError.
+ //
+ // Example:
+ // interface I1 { default void m() {} }
+ // interface I2 { default void m() {} }
+ // class C implements I1, I2 {
+ // ... invoke-interface I1.m ... <- IncompatibleClassChangeError
+ // }
+ for (DexEncodedMethod method : defaultMethods) {
+ // Conservatively find all possible targets for this method.
+ Set<DexEncodedMethod> interfaceTargets = appInfo.lookupInterfaceTargets(method.method);
+
+ // If [method] is not even an interface-target, then we can safely merge it. Otherwise we
+ // need to check for a conflict.
+ if (interfaceTargets.remove(method)) {
+ for (DexEncodedMethod interfaceTarget : interfaceTargets) {
+ DexClass enclosingClass = appInfo.definitionFor(interfaceTarget.method.holder);
+ if (enclosingClass != null && enclosingClass.isInterface()) {
+ // Found another default method that is different from the one in [source], aborting.
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
if (source.type == target.superType) {
// If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
@@ -657,12 +704,13 @@
// Returns the method that shadows the given method, or null if method is not shadowed.
private DexEncodedMethod findMethodInTarget(DexEncodedMethod method) {
- DexEncodedMethod actual = appInfo.resolveMethod(target.type, method.method).asSingleTarget();
- if (actual == null) {
- // May happen in case of missing classes.
+ ResolutionResult resolutionResult = appInfo.resolveMethod(target.type, method.method);
+ if (!resolutionResult.hasSingleTarget()) {
+ // May happen in case of missing classes, or if multiple implementations were found.
abortMerge = true;
return null;
}
+ DexEncodedMethod actual = resolutionResult.asSingleTarget();
if (actual != method) {
return actual;
}
diff --git a/src/test/java/com/android/tools/r8/resolution/singletarget/one/AbstractTopClass.java b/src/test/java/com/android/tools/r8/resolution/singletarget/one/AbstractTopClass.java
index d9ff675..c745dfa 100644
--- a/src/test/java/com/android/tools/r8/resolution/singletarget/one/AbstractTopClass.java
+++ b/src/test/java/com/android/tools/r8/resolution/singletarget/one/AbstractTopClass.java
@@ -5,33 +5,36 @@
public abstract class AbstractTopClass implements InterfaceWithDefault {
+ // Avoid AbstractTopClass.class.getCanonicalName() as it may change during shrinking.
+ private static final String TAG = "AbstractTopClass";
+
public void singleTargetAtTop() {
- System.out.println(AbstractTopClass.class.getCanonicalName());
+ System.out.println(TAG);
}
public void singleShadowingOverride() {
- System.out.println(AbstractTopClass.class.getCanonicalName());
+ System.out.println(TAG);
}
public abstract void abstractTargetAtTop();
public void overridenInAbstractClassOnly() {
- System.out.println(AbstractTopClass.class.getCanonicalName());
+ System.out.println(TAG);
}
public void overriddenInTwoSubTypes() {
- System.out.println(AbstractTopClass.class.getCanonicalName());
+ System.out.println(TAG);
}
public void definedInTwoSubTypes() {
- System.out.println(AbstractTopClass.class.getCanonicalName());
+ System.out.println(TAG);
}
public static void staticMethod() {
- System.out.println(AbstractTopClass.class.getCanonicalName());
+ System.out.println(TAG);
}
public void overriddenByIrrelevantInterface() {
- System.out.println(AbstractTopClass.class.getCanonicalName());
+ System.out.println(TAG);
}
}
diff --git a/src/test/java/com/android/tools/r8/resolution/singletarget/one/InterfaceWithDefault.java b/src/test/java/com/android/tools/r8/resolution/singletarget/one/InterfaceWithDefault.java
index 1a0c434..f0faedd 100644
--- a/src/test/java/com/android/tools/r8/resolution/singletarget/one/InterfaceWithDefault.java
+++ b/src/test/java/com/android/tools/r8/resolution/singletarget/one/InterfaceWithDefault.java
@@ -5,15 +5,18 @@
public interface InterfaceWithDefault {
+ // Avoid InterfaceWithDefault.class.getCanonicalName() as it may change during shrinking.
+ String TAG = "InterfaceWithDefault";
+
default void defaultMethod() {
- System.out.println(InterfaceWithDefault.class.getCanonicalName());
+ System.out.println(TAG);
}
default void overriddenDefault() {
- System.out.println(InterfaceWithDefault.class.getCanonicalName());
+ System.out.println(TAG);
}
default void overriddenInOtherInterface() {
- System.out.println(InterfaceWithDefault.class.getCanonicalName());
+ System.out.println(TAG);
}
}
diff --git a/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassOne.java b/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassOne.java
index 22be6a4..ff585af 100644
--- a/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassOne.java
+++ b/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassOne.java
@@ -5,18 +5,21 @@
public class SubSubClassOne extends AbstractSubClass implements IrrelevantInterfaceWithDefault {
+ // Avoid SubSubClassOne.class.getCanonicalName() as it may change during shrinking.
+ private static final String TAG = "SubSubClassOne";
+
@Override
public void abstractTargetAtTop() {
- System.out.println(SubSubClassOne.class.getCanonicalName());
+ System.out.println(TAG);
}
@Override
public void overriddenInTwoSubTypes() {
- System.out.println(SubSubClassOne.class.getCanonicalName());
+ System.out.println(TAG);
}
@Override
public void definedInTwoSubTypes() {
- System.out.println(SubSubClassOne.class.getCanonicalName());
+ System.out.println(TAG);
}
}