Update catch handlers after class merging

If an exception class A is merged into another exception class B, then all exception tables should be updated, and class A should be removed entirely.

Bug: 73958515
Change-Id: I596681b1a80837243cd6d78fb8df21e2e433a284
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 831edbe..5f0fb83 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -264,9 +264,10 @@
 
   public void setCode(
       IRCode ir,
+      GraphLense graphLense,
       RegisterAllocator registerAllocator,
       InternalOptions options) {
-    final DexBuilder builder = new DexBuilder(ir, registerAllocator, options);
+    final DexBuilder builder = new DexBuilder(ir, graphLense, registerAllocator, options);
     code = builder.build(method.getArity());
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 8201562..ee7df48 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -43,6 +43,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CatchHandlers;
@@ -81,6 +82,10 @@
   // The IR representation of the code to build.
   private final IRCode ir;
 
+  // Graph lense for building the exception handlers. Needed since program classes that inherit
+  // from Throwable may get merged into their subtypes during class merging.
+  private final GraphLense graphLense;
+
   // The register allocator providing register assignments for the code to build.
   private final RegisterAllocator registerAllocator;
 
@@ -116,11 +121,14 @@
 
   public DexBuilder(
       IRCode ir,
+      GraphLense graphLense,
       RegisterAllocator registerAllocator,
       InternalOptions options) {
     assert ir != null;
+    assert graphLense != null;
     assert registerAllocator != null;
     this.ir = ir;
+    this.graphLense = graphLense;
     this.registerAllocator = registerAllocator;
     this.options = options;
   }
@@ -706,7 +714,12 @@
           assert i == handlerGroup.getGuards().size() - 1;
           catchAllOffset = targetOffset;
         } else {
-          pairs.add(new TypeAddrPair(type, targetOffset));
+          // The type may have changed due to class merging.
+          // TODO(christofferqa): This assumes that the graph lense is context insensitive for the
+          // given type (which is always the case). Consider removing the context-argument from
+          // GraphLense.lookupType, since we do not currently have a use case for it.
+          DexType actualType = graphLense.lookupType(type, null);
+          pairs.add(new TypeAddrPair(actualType, targetOffset));
         }
       }
       TypeAddrPair[] pairsArray = pairs.toArray(new TypeAddrPair[pairs.size()]);
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 81dc436..df4e69c 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
@@ -532,7 +532,7 @@
     assert code.isConsistentSSA();
     code.traceBlocks();
     RegisterAllocator registerAllocator = performRegisterAllocation(code, method);
