Merge "Keep more annotations/attributes for inner/outer class relationship"
diff --git a/.gitignore b/.gitignore
index d9cb978..f15c3c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,8 @@
 third_party/benchmarks/santa-tracker.tar.gz
 third_party/chrome.tar.gz
 third_party/chrome
+third_party/dart-sdk
+third_party/dart-sdk.tar.gz
 third_party/desugar/desugar_*/
 third_party/desugar/desugar_*.tar.gz
 third_party/gmail/*
diff --git a/build.gradle b/build.gradle
index f5a3337..cfaacdb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1310,17 +1310,23 @@
         if (project.property('tool') == 'r8') {
             exclude "com/android/tools/r8/art/*/d8/**"
             exclude "com/android/tools/r8/jctf/d8/**"
-        } else {
-            assert(project.property('tool') == 'd8')
+            exclude "com/android/tools/r8/jctf/r8cf/**"
+        } else if (project.property('tool') == 'd8') {
             exclude "com/android/tools/r8/art/*/r8/**"
             exclude "com/android/tools/r8/jctf/r8/**"
+            exclude "com/android/tools/r8/jctf/r8cf/**"
+        } else {
+            assert(project.property('tool') == 'r8cf')
+            exclude "com/android/tools/r8/art/*/d8/**"
+            exclude "com/android/tools/r8/art/*/r8/**"
+            exclude "com/android/tools/r8/jctf/d8/**"
+            exclude "com/android/tools/r8/jctf/r8/**"
         }
     }
     if (!project.hasProperty('all_tests')) {
         exclude "com/android/tools/r8/art/dx/**"
         exclude "com/android/tools/r8/art/jack/**"
     }
-    // TODO(tamaskenez) enable jctf on all_tests when consolidated
     if (!project.hasProperty('jctf') && !project.hasProperty('only_jctf')) {
         exclude "com/android/tools/r8/jctf/**"
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardMemberRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardMemberRule.java
index d1bfc9a..5424cc2 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardMemberRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardMemberRule.java
@@ -191,7 +191,7 @@
           break;
         }
         // Type check.
-        if (!getType().matches(originalSignature.type)) {
+        if (!getType().matches(originalSignature.type, appView)) {
           break;
         }
         // Annotations check
@@ -228,9 +228,7 @@
         return RootSetBuilder.containsAnnotation(annotation, method.annotations);
       case METHOD:
         // Check return type.
-        // TODO(b/110141157): The name of the return type may have changed as a result of vertical
-        // class merging. We should use the original type name.
-        if (!type.matches(originalSignature.proto.returnType)) {
+        if (!type.matches(originalSignature.proto.returnType, appView)) {
           break;
         }
         // Fall through for access flags, name and arguments.
@@ -261,9 +259,7 @@
           break;
         }
         for (int i = 0; i < parameters.length; i++) {
-          // TODO(b/110141157): The names of the parameter types may have changed as a result of
-          // vertical class merging. We should use the original type names.
-          if (!arguments.get(i).matches(parameters[i])) {
+          if (!arguments.get(i).matches(parameters[i], appView)) {
             return false;
           }
         }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java b/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
index 34ee554..747dea9 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardTypeMatcher.java
@@ -5,6 +5,8 @@
 
 import static com.android.tools.r8.utils.DescriptorUtils.javaTypeToDescriptor;
 
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcards;
@@ -31,8 +33,21 @@
     TYPE
   }
 
+  // Evaluates this matcher on the given type.
   public abstract boolean matches(DexType type);
 
+  // Evaluates this matcher on the given type, and on all types that have been merged into the given
+  // type, if any.
+  public final boolean matches(DexType type, AppView<? extends AppInfo> appView) {
+    if (matches(type)) {
+      return true;
+    }
+    if (appView.verticallyMergedClasses() != null) {
+      return appView.verticallyMergedClasses().getSourcesFor(type).stream().anyMatch(this::matches);
+    }
+    return false;
+  }
+
   protected Iterable<ProguardWildcard> getWildcards() {
     return Collections::emptyIterator;
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index d5d5422..e117462 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -107,7 +107,10 @@
         // TODO(herhut): Warn about broken supertype chain?
         return false;
       }
-      if (name.matches(clazz.type) && containsAnnotation(annotation, clazz.annotations)) {
+      // TODO(b/110141157): Should the vertical class merger move annotations from the source to
+      // the target class? If so, it is sufficient only to apply the annotation-matcher to the
+      // annotations of `class`.
+      if (name.matches(clazz.type, appView) && containsAnnotation(annotation, clazz.annotations)) {
         return true;
       }
       type = clazz.superType;
@@ -386,16 +389,17 @@
       if (!satisfyAnnotation(rule, sourceClass)) {
         return;
       }
-      // TODO(b/110141157): Handle the situation where the class in the extends/implements clause
-      // has been merged.
-      if (rule.hasInheritanceClassName()
-          && !satisfyInheritanceRule(sourceClass, this::definitionForWithLiveTypes, rule)) {
-        // Try another live type since the current one doesn't satisfy the inheritance rule.
-        return;
-      }
       if (!rule.getClassNames().matches(sourceClass.type)) {
         return;
       }
+      if (rule.hasInheritanceClassName()) {
+        // Note that, in presence of vertical class merging, we check if the resulting class
+        // (i.e., the target class) satisfies the implements/extends-matcher.
+        if (!satisfyInheritanceRule(targetClass, this::definitionForWithLiveTypes, rule)) {
+          // Try another live type since the current one doesn't satisfy the inheritance rule.
+          return;
+        }
+      }
       Collection<ProguardMemberRule> memberKeepRules = rule.getMemberRules();
       if (memberKeepRules.isEmpty()) {
         materializeIfRule(rule);
@@ -456,6 +460,8 @@
     }
 
     private DexClass definitionForWithLiveTypes(DexType type) {
+      assert appView.verticallyMergedClasses() == null
+          || !appView.verticallyMergedClasses().hasBeenMergedIntoSubtype(type);
       return liveTypes.contains(type) ? appView.appInfo().definitionFor(type) : null;
     }
   }
@@ -592,8 +598,7 @@
     ProguardTypeMatcher inheritanceClassName = rule.getInheritanceClassName();
     ProguardTypeMatcher inheritanceAnnotation = rule.getInheritanceAnnotation();
     boolean extendsExpected =
-        anySuperTypeMatches(
-            clazz.superType, definitionFor, inheritanceClassName, inheritanceAnnotation);
+        satisfyExtendsRule(clazz, definitionFor, inheritanceClassName, inheritanceAnnotation);
     boolean implementsExpected = false;
     if (!extendsExpected) {
       implementsExpected =
@@ -618,6 +623,27 @@
     return false;
   }
 
+  private boolean satisfyExtendsRule(
+      DexClass clazz,
+      Function<DexType, DexClass> definitionFor,
+      ProguardTypeMatcher inheritanceClassName,
+      ProguardTypeMatcher inheritanceAnnotation) {
+    if (anySuperTypeMatches(
+        clazz.superType, definitionFor, inheritanceClassName, inheritanceAnnotation)) {
+      return true;
+    }
+
+    // It is possible that this class used to inherit from another class X, but no longer does it,
+    // because X has been merged into `clazz`.
+    if (appView.verticallyMergedClasses() != null) {
+      // TODO(b/110141157): Figure out what to do with annotations. Should the annotations of
+      // the DexClass corresponding to `sourceType` satisfy the `annotation`-matcher?
+      return appView.verticallyMergedClasses().getSourcesFor(clazz.type).stream()
+          .anyMatch(inheritanceClassName::matches);
+    }
+    return false;
+  }
+
   private boolean allRulesSatisfied(Collection<ProguardMemberRule> memberKeepRules,
       DexClass clazz) {
     for (ProguardMemberRule rule : memberKeepRules) {
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index a4f55c3..ed12177 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -60,6 +60,58 @@
   private final List<StringResource> mainDexListResources;
   private final List<String> mainDexClasses;
 
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    try {
+      if (!programResourceProviders.isEmpty()) {
+        builder.append("  Program resources:").append(System.lineSeparator());
+        printProgramResourceProviders(builder, programResourceProviders);
+      }
+      if (!classpathResourceProviders.isEmpty()) {
+        builder.append("  Classpath resources:").append(System.lineSeparator());
+        printClassFileProviders(builder, classpathResourceProviders);
+      }
+      if (!libraryResourceProviders.isEmpty()) {
+        builder.append("  Library resources:").append(System.lineSeparator());
+        printClassFileProviders(builder, libraryResourceProviders);
+      }
+    } catch (ResourceException e) {
+      e.printStackTrace();
+    }
+    return builder.toString();
+  }
+
+  private static void printProgramResourceProviders(
+      StringBuilder builder, Collection<ProgramResourceProvider> providers)
+      throws ResourceException {
+    for (ProgramResourceProvider provider : providers) {
+      for (ProgramResource resource : provider.getProgramResources()) {
+        printProgramResource(builder, resource);
+      }
+    }
+  }
+
+  private static void printClassFileProviders(
+      StringBuilder builder, Collection<ClassFileResourceProvider> providers) {
+    for (ClassFileResourceProvider provider : providers) {
+      for (String descriptor : provider.getClassDescriptors()) {
+        ProgramResource resource = provider.getProgramResource(descriptor);
+        printProgramResource(builder, resource);
+      }
+    }
+  }
+
+  private static void printProgramResource(StringBuilder builder, ProgramResource resource) {
+    builder.append("    ").append(resource.getOrigin());
+    Set<String> descriptors = resource.getClassDescriptors();
+    if (descriptors != null && !descriptors.isEmpty()) {
+      builder.append(" contains ");
+      StringUtils.append(builder, descriptors);
+    }
+    builder.append(System.lineSeparator());
+  }
+
   // See factory methods and AndroidApp.Builder below.
   private AndroidApp(
       ImmutableList<ProgramResourceProvider> programResourceProviders,
diff --git a/src/test/java/com/android/tools/r8/D8TestBuilder.java b/src/test/java/com/android/tools/r8/D8TestBuilder.java
new file mode 100644
index 0000000..368e8e4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/D8TestBuilder.java
@@ -0,0 +1,51 @@
+// 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 com.android.tools.r8;
+
+import com.android.tools.r8.D8Command.Builder;
+import com.android.tools.r8.TestBase.Backend;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class D8TestBuilder extends TestCompilerBuilder<D8Command, Builder, D8TestBuilder> {
+
+  private final D8Command.Builder builder;
+
+  private D8TestBuilder(TestState state, D8Command.Builder builder) {
+    super(state, builder, Backend.DEX);
+    this.builder = builder;
+  }
+
+  public static D8TestBuilder create(TestState state) {
+    return new D8TestBuilder(state, D8Command.builder());
+  }
+
+  @Override
+  D8TestBuilder self() {
+    return this;
+  }
+
+  @Override
+  void internalCompile(Builder builder) throws CompilationFailedException {
+    D8.run(builder.build());
+  }
+
+  public D8TestBuilder addClasspathClasses(Class<?>... classes) {
+    return addClasspathClasses(Arrays.asList(classes));
+  }
+
+  public D8TestBuilder addClasspathClasses(Collection<Class<?>> classes) {
+    return addClasspathFiles(getFilesForClasses(classes));
+  }
+
+  public D8TestBuilder addClasspathFiles(Path... files) {
+    return addClasspathFiles(Arrays.asList(files));
+  }
+
+  public D8TestBuilder addClasspathFiles(Collection<Path> files) {
+    builder.addClasspathFiles(files);
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
new file mode 100644
index 0000000..484adee
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -0,0 +1,158 @@
+// 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 com.android.tools.r8;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ListUtils;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class JvmTestBuilder extends TestBuilder<JvmTestBuilder> {
+
+  private static class ClassFileResource implements ProgramResource {
+
+    private final Path file;
+    private final String descriptor;
+    private final Origin origin;
+
+    ClassFileResource(Class<?> clazz) {
+      this(
+          ToolHelper.getClassFileForTestClass(clazz),
+          DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()));
+    }
+
+    ClassFileResource(Path file, String descriptor) {
+      this.file = file;
+      this.descriptor = descriptor;
+      origin = new PathOrigin(file);
+    }
+
+    @Override
+    public Kind getKind() {
+      return Kind.CF;
+    }
+
+    @Override
+    public InputStream getByteStream() throws ResourceException {
+      try {
+        return Files.newInputStream(file);
+      } catch (IOException e) {
+        throw new ResourceException(getOrigin(), e);
+      }
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return Collections.singleton(descriptor);
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return origin;
+    }
+  }
+
+  private static class ClassFileResourceProvider implements ProgramResourceProvider {
+
+    private final List<ProgramResource> resources;
+
+    public ClassFileResourceProvider(List<ProgramResource> resources) {
+      this.resources = resources;
+    }
+
+    @Override
+    public Collection<ProgramResource> getProgramResources() throws ResourceException {
+      return resources;
+    }
+
+    @Override
+    public DataResourceProvider getDataResourceProvider() {
+      return null;
+    }
+  }
+
+  // Ordered list of classpath entries.
+  private List<Path> classpath = new ArrayList<>();
+
+  private AndroidApp.Builder builder = AndroidApp.builder();
+
+  private JvmTestBuilder(TestState state) {
+    super(state);
+  }
+
+  public static JvmTestBuilder create(TestState state) {
+    return new JvmTestBuilder(state);
+  }
+
+  @Override
+  JvmTestBuilder self() {
+    return this;
+  }
+
+  @Override
+  public TestRunResult run(String mainClass) throws IOException {
+    ProcessResult result = ToolHelper.runJava(classpath, mainClass);
+    return new TestRunResult(builder.build(), result);
+  }
+
+  @Override
+  public JvmTestBuilder addLibraryFiles(Collection<Path> files) {
+    throw new Unimplemented("No support for changing the Java runtime library.");
+  }
+
+  @Override
+  public JvmTestBuilder addProgramClasses(Collection<Class<?>> classes) {
+    // Adding a collection of classes will build a jar of exactly those classes so that no other
+    // classes are made available via a too broad classpath directory.
+    List<ProgramResource> resources = ListUtils.map(classes, ClassFileResource::new);
+    AndroidApp build = AndroidApp.builder()
+        .addProgramResourceProvider(new ClassFileResourceProvider(resources)).build();
+    Path out;
+    try {
+      out = getState().getNewTempFolder().resolve("out.zip");
+      build.writeToZip(out, OutputMode.ClassFile);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+    classpath.add(out);
+    builder.addProgramFiles(out);
+    return self();
+  }
+
+  @Override
+  public JvmTestBuilder addProgramFiles(Collection<Path> files) {
+    throw new Unimplemented(
+        "No support for adding paths directly (we need to compute the descriptor)");
+  }
+
+  public JvmTestBuilder addClasspath(Path... paths) {
+    return addClasspath(Arrays.asList(paths));
+  }
+
+  public JvmTestBuilder addClasspath(List<Path> paths) {
+    for (Path path : paths) {
+      assert Files.isDirectory(path) || FileUtils.isArchive(path);
+      classpath.add(path);
+    }
+    return self();
+  }
+
+  public JvmTestBuilder addTestClasspath() {
+    return addClasspath(ToolHelper.getClassPathForTests());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
new file mode 100644
index 0000000..50e659c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -0,0 +1,84 @@
+// 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 com.android.tools.r8;
+
+import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class R8TestBuilder extends TestCompilerBuilder<R8Command, Builder, R8TestBuilder> {
+
+  private final R8Command.Builder builder;
+
+  private R8TestBuilder(TestState state, Builder builder, Backend backend) {
+    super(state, builder, backend);
+    this.builder = builder;
+  }
+
+  public static R8TestBuilder create(TestState state, Backend backend) {
+    return new R8TestBuilder(state, R8Command.builder(), backend);
+  }
+
+  private boolean enableInliningAnnotations = false;
+
+  @Override
+  R8TestBuilder self() {
+    return this;
+  }
+
+  @Override
+  public void internalCompile(Builder builder) throws CompilationFailedException {
+    if (enableInliningAnnotations) {
+      ToolHelper.allowTestProguardOptions(builder);
+    }
+    R8.run(builder.build());
+  }
+
+  public R8TestBuilder addKeepRules(String... rules) {
+    return addKeepRules(Arrays.asList(rules));
+  }
+
+  public R8TestBuilder addKeepRules(Collection<String> rules) {
+    builder.addProguardConfiguration(new ArrayList<>(rules), Origin.unknown());
+    return self();
+  }
+
+  public R8TestBuilder addKeepAllClassesRule() {
+    addKeepRules("-keep class ** { *; }");
+    return self();
+  }
+
+  public R8TestBuilder addKeepClassRules(Class<?>... classes) {
+    for (Class<?> clazz : classes) {
+      addKeepRules("-keep class " + clazz.getTypeName());
+    }
+    return self();
+  }
+
+  public R8TestBuilder addKeepPackageRules(Package pkg) {
+    return addKeepRules("-keep class " + pkg.getName() + ".*");
+  }
+
+  public R8TestBuilder addKeepMainRule(Class<?> mainClass) {
+    return addKeepRules(
+        StringUtils.joinLines(
+            "-keep class " + mainClass.getTypeName() + " {",
+            "  public static void main(java.lang.String[]);",
+            "}"));
+  }
+
+  public R8TestBuilder enableInliningAnnotations() {
+    if (!enableInliningAnnotations) {
+      enableInliningAnnotations = true;
+      addKeepRules(
+          "-forceinline class * { @com.android.tools.r8.ForceInline *; }",
+          "-neverinline class * { @com.android.tools.r8.NeverInline *; }");
+    }
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 94cc54b..efec188 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -59,6 +59,19 @@
 import org.objectweb.asm.ClassVisitor;
 
 public class TestBase {
+
+  public R8TestBuilder testForR8(Backend backend) {
+    return R8TestBuilder.create(new TestState(temp), backend);
+  }
+
+  public D8TestBuilder testForD8() {
+    return D8TestBuilder.create(new TestState(temp));
+  }
+
+  public JvmTestBuilder testForJvm() {
+    return JvmTestBuilder.create(new TestState(temp));
+  }
+
   public enum Backend {
     CF,
     DEX
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
new file mode 100644
index 0000000..6d5a158
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -0,0 +1,89 @@
+// 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 com.android.tools.r8;
+
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+
+import com.android.tools.r8.utils.ListUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public abstract class TestBuilder<T extends TestBuilder<T>> {
+
+  private final TestState state;
+
+  public TestBuilder(TestState state) {
+    this.state = state;
+  }
+
+  public TestState getState() {
+    return state;
+  }
+
+  abstract T self();
+
+  public abstract TestRunResult run(String mainClass)
+      throws IOException, CompilationFailedException;
+
+  public TestRunResult run(Class mainClass) throws IOException, CompilationFailedException {
+    return run(mainClass.getTypeName());
+  }
+
+  public abstract T addProgramFiles(Collection<Path> files);
+
+  public T addProgramClasses(Class<?>... classes) {
+    return addProgramClasses(Arrays.asList(classes));
+  }
+
+  public T addProgramClasses(Collection<Class<?>> classes) {
+    return addProgramFiles(getFilesForClasses(classes));
+  }
+
+  public T addProgramFiles(Path... files) {
+    return addProgramFiles(Arrays.asList(files));
+  }
+
+  public T addProgramClassesAndInnerClasses(Class<?>... classes) throws IOException {
+    return addProgramClassesAndInnerClasses(Arrays.asList(classes));
+  }
+
+  public T addProgramClassesAndInnerClasses(Collection<Class<?>> classes) throws IOException {
+    return addProgramFiles(getFilesForClassesAndInnerClasses(classes));
+  }
+
+  public abstract T addLibraryFiles(Collection<Path> files);
+
+  public T addLibraryClasses(Class<?>... classes) {
+    return addLibraryClasses(Arrays.asList(classes));
+  }
+
+  public T addLibraryClasses(Collection<Class<?>> classes) {
+    return addLibraryFiles(getFilesForClasses(classes));
+  }
+
+  public T addLibraryFiles(Path... files) {
+    return addLibraryFiles(Arrays.asList(files));
+  }
+
+  static Collection<Path> getFilesForClasses(Collection<Class<?>> classes) {
+    return ListUtils.map(classes, ToolHelper::getClassFileForTestClass);
+  }
+
+  static Collection<Path> getFilesForClassesAndInnerClasses(Collection<Class<?>> classes)
+      throws IOException {
+    Set<Path> paths = new HashSet<>();
+    for (Class clazz : classes) {
+      Path path = ToolHelper.getClassFileForTestClass(clazz);
+      String prefix = path.toString().replace(CLASS_EXTENSION, "$");
+      paths.addAll(
+          ToolHelper.getClassFilesForTestDirectory(
+              path.getParent(), p -> p.equals(path) || p.toString().startsWith(prefix)));
+    }
+    return paths;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
new file mode 100644
index 0000000..9996dde
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -0,0 +1,48 @@
+// 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 com.android.tools.r8;
+
+import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.AndroidApp;
+import java.io.IOException;
+import java.nio.file.Path;
+
+public class TestCompileResult {
+  private final TestState state;
+  private final Backend backend;
+  private final AndroidApp app;
+
+  public TestCompileResult(TestState state, Backend backend, AndroidApp app) {
+    this.state = state;
+    this.backend = backend;
+    this.app = app;
+  }
+
+  public TestRunResult run(String mainClass) throws IOException {
+    switch (backend) {
+      case DEX:
+        return runArt(mainClass);
+      case CF:
+        return runJava(mainClass);
+      default:
+        throw new Unreachable();
+    }
+  }
+
+  private TestRunResult runJava(String mainClass) throws IOException {
+    Path out = state.getNewTempFolder().resolve("out.zip");
+    app.writeToZip(out, OutputMode.ClassFile);
+    ProcessResult result = ToolHelper.runJava(out, mainClass);
+    return new TestRunResult(app, result);
+  }
+
+  private TestRunResult runArt(String mainClass) throws IOException {
+    Path out = state.getNewTempFolder().resolve("out.zip");
+    app.writeToZip(out, OutputMode.DexIndexed);
+    ProcessResult result = ToolHelper.runArtRaw(out.toString(), mainClass);
+    return new TestRunResult(app, result);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
new file mode 100644
index 0000000..c6af2c7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -0,0 +1,95 @@
+// 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 com.android.tools.r8;
+
+import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidAppConsumers;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+
+public abstract class TestCompilerBuilder<
+        C extends BaseCompilerCommand,
+        B extends BaseCompilerCommand.Builder<C, B>,
+        T extends TestCompilerBuilder<C, B, T>>
+    extends TestBuilder<T> {
+
+  private final B builder;
+  private final Backend backend;
+
+  // Default initialized setup. Can be overwritten if needed.
+  private Path defaultLibrary;
+  private ProgramConsumer programConsumer;
+  private AndroidApiLevel defaultMinApiLevel = ToolHelper.getMinApiLevelForDexVm();
+
+  TestCompilerBuilder(TestState state, B builder, Backend backend) {
+    super(state);
+    this.builder = builder;
+    this.backend = backend;
+    defaultLibrary = TestBase.runtimeJar(backend);
+    programConsumer = TestBase.emptyConsumer(backend);
+  }
+
+  abstract T self();
+
+  abstract void internalCompile(B builder) throws CompilationFailedException;
+
+  public TestCompileResult compile() throws CompilationFailedException {
+    AndroidAppConsumers sink = new AndroidAppConsumers();
+    builder.setProgramConsumer(sink.wrapProgramConsumer(programConsumer));
+    if (defaultLibrary != null) {
+      builder.addLibraryFiles(defaultLibrary);
+    }
+    if (backend == Backend.DEX && defaultMinApiLevel != null) {
+      builder.setMinApiLevel(defaultMinApiLevel.getLevel());
+    }
+    internalCompile(builder);
+    return new TestCompileResult(getState(), backend, sink.build());
+  }
+
+  @Override
+  public TestRunResult run(String mainClass) throws IOException, CompilationFailedException {
+    return compile().run(mainClass);
+  }
+
+  public T setMode(CompilationMode mode) {
+    builder.setMode(mode);
+    return self();
+  }
+
+  public T debug() {
+    return setMode(CompilationMode.DEBUG);
+  }
+
+  public T release() {
+    return setMode(CompilationMode.RELEASE);
+  }
+
+  public T setMinApi(AndroidApiLevel minApiLevel) {
+    // Should we ignore min-api calls when backend == CF?
+    this.defaultMinApiLevel = null;
+    builder.setMinApiLevel(minApiLevel.getLevel());
+    return self();
+  }
+
+  public T setProgramConsumer(ProgramConsumer programConsumer) {
+    assert programConsumer != null;
+    this.programConsumer = programConsumer;
+    return self();
+  }
+
+  @Override
+  public T addProgramFiles(Collection<Path> files) {
+    builder.addProgramFiles(files);
+    return self();
+  }
+
+  @Override
+  public T addLibraryFiles(Collection<Path> files) {
+    defaultLibrary = null;
+    builder.addLibraryFiles(files);
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
new file mode 100644
index 0000000..462ca1f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -0,0 +1,68 @@
+// 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 com.android.tools.r8;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+
+public class TestRunResult {
+  private final AndroidApp app;
+  private final ProcessResult result;
+
+  public TestRunResult(AndroidApp app, ProcessResult result) {
+    this.app = app;
+    this.result = result;
+  }
+
+  public TestRunResult assertSuccess() {
+    assertEquals(errorMessage("Expected run to succeed."), 0, result.exitCode);
+    return this;
+  }
+
+  public TestRunResult assertFailure() {
+    assertNotEquals(errorMessage("Expected run to fail."), 0, result.exitCode);
+    return this;
+  }
+
+  public void assertSuccessWithOutput(String expected) {
+    assertSuccess();
+    assertEquals(errorMessage("Run std output incorrect."), expected, result.stdout);
+  }
+
+  public CodeInspector inspector() throws IOException, ExecutionException {
+    // Inspection post run implies success. If inspection of an invalid program is needed it should
+    // be done on the compilation result or on the input.
+    assertSuccess();
+    assertNotNull(app);
+    return new CodeInspector(app);
+  }
+
+  private String errorMessage(String message) {
+    StringBuilder builder = new StringBuilder(message).append('\n');
+    printInfo(builder);
+    return builder.toString();
+  }
+
+  private void printInfo(StringBuilder builder) {
+    builder.append("APPLICATION: ");
+    printApplication(builder);
+    builder.append('\n');
+    printProcessResult(builder);
+  }
+
+  private void printApplication(StringBuilder builder) {
+    builder.append(app == null ? "<default>" : app.toString());
+  }
+
+  private void printProcessResult(StringBuilder builder) {
+    builder.append("COMMAND: ").append(result.command).append('\n').append(result);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestState.java b/src/test/java/com/android/tools/r8/TestState.java
new file mode 100644
index 0000000..d516c7c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestState.java
@@ -0,0 +1,21 @@
+// 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 com.android.tools.r8;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import org.junit.rules.TemporaryFolder;
+
+public class TestState {
+
+  private final TemporaryFolder temp;
+
+  public TestState(TemporaryFolder temp) {
+    this.temp = temp;
+  }
+
+  public Path getNewTempFolder() throws IOException {
+    return temp.newFolder().toPath();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 515d191..8ae0dad 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -1531,11 +1531,17 @@
     public final int exitCode;
     public final String stdout;
     public final String stderr;
+    public final String command;
 
-    ProcessResult(int exitCode, String stdout, String stderr) {
+    ProcessResult(int exitCode, String stdout, String stderr, String command) {
       this.exitCode = exitCode;
       this.stdout = stdout;
       this.stderr = stderr;
+      this.command = command;
+    }
+
+    ProcessResult(int exitCode, String stdout, String stderr) {
+      this(exitCode, stdout, stderr, null);
     }
 
     @Override
@@ -1574,7 +1580,8 @@
   }
 
   public static ProcessResult runProcess(ProcessBuilder builder) throws IOException {
-    System.out.println(String.join(" ", builder.command()));
+    String command = String.join(" ", builder.command());
+    System.out.println(command);
     Process p = builder.start();
     // Drain stdout and stderr so that the process does not block. Read stdout and stderr
     // in parallel to make sure that neither buffer can get filled up which will cause the
@@ -1592,7 +1599,8 @@
     } catch (InterruptedException e) {
       throw new RuntimeException("Execution interrupted", e);
     }
-    return new ProcessResult(p.exitValue(), stdoutReader.getResult(), stderrReader.getResult());
+    return new ProcessResult(
+        p.exitValue(), stdoutReader.getResult(), stderrReader.getResult(), command);
   }
 
   public static R8Command.Builder addProguardConfigurationConsumer(
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/IndirectSuperInterfaceTest.java b/src/test/java/com/android/tools/r8/memberrebinding/IndirectSuperInterfaceTest.java
index 9552a61..977b925 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/IndirectSuperInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/IndirectSuperInterfaceTest.java
@@ -3,17 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.memberrebinding;
 
-import static org.junit.Assert.assertEquals;
-
 import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.R8;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidAppConsumers;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
@@ -149,38 +141,23 @@
 
   @Test
   public void test() throws Exception {
-    String expected =
-        String.join(
-            System.lineSeparator(),
-            "A::method -> InterfaceA::method",
-            "B::method -> InterfaceB::method",
-            "C::method -> InterfaceC::method",
-            "D::method -> InterfaceD::method");
-    assertEquals(expected, runOnJava(TestClass.class));
+    String expected = StringUtils.joinLines(
+        "A::method -> InterfaceA::method",
+        "B::method -> InterfaceB::method",
+        "C::method -> InterfaceC::method",
+        "D::method -> InterfaceD::method");
 
-    AndroidAppConsumers sink = new AndroidAppConsumers();
-    Builder builder = R8Command.builder();
-    for (Class<?> clazz : CLASSES) {
-      builder.addClassProgramData(ToolHelper.getClassAsBytes(clazz), Origin.unknown());
-    }
-    builder
-        .setProgramConsumer(sink.wrapProgramConsumer(emptyConsumer(backend)))
-        .addLibraryFiles(runtimeJar(backend))
-        .addProguardConfiguration(
-            ImmutableList.of(
-                // Keep all classes to prevent changes to the class hierarchy (e.g., due to
-                // vertical class merging).
-                "-keep class " + InterfaceA.class.getPackage().getName() + ".*",
-                keepMainProguardConfigurationWithInliningAnnotation(TestClass.class)),
-            Origin.unknown());
-    ToolHelper.allowTestProguardOptions(builder);
-    if (backend == Backend.DEX) {
-      builder.setMinApiLevel(ToolHelper.getMinApiLevelForDexVm().getLevel());
-    }
-    R8.run(builder.build());
+    testForJvm()
+        .addTestClasspath()
+        .run(TestClass.class)
+        .assertSuccessWithOutput(expected);
 
-    ProcessResult result = runOnVMRaw(sink.build(), TestClass.class, backend);
-    assertEquals(result.toString(), 0, result.exitCode);
-    assertEquals(expected, result.stdout);
+    testForR8(backend)
+        .addProgramClasses(CLASSES)
+        .addKeepPackageRules(TestClass.class.getPackage())
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .run(TestClass.class)
+        .assertSuccessWithOutput(expected);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepAttributesTest.java b/src/test/java/com/android/tools/r8/shaking/KeepAttributesTest.java
index 96ce54a..6992f97 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAttributesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAttributesTest.java
@@ -9,15 +9,8 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.R8;
-import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.forceproguardcompatibility.keepattributes.TestKeepAttributes;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.AndroidAppConsumers;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -70,7 +63,6 @@
   public void keepLineNumberTable()
       throws CompilationFailedException, IOException, ExecutionException {
     List<String> keepRules = ImmutableList.of(
-        "-keep class ** { *; }",
         "-keepattributes " + ProguardKeepAttributes.LINE_NUMBER_TABLE
     );
     MethodSubject mainMethod = compileRunAndGetMain(keepRules, CompilationMode.RELEASE);
@@ -82,7 +74,6 @@
   public void keepLineNumberTableAndLocalVariableTable()
       throws CompilationFailedException, IOException, ExecutionException {
     List<String> keepRules = ImmutableList.of(
-        "-keep class ** { *; }",
         "-keepattributes "
             + ProguardKeepAttributes.LINE_NUMBER_TABLE
             + ", "
@@ -97,7 +88,6 @@
   @Test
   public void keepLocalVariableTable() throws IOException, ExecutionException {
     List<String> keepRules = ImmutableList.of(
-        "-keep class ** { *; }",
         "-keepattributes " + ProguardKeepAttributes.LOCAL_VARIABLE_TABLE
     );
     // Compiling with a keep rule for locals but no line results in an error in R8.
@@ -113,21 +103,14 @@
 
   private MethodSubject compileRunAndGetMain(List<String> keepRules, CompilationMode mode)
       throws CompilationFailedException, IOException, ExecutionException {
-    AndroidAppConsumers sink = new AndroidAppConsumers();
-    R8.run(
-        R8Command.builder()
-            .setMode(mode)
-            .addProgramFiles(
-                ToolHelper.getClassFilesForTestDirectory(
-                    ToolHelper.getClassFileForTestClass(CLASS).getParent()))
-            .addLibraryFiles(runtimeJar(backend))
-            .addProguardConfiguration(keepRules, Origin.unknown())
-            .setProgramConsumer(sink.wrapProgramConsumer(emptyConsumer(backend)))
-            .build());
-    AndroidApp app = sink.build();
-    CodeInspector codeInspector = new CodeInspector(app);
-    runOnVM(app, CLASS.getTypeName(), backend);
-    return codeInspector.clazz(CLASS).mainMethod();
+    return testForR8(backend)
+        .setMode(mode)
+        .addProgramClassesAndInnerClasses(CLASS)
+        .addKeepAllClassesRule()
+        .addKeepRules(keepRules)
+        .run(CLASS)
+        .inspector()
+        .clazz(CLASS)
+        .mainMethod();
   }
-
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/ExtendsMergedTypeDirectlyTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/ExtendsMergedTypeDirectlyTest.java
new file mode 100644
index 0000000..2523363
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/ExtendsMergedTypeDirectlyTest.java
@@ -0,0 +1,58 @@
+// 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 com.android.tools.r8.shaking.ifrule.verticalclassmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+
+public class ExtendsMergedTypeDirectlyTest extends MergedTypeBaseTest {
+
+  static class TestClass extends C {
+
+    public static void main(String[] args) {
+      System.out.print("Hello world");
+    }
+  }
+
+  public ExtendsMergedTypeDirectlyTest(Backend backend, boolean enableVerticalClassMerging) {
+    super(backend, enableVerticalClassMerging);
+  }
+
+  @Override
+  public Class<?> getTestClass() {
+    return TestClass.class;
+  }
+
+  @Override
+  public String getConditionForProguardIfRule() {
+    // After class merging, TestClass will no longer extend C, but we should still keep the class
+    // Unused in the output.
+    return "-if class **$TestClass extends **$C";
+  }
+
+  @Override
+  public String getExpectedStdout() {
+    return "Hello world";
+  }
+
+  public void inspect(CodeInspector inspector) {
+    super.inspect(inspector);
+
+    if (enableVerticalClassMerging) {
+      // Check that TestClass no longer extends C.
+      ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+      assertThat(testClassSubject, isPresent());
+      assertEquals("java.lang.Object", testClassSubject.getDexClass().superType.toSourceString());
+
+      // Check that C is no longer present.
+      assertThat(inspector.clazz(C.class), not(isPresent()));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/ExtendsMergedTypeIndirectlyTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/ExtendsMergedTypeIndirectlyTest.java
new file mode 100644
index 0000000..10cff1e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/ExtendsMergedTypeIndirectlyTest.java
@@ -0,0 +1,50 @@
+// 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 com.android.tools.r8.shaking.ifrule.verticalclassmerging;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+
+public class ExtendsMergedTypeIndirectlyTest extends MergedTypeBaseTest {
+
+  static class TestClass extends B {
+
+    public static void main(String[] args) {
+      // The instantiation of B prevents it from being merged into TestClass.
+      System.out.print(new B().getClass().getName());
+    }
+  }
+
+  public ExtendsMergedTypeIndirectlyTest(Backend backend, boolean enableVerticalClassMerging) {
+    super(backend, enableVerticalClassMerging);
+  }
+
+  @Override
+  public Class<?> getTestClass() {
+    return TestClass.class;
+  }
+
+  @Override
+  public String getConditionForProguardIfRule() {
+    // After class merging, B will no longer extend A (and therefore, TestClass will no longer
+    // extend A indirectly), but we should still keep the class Unused in the output.
+    return "-if class **$TestClass extends **$A";
+  }
+
+  @Override
+  public String getExpectedStdout() {
+    return B.class.getName();
+  }
+
+  public void inspect(CodeInspector inspector) {
+    super.inspect(inspector);
+
+    // Verify that TestClass still inherits from B.
+    ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+    assertEquals(B.class.getTypeName(), testClassSubject.getDexClass().superType.toSourceString());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
index 9d2e352..18783750 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
@@ -59,11 +59,9 @@
 }
 
 // TODO(b/110141157):
-// - Add tests where the return type of a kept method changes.
-// - Add tests where the parameter type of a kept method changes.
-// - Add tests where the type of a kept field changes.
 // - Add tests where fields and methods get renamed due to naming conflicts.
-// - Add tests where the type in a implements/extends clause has changed.
+// - Add tests where the type in a implements clause has changed.
+// - Add tests where the then-clause of an -if rule keeps a class that has been merged into another.
 @RunWith(Parameterized.class)
 public class IfRuleWithVerticalClassMerging extends TestBase {
 
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
new file mode 100644
index 0000000..a877fec
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
@@ -0,0 +1,36 @@
+// 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 com.android.tools.r8.shaking.ifrule.verticalclassmerging;
+
+public class MergedFieldTypeTest extends MergedTypeBaseTest {
+
+  static class TestClass {
+
+    private static A field = new B();
+
+    public static void main(String[] args) {
+      System.out.print(field.getClass().getName());
+    }
+  }
+
+  public MergedFieldTypeTest(Backend backend, boolean enableVerticalClassMerging) {
+    super(backend, enableVerticalClassMerging);
+  }
+
+  @Override
+  public Class<?> getTestClass() {
+    return TestClass.class;
+  }
+
+  @Override
+  public String getConditionForProguardIfRule() {
+    return "-if class **$TestClass { **$A field; }";
+  }
+
+  @Override
+  public String getExpectedStdout() {
+    return B.class.getName();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java
new file mode 100644
index 0000000..7447efd1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedParameterTypeTest.java
@@ -0,0 +1,38 @@
+// 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 com.android.tools.r8.shaking.ifrule.verticalclassmerging;
+
+public class MergedParameterTypeTest extends MergedTypeBaseTest {
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      method(new B());
+    }
+
+    public static void method(A obj) {
+      System.out.print(obj.getClass().getName());
+    }
+  }
+
+  public MergedParameterTypeTest(Backend backend, boolean enableVerticalClassMerging) {
+    super(backend, enableVerticalClassMerging);
+  }
+
+  @Override
+  public Class<?> getTestClass() {
+    return TestClass.class;
+  }
+
+  @Override
+  public String getConditionForProguardIfRule() {
+    return "-if class **$TestClass { void method(**$A); }";
+  }
+
+  @Override
+  public String getExpectedStdout() {
+    return B.class.getName();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedReturnTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedReturnTypeTest.java
new file mode 100644
index 0000000..da4a9f1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedReturnTypeTest.java
@@ -0,0 +1,38 @@
+// 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 com.android.tools.r8.shaking.ifrule.verticalclassmerging;
+
+public class MergedReturnTypeTest extends MergedTypeBaseTest {
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.print(method().getClass().getName());
+    }
+
+    public static A method() {
+      return new B();
+    }
+  }
+
+  public MergedReturnTypeTest(Backend backend, boolean enableVerticalClassMerging) {
+    super(backend, enableVerticalClassMerging);
+  }
+
+  @Override
+  public Class<?> getTestClass() {
+    return TestClass.class;
+  }
+
+  @Override
+  public String getConditionForProguardIfRule() {
+    return "-if class **$TestClass { **$A method(); }";
+  }
+
+  @Override
+  public String getExpectedStdout() {
+    return B.class.getName();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java
new file mode 100644
index 0000000..d2ce210
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedTypeBaseTest.java
@@ -0,0 +1,98 @@
+// 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 com.android.tools.r8.shaking.ifrule.verticalclassmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public abstract class MergedTypeBaseTest extends TestBase {
+
+  private final List<Class> CLASSES =
+      ImmutableList.of(A.class, B.class, C.class, Unused.class, getTestClass());
+
+  static class A {}
+
+  static class B extends A {}
+
+  static class C {}
+
+  static class Unused {}
+
+  final Backend backend;
+  final boolean enableVerticalClassMerging;
+
+  public MergedTypeBaseTest(Backend backend, boolean enableVerticalClassMerging) {
+    this.backend = backend;
+    this.enableVerticalClassMerging = enableVerticalClassMerging;
+  }
+
+  @Parameters(name = "Backend: {0}, vertical class merging: {1}")
+  public static Collection<Object[]> data() {
+    // We don't run this on Proguard, as Proguard does not merge A into B.
+    return ImmutableList.of(
+        new Object[] {Backend.DEX, true},
+        new Object[] {Backend.DEX, false},
+        new Object[] {Backend.CF, true},
+        new Object[] {Backend.CF, false});
+  }
+
+  public abstract Class<?> getTestClass();
+
+  public abstract String getConditionForProguardIfRule();
+
+  public abstract String getExpectedStdout();
+
+  public void inspect(CodeInspector inspector) {
+    assertThat(inspector.clazz(Unused.class), isPresent());
+
+    // Verify that A is no longer present when vertical class merging is enabled.
+    if (enableVerticalClassMerging) {
+      assertThat(inspector.clazz(A.class), not(isPresent()));
+    }
+  }
+
+  @Test
+  public void testIfRule() throws Exception {
+    String expected = getExpectedStdout();
+    assertEquals(expected, runOnJava(getTestClass()));
+
+    String config =
+        StringUtils.joinLines(
+            "-keep class " + getTestClass().getTypeName() + " {",
+            "  public static void main(java.lang.String[]);",
+            "}",
+            getConditionForProguardIfRule(),
+            "-keep class " + Unused.class.getTypeName());
+    AndroidApp output = compileWithR8(readClasses(CLASSES), config, this::configure, backend);
+    assertEquals(expected, runOnVM(output, getTestClass(), backend));
+    inspect(new CodeInspector(output));
+  }
+
+  private void configure(InternalOptions options) {
+    options.enableMinification = false;
+    options.enableVerticalClassMerging = enableVerticalClassMerging;
+
+    // TODO(b/110148109): Allow ordinary method inlining when -if rules work with inlining.
+    options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+  }
+}
diff --git a/third_party/dart-sdk.tar.gz.sha1 b/third_party/dart-sdk.tar.gz.sha1
new file mode 100644
index 0000000..f8b4a60
--- /dev/null
+++ b/third_party/dart-sdk.tar.gz.sha1
@@ -0,0 +1 @@
+0f15c6a81827ce4979b6fcf6183ef35df335d9dc
\ No newline at end of file
diff --git a/tools/disasm.py b/tools/disasm.py
index 0d2599a..a9a5a0a 100755
--- a/tools/disasm.py
+++ b/tools/disasm.py
@@ -7,4 +7,4 @@
 import toolhelper
 
 if __name__ == '__main__':
-  sys.exit(toolhelper.run('disasm', sys.argv[1:]))
+  sys.exit(toolhelper.run('disasm', sys.argv[1:]), debug=False)
diff --git a/tools/performance_try.py b/tools/performance_try.py
new file mode 100755
index 0000000..5e6404d
--- /dev/null
+++ b/tools/performance_try.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+# 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.
+
+import os
+import subprocess
+import sys
+import utils
+
+SCRIPT = '/google/data/ro/teams/dart/golem/bin/golem4.dart'
+DART = os.path.join(utils.THIRD_PARTY, 'dart-sdk', 'bin', 'dart')
+
+def Main():
+  args = sys.argv[1:]
+  if len(args) != 1:
+    print('Performance tracking takes exactly one argument, the name for display')
+  subprocess.check_call([DART, SCRIPT, args[0]])
+
+if __name__ == '__main__':
+  sys.exit(Main())
diff --git a/tools/test.py b/tools/test.py
index 72e7e01..cd9b70f 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -52,14 +52,14 @@
       help='Print a line before a tests starts and after it ends to stdout.',
       default=False, action='store_true')
   result.add_option('--tool',
-      help='Tool to run ART tests with: "r8" (default) or "d8". Ignored if'
-          ' "--all_tests" enabled.',
-      default=None, choices=["r8", "d8"])
+      help='Tool to run ART tests with: "r8" (default) or "d8" or "r8cf"'
+          ' (r8 w/CF-backend). Ignored if "--all_tests" enabled.',
+      default=None, choices=["r8", "d8", "r8cf"])
   result.add_option('--jctf',
-      help='Run JCTF tests with: "r8" (default) or "d8".',
+      help='Run JCTF tests with: "r8" (default) or "d8" or "r8cf".',
       default=False, action='store_true')
   result.add_option('--only-jctf', '--only_jctf',
-      help='Run only JCTF tests with: "r8" (default) or "d8".',
+      help='Run only JCTF tests with: "r8" (default) or "d8" or "r8cf".',
       default=False, action='store_true')
   result.add_option('--jctf-compile-only', '--jctf_compile_only',
       help="Don't run, only compile JCTF tests.",