Version 2.1.64

Cherry pick: Disable class merging from base to feature
CL: https://r8-review.googlesource.com/c/r8/+/52963

Cherry pick: Disable devirtualization across feature splits
CL: https://r8-review.googlesource.com/c/r8/+/53024

Cherry pick: Add tests for vertical class merging in presence of feature splits
CL: https://r8-review.googlesource.com/c/r8/+/52962

Bug: 164937965, 165324486
Change-Id: Ibf315981ff3ef840e232e7459d4ae1559bebe393
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 69ccb47..8a1d666 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "2.1.63";
+  public static final String LABEL = "2.1.64";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java b/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java
index 610104a..74ef8d9 100644
--- a/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java
+++ b/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java
@@ -120,7 +120,7 @@
     return !isInFeature(clazz);
   }
 
-  public boolean inSameFeatureOrBase(DexMethod a, DexMethod b){
+  public boolean inSameFeatureOrBase(DexMethod a, DexMethod b) {
     return inSameFeatureOrBase(a.holder, b.holder);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index 7752d6c..ad2c233 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize;
 
+import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -23,7 +24,6 @@
 import com.android.tools.r8.ir.code.InvokeSuper;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -165,10 +165,9 @@
         if (holderClass == null || holderClass.isInterface()) {
           continue;
         }
+
         // Due to the potential downcast below, make sure the new target holder is visible.
-        ConstraintWithTarget visibility =
-            ConstraintWithTarget.classIsVisible(context.getHolder(), holderType, appView);
-        if (visibility == ConstraintWithTarget.NEVER) {
+        if (AccessControl.isClassAccessible(holderClass, context, appView).isPossiblyFalse()) {
           continue;
         }
 
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 da9038d..92cf1b7 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -8,6 +8,7 @@
 import static com.android.tools.r8.ir.code.Invoke.Type.STATIC;
 
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.Code;
@@ -359,10 +360,14 @@
         .map(DexEncodedMember::toReference)
         .noneMatch(appInfo::isPinned);
 
-    if (appView.options().featureSplitConfiguration != null
-        && appView.options().featureSplitConfiguration.isInFeature(sourceClass)) {
+    FeatureSplitConfiguration featureSplitConfiguration =
+        appView.options().featureSplitConfiguration;
+    if (featureSplitConfiguration != null) {
       // TODO(b/141452765): Allow class merging between classes in features.
-      return false;
+      if (featureSplitConfiguration.isInFeature(sourceClass)
+          || featureSplitConfiguration.isInFeature(targetClass)) {
+        return false;
+      }
     }
     if (appView.appServices().allServiceTypes().contains(sourceClass.type)
         && appInfo.isPinned(targetClass.type)) {
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 2675329..64751df 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -3,11 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.dexsplitter.SplitterTestBase.simpleSplitProvider;
 import static org.hamcrest.CoreMatchers.containsString;
 
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase.KeepRuleConsumer;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.RunInterface;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.origin.Origin;
@@ -63,6 +66,7 @@
   private List<String> keepRules = new ArrayList<>();
   private List<Path> mainDexRulesFiles = new ArrayList<>();
   private List<String> applyMappingMaps = new ArrayList<>();
+  private final List<Path> features = new ArrayList<>();
 
   @Override
   R8TestCompileResult internalCompile(
@@ -136,7 +140,8 @@
             box.syntheticProguardRules,
             proguardMapBuilder.toString(),
             graphConsumer,
-            builder.getMinApiLevel());
+            builder.getMinApiLevel(),
+            features);
     switch (allowedDiagnosticMessages) {
       case ALL:
         compileResult.assertDiagnosticThatMatches(new IsAnything<>());
@@ -553,8 +558,22 @@
     return self();
   }
 
+  public T addFeatureSplitRuntime() {
+    addProgramClasses(SplitRunner.class, RunInterface.class);
+    addKeepClassAndMembersRules(SplitRunner.class, RunInterface.class);
+    return self();
+  }
+
   public T addFeatureSplit(Function<FeatureSplit.Builder, FeatureSplit> featureSplitBuilder) {
     builder.addFeatureSplit(featureSplitBuilder);
     return self();
   }
+
+  public T addFeatureSplit(Class<?>... classes) throws IOException {
+    Path path = getState().getNewTempFile("feature.zip");
+    builder.addFeatureSplit(
+        builder -> simpleSplitProvider(builder, path, getState().getTempFolder(), classes));
+    features.add(path);
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 5dac954..b217591 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -3,12 +3,19 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
 import com.android.tools.r8.shaking.CollectingGraphConsumer;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
 import java.io.IOException;
@@ -23,6 +30,7 @@
   private final String proguardMap;
   private final CollectingGraphConsumer graphConsumer;
   private final int minApiLevel;
+  private final List<Path> features;
 
   R8TestCompileResult(
       TestState state,
@@ -32,13 +40,15 @@
       List<ProguardConfigurationRule> syntheticProguardRules,
       String proguardMap,
       CollectingGraphConsumer graphConsumer,
-      int minApiLevel) {
+      int minApiLevel,
+      List<Path> features) {
     super(state, app, outputMode);
     this.proguardConfiguration = proguardConfiguration;
     this.syntheticProguardRules = syntheticProguardRules;
     this.proguardMap = proguardMap;
     this.graphConsumer = graphConsumer;
     this.minApiLevel = minApiLevel;
+    this.features = features;
   }
 
   @Override
@@ -56,6 +66,10 @@
     return self();
   }
 
+  public Path getFeature(int index) {
+    return features.get(index);
+  }
+
   @Override
   public String getStdout() {
     return state.getStdout();
@@ -71,6 +85,21 @@
     return new CodeInspector(app, proguardMap);
   }
 
+  private CodeInspector featureInspector(Path feature) throws IOException {
+    return new CodeInspector(
+        AndroidApp.builder().addProgramFile(feature).setProguardMapOutputData(proguardMap).build());
+  }
+
+  public <E extends Throwable> R8TestCompileResult inspect(
+      ThrowingConsumer<CodeInspector, E>... consumers) throws IOException, E {
+    assertEquals(1 + features.size(), consumers.length);
+    consumers[0].accept(inspector());
+    for (int i = 0; i < features.size(); i++) {
+      consumers[i + 1].accept(featureInspector(features.get(i)));
+    }
+    return self();
+  }
+
   public GraphInspector graphInspector() throws IOException {
     assert graphConsumer != null;
     return new GraphInspector(graphConsumer, inspector());
@@ -101,6 +130,37 @@
     return new R8TestRunResult(app, runtime, result, proguardMap, this::graphInspector);
   }
 
+  public R8TestRunResult runFeature(TestRuntime runtime, Class<?> mainFeatureClass)
+      throws IOException {
+    return runFeature(runtime, mainFeatureClass, features.get(0));
+  }
+
+  public R8TestRunResult runFeature(
+      TestRuntime runtime, Class<?> mainFeatureClass, Path feature, Path... featureDependencies)
+      throws IOException {
+    assert getBackend() == runtime.getBackend();
+    ClassSubject mainClassSubject = inspector().clazz(SplitRunner.class);
+    assertThat("Did you forget a keep rule for the main method?", mainClassSubject, isPresent());
+    assertThat(
+        "Did you forget a keep rule for the main method?",
+        mainClassSubject.mainMethod(),
+        isPresent());
+    ClassSubject mainFeatureClassSubject = featureInspector(feature).clazz(mainFeatureClass);
+    assertThat(
+        "Did you forget a keep rule for the run method?", mainFeatureClassSubject, isPresent());
+    assertThat(
+        "Did you forget a keep rule for the run method?",
+        mainFeatureClassSubject.uniqueMethodWithName("run"),
+        isPresent());
+    String[] args = new String[2 + featureDependencies.length];
+    args[0] = mainFeatureClassSubject.getFinalName();
+    args[1] = feature.toString();
+    for (int i = 2; i < args.length; i++) {
+      args[i] = featureDependencies[i - 2].toString();
+    }
+    return runArt(runtime, additionalRunClassPath, mainClassSubject.getFinalName(), args);
+  }
+
   public String getProguardMap() {
     return proguardMap;
   }
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 0816496..3294828 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -419,7 +419,7 @@
     return createRunResult(runtime, result);
   }
 
-  private RR runArt(
+  RR runArt(
       TestRuntime runtime, List<Path> additionalClassPath, String mainClass, String... arguments)
       throws IOException {
     DexVm vm = runtime.asDex().getVm();
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index b9020eb..a088c00 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.RunInterface;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.TypeReference;
 import java.io.IOException;
@@ -171,6 +172,31 @@
     return self();
   }
 
+  public T addKeepFeatureMainRule(Class<?> mainClass) {
+    return addKeepFeatureMainRule(mainClass.getTypeName());
+  }
+
+  public T addKeepFeatureMainRules(Class<?>... mainClasses) {
+    for (Class<?> mainClass : mainClasses) {
+      this.addKeepFeatureMainRule(mainClass);
+    }
+    return self();
+  }
+
+  public T addKeepFeatureMainRule(String mainClass) {
+    return addKeepRules(
+        "-keep public class " + mainClass,
+        "    implements " + RunInterface.class.getTypeName() + " {",
+        "  public void <init>();",
+        "  public void run();",
+        "}");
+  }
+
+  public T addKeepFeatureMainRules(List<String> mainClasses) {
+    mainClasses.forEach(this::addKeepFeatureMainRule);
+    return self();
+  }
+
   public T addKeepMethodRules(Class<?> clazz, String... methodSignatures) {
     StringBuilder sb = new StringBuilder();
     sb.append("-keep class " + clazz.getTypeName() + " {\n");
diff --git a/src/test/java/com/android/tools/r8/TestState.java b/src/test/java/com/android/tools/r8/TestState.java
index 78388bc..6ba75d7 100644
--- a/src/test/java/com/android/tools/r8/TestState.java
+++ b/src/test/java/com/android/tools/r8/TestState.java
@@ -19,10 +19,18 @@
     this.temp = temp;
   }
 
+  public TemporaryFolder getTempFolder() {
+    return temp;
+  }
+
   public Path getNewTempFolder() throws IOException {
     return temp.newFolder().toPath();
   }
 
+  public Path getNewTempFile(String name) throws IOException {
+    return getNewTempFolder().resolve(name);
+  }
+
   DiagnosticsHandler getDiagnosticsHandler() {
     return messages;
   }
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DevirtualizationAcrossFeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/DevirtualizationAcrossFeatureSplitTest.java
new file mode 100644
index 0000000..538f764
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DevirtualizationAcrossFeatureSplitTest.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2020, 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.dexsplitter;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DevirtualizationAcrossFeatureSplitTest extends SplitterTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public DevirtualizationAcrossFeatureSplitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(BaseClass.class, BaseInterface.class)
+        .addFeatureSplitRuntime()
+        .addFeatureSplit(FeatureMain.class, BaseInterfaceImpl.class)
+        .addKeepFeatureMainRules(FeatureMain.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .runFeature(parameters.getRuntime(), FeatureMain.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  // Base.
+
+  public static class BaseClass {
+
+    @NeverInline
+    public static void run(BaseInterface instance) {
+      instance.greet();
+    }
+  }
+
+  public interface BaseInterface {
+
+    void greet();
+  }
+
+  // Feature.
+
+  public static class FeatureMain implements RunInterface {
+
+    @Override
+    public void run() {
+      BaseClass.run(new BaseInterfaceImpl());
+    }
+  }
+
+  public static class BaseInterfaceImpl implements BaseInterface {
+
+    @Override
+    public void greet() {
+      System.out.println("Hello world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
index 16291c4..fbe1111 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
@@ -269,27 +269,30 @@
     void run();
   }
 
-  static class SplitRunner {
+  public static class SplitRunner {
     /* We support two different modes:
      *   - One argument to main:
      *     Pass in the class to be loaded, must implement RunInterface, run will be called
-     *   - Two arguments to main:
+     *   - Two or more arguments to main:
      *     Pass in the class to be loaded, must implement RunInterface, run will be called
-     *     Pass in the feature split that we class load
-     *
+     *     Pass in the feature split that we class load, and an optional list of other feature
+     *     splits that must be loaded before the given feature split.
      */
     public static void main(String[] args) {
-      if (args.length < 1 || args.length > 2) {
+      if (args.length < 1) {
         throw new RuntimeException("Unsupported number of arguments");
       }
       String classToRun = args[0];
       ClassLoader loader = SplitRunner.class.getClassLoader();
-      // In the case where we simulate splits, we pass in the feature as the second argument
-      if (args.length == 2) {
-        try {
-          loader = new PathClassLoader(args[1], SplitRunner.class.getClassLoader());
-        } catch (MalformedURLException e) {
-          throw new RuntimeException("Failed reading input URL");
+      // In the case where we simulate splits, the second argument is the feature to load, followed
+      // by all the other features that it depends on.
+      if (args.length >= 2) {
+        for (int i = args.length - 1; i >= 1; i--) {
+          try {
+            loader = new PathClassLoader(args[i], loader);
+          } catch (MalformedURLException e) {
+            throw new RuntimeException("Failed reading input URL");
+          }
         }
       }
 
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingAcrossFeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingAcrossFeatureSplitTest.java
new file mode 100644
index 0000000..4ff8f4b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingAcrossFeatureSplitTest.java
@@ -0,0 +1,116 @@
+// Copyright (c) 2020, 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.dexsplitter;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.dexsplitter.VerticalClassMergingInFeatureSplitTest.BaseClass;
+import com.android.tools.r8.dexsplitter.VerticalClassMergingInFeatureSplitTest.Feature1Class;
+import com.android.tools.r8.dexsplitter.VerticalClassMergingInFeatureSplitTest.Feature2Class;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class VerticalClassMergingAcrossFeatureSplitTest extends SplitterTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public VerticalClassMergingAcrossFeatureSplitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(BaseClass.class)
+            .addFeatureSplitRuntime()
+            .addFeatureSplit(Feature1Class.class)
+            .addFeatureSplit(Feature2Main.class, Feature2Class.class)
+            .addKeepFeatureMainRule(Feature2Main.class)
+            .enableInliningAnnotations()
+            .enableNeverClassInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::inspectBase, this::inspectFeature1, this::inspectFeature2);
+
+    // Run feature 2 on top of feature 1.
+    compileResult
+        .runFeature(
+            parameters.getRuntime(),
+            Feature2Main.class,
+            compileResult.getFeature(1),
+            compileResult.getFeature(0))
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspectBase(CodeInspector inspector) {
+    assertThat(inspector.clazz(BaseClass.class), isPresent());
+  }
+
+  private void inspectFeature1(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1Class.class), isPresent());
+  }
+
+  private void inspectFeature2(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature2Class.class), isPresent());
+  }
+
+  // Base.
+
+  public static class BaseClass {
+
+    @NeverInline
+    public void greet() {
+      System.out.println("world!");
+    }
+  }
+
+  // Feature 1.
+
+  public static class Feature1Class extends BaseClass {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print(" ");
+      super.greet();
+    }
+  }
+
+  // Feature 2.
+
+  public static class Feature2Main implements RunInterface {
+
+    @Override
+    public void run() {
+      new Feature2Class().greet();
+    }
+  }
+
+  @NeverClassInline
+  static class Feature2Class extends Feature1Class {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print("Hello");
+      super.greet();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingInFeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingInFeatureSplitTest.java
new file mode 100644
index 0000000..1a5d43b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingInFeatureSplitTest.java
@@ -0,0 +1,159 @@
+// Copyright (c) 2020, 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.dexsplitter;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class VerticalClassMergingInFeatureSplitTest extends SplitterTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public VerticalClassMergingInFeatureSplitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(BaseClass.class, BaseClassWithBaseSubclass.class)
+            .addFeatureSplitRuntime()
+            .addFeatureSplit(
+                Feature1Main.class, Feature1Class.class, Feature1ClassWithSameFeatureSubclass.class)
+            .addFeatureSplit(
+                Feature2Main.class, Feature2Class.class, Feature2ClassWithSameFeatureSubclass.class)
+            .addKeepFeatureMainRules(Feature1Main.class, Feature2Main.class)
+            .enableInliningAnnotations()
+            .enableNeverClassInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::inspectBase, this::inspectFeature1, this::inspectFeature2);
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature1Main.class, compileResult.getFeature(0))
+        .assertSuccessWithOutputLines("Hello world!");
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature2Main.class, compileResult.getFeature(1))
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspectBase(CodeInspector inspector) {
+    assertThat(inspector.clazz(BaseClass.class), isPresent());
+    assertThat(inspector.clazz(BaseClassWithBaseSubclass.class), not(isPresent()));
+  }
+
+  private void inspectFeature1(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1Class.class), isPresent());
+    // TODO(b/141452765): Should be absent.
+    assertThat(inspector.clazz(Feature1ClassWithSameFeatureSubclass.class), isPresent());
+  }
+
+  private void inspectFeature2(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature2Class.class), isPresent());
+    // TODO(b/141452765): Should be absent.
+    assertThat(inspector.clazz(Feature2ClassWithSameFeatureSubclass.class), isPresent());
+  }
+
+  // Base.
+
+  static class BaseClassWithBaseSubclass {
+
+    @NeverInline
+    public void greet() {
+      System.out.print(" ");
+    }
+  }
+
+  @NeverClassInline
+  public static class BaseClass extends BaseClassWithBaseSubclass {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print("Hello");
+      super.greet();
+    }
+  }
+
+  // Feature 1.
+
+  public static class Feature1Main implements RunInterface {
+
+    @Override
+    public void run() {
+      new BaseClass().greet();
+      new Feature1Class().greet();
+    }
+  }
+
+  static class Feature1ClassWithSameFeatureSubclass {
+
+    @NeverInline
+    public void greet() {
+      System.out.println("!");
+    }
+  }
+
+  @NeverClassInline
+  static class Feature1Class extends Feature1ClassWithSameFeatureSubclass {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print("world");
+      super.greet();
+    }
+  }
+
+  // Feature 2.
+
+  public static class Feature2Main implements RunInterface {
+
+    @NeverInline
+    @Override
+    public void run() {
+      new BaseClass().greet();
+      new Feature2Class().greet();
+    }
+  }
+
+  static class Feature2ClassWithSameFeatureSubclass {
+
+    @NeverInline
+    public void greet() {
+      System.out.println("!");
+    }
+  }
+
+  @NeverClassInline
+  static class Feature2Class extends Feature2ClassWithSameFeatureSubclass {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print("world");
+      super.greet();
+    }
+  }
+}