-    method.setCode(code, registerAllocator, options);
+    method.setCode(code, graphLense, registerAllocator, options);
     if (Log.ENABLED) {
       Log.debug(getClass(), "Resulting dex code for %s:\n%s",
           method.toSourceString(), logCode(options, method));
@@ -784,7 +784,7 @@
   private void finalizeToDex(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     // Perform register allocation.
     RegisterAllocator registerAllocator = performRegisterAllocation(code, method);
-    method.setCode(code, registerAllocator, options);
+    method.setCode(code, graphLense, registerAllocator, options);
     updateHighestSortingStrings(method);
     if (Log.ENABLED) {
       Log.debug(getClass(), "Resulting dex code for %s:\n%s",
diff --git a/src/test/examples/classmerging/ExceptionTest.java b/src/test/examples/classmerging/ExceptionTest.java
new file mode 100644
index 0000000..b365b2a
--- /dev/null
+++ b/src/test/examples/classmerging/ExceptionTest.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2018, 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 classmerging;
+
+public class ExceptionTest {
+  public static void main(String[] args) {
+    // The following will lead to a catch handler for ExceptionA, which is merged into ExceptionB.
+    try {
+      throw new ExceptionB("Ouch!");
+    } catch (ExceptionA exception) {
+      System.out.println("Caught exception: " + exception.getMessage());
+    }
+  }
+
+  // Will be merged into ExceptionB when class merging is enabled.
+  public static class ExceptionA extends Exception {
+    public ExceptionA(String message) {
+      super(message);
+    }
+  }
+
+  public static class ExceptionB extends ExceptionA {
+    public ExceptionB(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 1cc5b87..f9e6d52 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -7,6 +7,9 @@
 -keep public class classmerging.Test {
   public static void main(...);
 }
+-keep public class classmerging.ExceptionTest {
+  public static void main(...);
+}
 
 # TODO(herhut): Consider supporting merging of inner-class attributes.
 # -keepattributes *
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index fbf2d02..5200a09 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -151,6 +151,11 @@
     return builder.build();
   }
 
+  /** Build an AndroidApp from the specified program files. */
+  protected AndroidApp readProgramFiles(Path... programFiles) throws IOException {
+    return AndroidApp.builder().addProgramFiles(programFiles).build();
+  }
+
   /**
    * Create a temporary JAR file containing the specified test classes.
    */
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 d5d2c39..69f925c 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -3,29 +3,34 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.classmerging;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
 
-public class ClassMergingTest {
+public class ClassMergingTest extends TestBase {
 
+  private static final Path CF_DIR =
+      Paths.get(ToolHelper.BUILD_DIR).resolve("classes/examples/classmerging");
   private static final Path EXAMPLE_JAR = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR)
       .resolve("classmerging.jar");
   private static final Path EXAMPLE_KEEP = Paths.get(ToolHelper.EXAMPLES_DIR)
@@ -33,17 +38,14 @@
   private static final Path DONT_OPTIMIZE = Paths.get(ToolHelper.EXAMPLES_DIR)
       .resolve("classmerging").resolve("keep-rules-dontoptimize.txt");
 
-  @Rule
-  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
-
   private void configure(InternalOptions options) {
     options.enableClassMerging = true;
     options.enableClassInlining = false;
+    options.enableMinification = false;
   }
 
   private void runR8(Path proguardConfig, Consumer<InternalOptions> optionsConsumer)
-      throws IOException, ProguardRuleParserException, ExecutionException,
-      CompilationFailedException {
+      throws IOException, ExecutionException, CompilationFailedException {
     ToolHelper.runR8(
         R8Command.builder()
             .setOutput(Paths.get(temp.getRoot().getCanonicalPath()), OutputMode.DexIndexed)
@@ -73,13 +75,12 @@
       assertFalse(inspector.clazz(candidate).isPresent());
     }
     assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent());
-    assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent());
     assertTrue(inspector.clazz("classmerging.Outer$SubClass").isPresent());
     assertTrue(inspector.clazz("classmerging.SubClass").isPresent());
   }
 
   @Test
-  public void testClassesShouldNotMerged() throws Exception {
+  public void testClassesHaveNotBeenMerged() throws Exception {
     runR8(DONT_OPTIMIZE, null);
     for (String candidate : CAN_BE_MERGED) {
       assertTrue(inspector.clazz(candidate).isPresent());
@@ -100,4 +101,39 @@
     assertTrue(inspector.clazz("classmerging.SubClassThatReferencesSuperMethod").isPresent());
   }
 
+  // If an exception class A is merged into another exception class B, then all exception tables
+  // should be updated, and class A should be removed entirely.
+  @Test
+  public void testExceptionTables() throws Exception {
+    String main = "classmerging.ExceptionTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ExceptionTest.class"),
+          CF_DIR.resolve("ExceptionTest$ExceptionA.class"),
+          CF_DIR.resolve("ExceptionTest$ExceptionB.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of("classmerging.ExceptionTest", "classmerging.ExceptionTest$ExceptionB");
+    runTest(main, programFiles, preservedClassNames);
+  }
+
+  private void runTest(String main, Path[] programFiles, Set<String> preservedClassNames)
+      throws Exception {
+    AndroidApp input = readProgramFiles(programFiles);
+    AndroidApp output = compileWithR8(input, EXAMPLE_KEEP, this::configure);
+    DexInspector inspector = new DexInspector(output);
+    // Check that all classes in [preservedClassNames] are in fact preserved.
+    for (String className : preservedClassNames) {
+      assertTrue(
+          "Class " + className + " should be present", inspector.clazz(className).isPresent());
+    }
+    // Check that all other classes have been removed.
+    for (FoundClassSubject classSubject : inspector.allClasses()) {
+      String className = classSubject.getDexClass().toSourceString();
+      assertTrue(
+          "Class " + className + " should be absent", preservedClassNames.contains(className));
+    }
+    // Check that the R8-generated code produces the same result as D8-generated code.
+    assertEquals(runOnArt(compileWithD8(input), main), runOnArt(output, main));
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index 4214a67..d6e3612 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -38,6 +38,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.code.CatchHandlers;
@@ -652,7 +653,7 @@
                 code);
         IRCode ir = code.buildIR(method, null, options, Origin.unknown());
         RegisterAllocator allocator = new LinearScanRegisterAllocator(ir, options);
-        method.setCode(ir, allocator, options);
+        method.setCode(ir, GraphLense.getIdentityLense(), allocator, options);
         directMethods[i] = method;
       }
       builder.addProgramClass(