Rewrite super calls to previously merged classes in vertical class merger

Change-Id: Ieeae36c7e13a3a2fe013a7003334173c2bd13c8d
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 1134686..f43106b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -630,7 +630,7 @@
     printMethod(code, "Initial IR (SSA)");
 
     if (options.canHaveArtStringNewInitBug()) {
-      codeRewriter.ensureDirectStringNewToInit(code);
+      CodeRewriter.ensureDirectStringNewToInit(code);
     }
 
     if (options.debug) {
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 f0519a1..d06c73a 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -71,10 +71,18 @@
   private final DexApplication application;
   private final AppInfoWithLiveness appInfo;
   private final GraphLense graphLense;
-  private final Map<DexType, DexType> mergedClasses = new HashMap<>();
   private final Timing timing;
   private Collection<DexMethod> invokes;
 
+  // Map from source class to target class.
+  private final Map<DexType, DexType> mergedClasses = new HashMap<>();
+
+  // Map from target class to the super classes that have been merged into the target class.
+  private final Map<DexType, Set<DexType>> mergedClassesInverse = new HashMap<>();
+
+  // The resulting graph lense that should be used after class merging.
+  private final VerticalClassMergerGraphLense.Builder renamedMembersLense;
+
   public VerticalClassMerger(
       DexApplication application,
       AppInfoWithLiveness appInfo,
@@ -83,6 +91,7 @@
     this.application = application;
     this.appInfo = appInfo;
     this.graphLense = graphLense;
+    this.renamedMembersLense = VerticalClassMergerGraphLense.builder(appInfo);
     this.timing = timing;
   }
 
@@ -319,10 +328,6 @@
     // Types that are pinned (in addition to those where appInfo.isPinned returns true).
     Set<DexType> pinnedTypes = getPinnedTypes(classes);
 
-    // The resulting graph lense that should be used after class merging.
-    VerticalClassMergerGraphLense.Builder renamedMembersLense =
-        VerticalClassMergerGraphLense.builder(appInfo);
-
     Iterator<DexProgramClass> classIterator = classes.iterator();
 
     // Visit the program classes in a top-down order according to the class hierarchy.
@@ -533,7 +538,7 @@
             continue;
           }
         } else {
-          // The method is shadowed. If it is abstract, we can simply move it to the subclass.
+          // The method is not shadowed. If it is abstract, we can simply move it to the subclass.
           // Non-abstract methods are handled below (they cannot simply be moved to the subclass as
           // a virtual method, because they might be the target of an invoke-super instruction).
           if (virtualMethod.accessFlags.isAbstract()) {
@@ -638,6 +643,8 @@
       source.interfaces = DexTypeList.empty();
       // Step 4: Record merging.
       mergedClasses.put(source.type, target.type);
+      mergedClassesInverse.computeIfAbsent(target.type, key -> new HashSet<>()).add(source.type);
+      assert !abortMerge;
       return true;
     }
 
@@ -658,13 +665,38 @@
         DexMethod signatureInHolder =
             application.dexItemFactory.createMethod(holder.type, oldTarget.proto, oldTarget.name);
         // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
-        if (resolutionSucceeds(signatureInHolder)) {
+        boolean resolutionSucceeds =
+            holder.lookupVirtualMethod(signatureInHolder) != null
+                || appInfo.lookupSuperTarget(signatureInHolder, holder.type) != null;
+        if (resolutionSucceeds) {
           deferredRenamings.mapVirtualMethodToDirectInType(
               signatureInHolder, newTarget, target.type);
-          holder = holder.superType != null ? appInfo.definitionFor(holder.superType) : null;
         } else {
           break;
         }
+
+        // Consider that A gets merged into B and B's subclass C gets merged into D. Instructions
+        // on the form "invoke-super {B,C,D}.m()" in D are changed into "invoke-direct D.m$C()" by
+        // the code above. However, instructions on the form "invoke-super A.m()" should also be
+        // changed into "invoke-direct D.m$C()". This is achieved by also considering the classes
+        // that have been merged into [holder].
+        Set<DexType> mergedTypes = mergedClassesInverse.get(holder.type);
+        if (mergedTypes != null) {
+          for (DexType type : mergedTypes) {
+            DexMethod signatureInType =
+                application.dexItemFactory.createMethod(type, oldTarget.proto, oldTarget.name);
+            // Resolution would have succeeded if the method used to be in [type], or if one of
+            // its super classes declared the method.
+            boolean resolutionSucceededBeforeMerge =
+                renamedMembersLense.hasMappingForSignatureInContext(holder.type, signatureInType)
+                    || appInfo.lookupSuperTarget(signatureInHolder, holder.type) != null;
+            if (resolutionSucceededBeforeMerge) {
+              deferredRenamings.mapVirtualMethodToDirectInType(
+                  signatureInType, newTarget, target.type);
+            }
+          }
+        }
+        holder = holder.superType != null ? appInfo.definitionFor(holder.superType) : null;
       }
     }
 
@@ -688,13 +720,6 @@
       deferredRenamings.markMethodAsMerged(method);
     }
 
-    private boolean resolutionSucceeds(DexMethod targetMethod) {
-      DexClass enclosingClass = appInfo.definitionFor(targetMethod.holder);
-      return enclosingClass != null
-          && (enclosingClass.lookupVirtualMethod(targetMethod) != null
-              || appInfo.lookupSuperTarget(targetMethod, enclosingClass.type) != null);
-    }
-
     private DexEncodedMethod buildBridgeMethod(
         DexEncodedMethod signature, DexMethod invocationTarget) {
       DexType holder = target.type;
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java
index 99fb63a..4cfe8db 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java
@@ -162,6 +162,15 @@
           previousLense);
     }
 
+    public boolean hasMappingForSignatureInContext(DexType context, DexMethod signature) {
+      Map<DexMethod, DexMethod> virtualToDirectMethodMap =
+          contextualVirtualToDirectMethodMaps.get(context);
+      if (virtualToDirectMethodMap != null) {
+        return virtualToDirectMethodMap.containsKey(signature);
+      }
+      return false;
+    }
+
     public void markMethodAsMerged(DexMethod method) {
       mergedMethodsBuilder.add(method);
     }
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index f141d36..40299cc 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.classmerging;
 
+import static com.android.tools.r8.smali.SmaliBuilder.buildCode;
 import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -178,14 +179,12 @@
             "classmerging.SubClassThatReferencesSuperMethod",
             "classmerging.SuperClassWithReferencedMethod");
     smaliBuilder.addInitializer(
-        ImmutableList.of(),
         0,
         "invoke-direct {p0}, Lclassmerging/SuperClassWithReferencedMethod;-><init>()V",
         "return-void");
     smaliBuilder.addInstanceMethod(
         "java.lang.String",
         "referencedMethod",
-        ImmutableList.of(),
         2,
         "sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
         "const-string v1, \"In referencedMethod on SubClassThatReferencesSuperMethod\"",
@@ -209,6 +208,140 @@
         getProguardConfig(EXAMPLE_KEEP, "-keep class *"));
   }
 
+  // The following test checks that our rewriting of invoke-super instructions in a class F works
+  // if a super type of F has previously been merged into its sub class.
+  //
+  //   class A {}                                               <- A is kept to preserve the error
+  //   class B extends A {                                         in F.invokeMethodOnA().
+  //     public void m() {}
+  //   }
+  //   class C extends B {                                      <- C will be merged into D.
+  //     @Override
+  //     public void m() { "invoke-super B.m()" }
+  //   }
+  //   class D extends C {
+  //     @Override
+  //     public void m() { "invoke-super C.m()" }
+  //   }
+  //   class E extends D {                                      <- E will be merged into F.
+  //     @Override
+  //     public void m() { "invoke-super D.m()" }
+  //   }
+  //   class F extends E {
+  //     @Override
+  //     public void m() { throw new Exception() }              <- Should be dead code.
+  //
+  //     public void invokeMethodOnA() { "invoke-super A.m()" } <- Should yield NoSuchMethodError.
+  //     public void invokeMethodOnB() { "invoke-super B.m()" }
+  //     public void invokeMethodOnC() { "invoke-super C.m()" }
+  //     public void invokeMethodOnD() { "invoke-super D.m()" }
+  //     public void invokeMethodOnE() { "invoke-super E.m()" }
+  //     public void invokeMethodOnF() { "invoke-super F.m()" }
+  //   }
+  @Test
+  public void testSuperCallToMergedClassIsRewritten() throws Exception {
+    String main = "classmerging.SuperCallToMergedClassIsRewrittenTest";
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.SuperCallToMergedClassIsRewrittenTest",
+            "classmerging.A",
+            "classmerging.B",
+            "classmerging.D",
+            "classmerging.F");
+
+    SmaliBuilder smaliBuilder = new SmaliBuilder();
+
+    smaliBuilder.addClass(main);
+    smaliBuilder.addMainMethod(
+        2,
+        // Instantiate B so that it is not merged into C.
+        "new-instance v1, Lclassmerging/B;",
+        "invoke-direct {v1}, Lclassmerging/B;-><init>()V",
+        "invoke-virtual {v1}, Lclassmerging/B;->m()V",
+        // Instantiate D so that it is not merged into E.
+        "new-instance v1, Lclassmerging/D;",
+        "invoke-direct {v1}, Lclassmerging/D;-><init>()V",
+        "invoke-virtual {v1}, Lclassmerging/D;->m()V",
+        // Start the actual testing.
+        "new-instance v1, Lclassmerging/F;",
+        "invoke-direct {v1}, Lclassmerging/F;-><init>()V",
+        "invoke-virtual {v1}, Lclassmerging/F;->invokeMethodOnB()V",
+        "invoke-virtual {v1}, Lclassmerging/F;->invokeMethodOnC()V",
+        "invoke-virtual {v1}, Lclassmerging/F;->invokeMethodOnD()V",
+        "invoke-virtual {v1}, Lclassmerging/F;->invokeMethodOnE()V",
+        "invoke-virtual {v1}, Lclassmerging/F;->invokeMethodOnF()V",
+        // The method invokeMethodOnA() should yield a NoSuchMethodError.
+        ":try_start",
+        "invoke-virtual {v1}, Lclassmerging/F;->invokeMethodOnA()V",
+        ":try_end",
+        "return-void",
+        ".catch Ljava/lang/NoSuchMethodError; {:try_start .. :try_end} :catch",
+        ":catch",
+        smaliCodeForPrinting("NoSuchMethodError", 0, 1),
+        "return-void");
+
+    // Class A deliberately has no method m. We need to make sure that the "invoke-super A.m()"
+    // instruction in class F is not rewritten into something that does not throw.
+    smaliBuilder.addClass("classmerging.A");
+
+    // Class B declares a virtual method m() that prints "In B.m()".
+    smaliBuilder.addClass("classmerging.B", "classmerging.A");
+    smaliBuilder.addInstanceMethod(
+        "void", "m", 2, smaliCodeForPrinting("In B.m()", 0, 1), "return-void");
+
+    // Class C, D, and E declare a virtual method m() that prints "In C.m()", "In D.m()", and
+    // "In E.m()", respectively.
+    String[][] pairs =
+        new String[][] {new String[] {"C", "B"}, new String[] {"D", "C"}, new String[] {"E", "D"}};
+    for (String[] pair : pairs) {
+      String name = pair[0], superName = pair[1];
+      smaliBuilder.addClass("classmerging." + name, "classmerging." + superName);
+      smaliBuilder.addInstanceMethod(
+          "void",
+          "m",
+          2,
+          smaliCodeForPrinting("In " + name + ".m()", 0, 1),
+          buildCode("invoke-super {p0}, Lclassmerging/" + superName + ";->m()V", "return-void"));
+    }
+
+    // Class F declares a virtual method m that throws an exception (it is expected to be dead).
+    smaliBuilder.addClass("classmerging.F", "classmerging.E");
+    smaliBuilder.addInstanceMethod(
+        "void",
+        "m",
+        1,
+        "new-instance v1, Ljava/lang/Exception;",
+        "invoke-direct {v1}, Ljava/lang/Exception;-><init>()V",
+        "throw v1");
+
+    // Add methods to F with an "invoke-super X.m()" instruction for X in {A, B, C, D, E, F}.
+    for (String type : ImmutableList.of("A", "B", "C", "D", "E", "F")) {
+      String code =
+          buildCode("invoke-super {p0}, Lclassmerging/" + type + ";->m()V", "return-void");
+      smaliBuilder.addInstanceMethod("void", "invokeMethodOn" + type, 0, code);
+    }
+
+    // Build app.
+    AndroidApp.Builder builder = AndroidApp.builder();
+    builder.addDexProgramData(smaliBuilder.compile(), Origin.unknown());
+
+    // Run test.
+    runTestOnInput(
+        main,
+        builder.build(),
+        preservedClassNames::contains,
+        String.format("-keep class %s { public static void main(...); }", main));
+  }
+
+  private static String smaliCodeForPrinting(String message, int reg0, int reg1) {
+    return buildCode(
+        String.format("sget-object v%d, Ljava/lang/System;->out:Ljava/io/PrintStream;", reg0),
+        String.format("const-string v1, \"%s\"", message),
+        String.format(
+            "invoke-virtual {v%d, v%d}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
+            reg0, reg1));
+  }
+
   @Test
   public void testConflictingInterfaceSignatures() throws Exception {
     String main = "classmerging.ConflictingInterfaceSignaturesTest";
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliBuilder.java b/src/test/java/com/android/tools/r8/smali/SmaliBuilder.java
index 9a6e778..e818801 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliBuilder.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliBuilder.java
@@ -285,6 +285,16 @@
     return addMethod("public abstract", returnType, name, parameters, -1, null);
   }
 
+  public MethodSignature addInitializer(int locals, String... instructions) {
+    return addMethod(
+        "public constructor",
+        "void",
+        "<init>",
+        ImmutableList.of(),
+        locals,
+        buildCode(instructions));
+  }
+
   public MethodSignature addInitializer(
       List<String> parameters, int locals, String... instructions) {
     return addMethod(
@@ -292,6 +302,11 @@
   }
 
   public MethodSignature addInstanceMethod(
+      String returnType, String name, int locals, String... instructions) {
+    return addInstanceMethod(returnType, name, ImmutableList.of(), locals, buildCode(instructions));
+  }
+
+  public MethodSignature addInstanceMethod(
       String returnType, String name, List<String> parameters, int locals, String... instructions) {
     return addInstanceMethod(returnType, name, parameters, locals, buildCode(instructions));
   }
@@ -315,7 +330,7 @@
     getSource(currentClassName).add(builder.toString());
   }
 
-  private static String buildCode(String... instructions) {
+  public static String buildCode(String... instructions) {
     StringBuilder builder = new StringBuilder();
     for (String instruction : instructions) {
       builder.append(instruction);