Add tests for various valid and invalid usages of invokespecial.

These tests hit a lot of incorrect behavior, both valid inputs being incorrectly
compiled, as well as failing inputs compiled to running programs or other
failures, and finally some inputs simply don't verify, so the compiler could
choose to reject them (but does not currently).

Bug: 145187969
Bug: 145775365
Bug: 144410139
Change-Id: Id7a9067ae9242bc3aa4487f509a1002a1403142b
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 2390bc6..cc8af52 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -215,13 +215,20 @@
    */
   public DexEncodedMethod lookupSuperTarget(DexMethod method, DexType invocationContext) {
     assert checkIfObsolete();
+    assert invocationContext.isClassType();
+    DexClass context = definitionFor(invocationContext);
+    return context == null ? null : lookupSuperTarget(method, context);
+  }
+
+  public DexEncodedMethod lookupSuperTarget(DexMethod method, DexClass invocationContext) {
+    assert checkIfObsolete();
     return resolveMethod(method.holder, method).lookupInvokeSuperTarget(invocationContext, this);
   }
 
   /**
    * Lookup direct method following the super chain from the holder of {@code method}.
-   * <p>
-   * This method will lookup private and constructor methods.
+   *
+   * <p>This method will lookup private and constructor methods.
    *
    * @param method the method to lookup
    * @return The actual target for {@code method} or {@code null} if none found.
diff --git a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
index bdea265..32f691c 100644
--- a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
@@ -6,7 +6,6 @@
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.Sets;
@@ -61,8 +60,12 @@
 
   public abstract boolean isValidVirtualTargetForDynamicDispatch();
 
+  /** Lookup the single target of an invoke-special on this resolution result if possible. */
+  public abstract DexEncodedMethod lookupInvokeSpecialTarget(
+      DexProgramClass context, AppInfoWithSubtyping appInfo);
+
   /** Lookup the single target of an invoke-super on this resolution result if possible. */
-  public abstract DexEncodedMethod lookupInvokeSuperTarget(DexType context, AppInfo appInfo);
+  public abstract DexEncodedMethod lookupInvokeSuperTarget(DexClass context, AppInfo appInfo);
 
   public final Set<DexEncodedMethod> lookupVirtualDispatchTargets(
       boolean isInterface, AppInfoWithSubtyping appInfo) {
@@ -143,6 +146,96 @@
     }
 
     /**
+     * This is intended to model the actual behavior of invoke-special on a JVM.
+     *
+     * <p>See https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.invokespecial
+     * and comments below for deviations due to diverging behavior on actual JVMs.
+     */
+    @Override
+    public DexEncodedMethod lookupInvokeSpecialTarget(
+        DexProgramClass context, AppInfoWithSubtyping appInfo) {
+      // If the resolution is non-accessible the no target exists.
+      if (!isAccessibleFrom(context, appInfo)) {
+        return null;
+      }
+
+      // Statics cannot be targeted by invoke-special.
+      if (getResolvedMethod().isStatic()) {
+        return null;
+      }
+
+      // The symbolic reference is the holder type that resolution was initiated at.
+      DexClass symbolicReference = initialResolutionHolder;
+
+      // First part of the spec is to determine the starting point for lookup for invoke special.
+      // Notice that the specification indicates that the immediate super type should
+      // be used when three items hold, the second being:
+      //   is-class(sym-ref) => is-super(sym-ref, context)
+      // in the case of an interface that is trivially satisfied, which would lead the initial type
+      // to be java.lang.Object. However in practice the lookup appears to start at the symbolic
+      // reference in the case of interfaces, so the second condition should likely be interpreted:
+      //   is-class(sym-ref) *and* is-super(sym-ref, context).
+      final DexClass initialType;
+      if (!resolvedMethod.isInstanceInitializer()
+          && !symbolicReference.isInterface()
+          && isSuperclass(symbolicReference, context, appInfo)) {
+        // If reference is a super type of the context then search starts at the immediate super.
+        initialType = appInfo.definitionFor(context.superType);
+      } else {
+        // Otherwise it starts at the reference itself.
+        initialType = symbolicReference;
+      }
+      // Abort if for some reason the starting point could not be found.
+      if (initialType == null) {
+        return null;
+      }
+      // 1-3. Search the initial class and its supers in order for a matching instance method.
+      DexMethod method = getResolvedMethod().method;
+      DexEncodedMethod target = null;
+      DexClass current = initialType;
+      while (current != null) {
+        target = current.lookupMethod(method);
+        if (target != null) {
+          break;
+        }
+        current = current.superType == null ? null : appInfo.definitionFor(current.superType);
+      }
+      // 4. Otherwise, it is the single maximally specific method:
+      if (target == null) {
+        target = appInfo.resolveMaximallySpecificMethods(initialType, method).getSingleTarget();
+      }
+      if (target == null) {
+        return null;
+      }
+      // Linking exceptions:
+      // A non-instance method throws IncompatibleClassChangeError.
+      if (target.isStatic()) {
+        return null;
+      }
+      // An instance initializer that is not to the symbolic reference throws NoSuchMethodError.
+      // It appears as if this check is also in place for non-initializer methods too.
+      // See NestInvokeSpecialMethodAccessWithIntermediateTest.
+      if ((target.isInstanceInitializer() || target.isPrivateMethod())
+          && target.method.holder != symbolicReference.type) {
+        return null;
+      }
+      // Runtime exceptions:
+      // An abstract method throws AbstractMethodError.
+      if (target.isAbstract()) {
+        return null;
+      }
+      // Should we check access control again?
+      if (!AccessControl.isMethodAccessible(target, initialType, context, appInfo)) {
+        return null;
+      }
+      return target;
+    }
+
+    private static boolean isSuperclass(DexClass sup, DexClass sub, AppInfoWithSubtyping appInfo) {
+      return sup != sub && appInfo.isSubtype(sub.type, sup.type);
+    }
+
+    /**
      * Lookup super method following the super chain from the holder of {@code method}.
      *
      * <p>This method will resolve the method on the holder of {@code method} and only return a
@@ -164,17 +257,17 @@
      * @return The actual target for the invoke-super or {@code null} if none found.
      */
     @Override
-    public DexEncodedMethod lookupInvokeSuperTarget(DexType context, AppInfo appInfo) {
+    public DexEncodedMethod lookupInvokeSuperTarget(DexClass context, AppInfo appInfo) {
+      assert context != null;
       DexMethod method = resolvedMethod.method;
       // TODO(b/145775365): Check the requirements for an invoke-special to a protected method.
       // See https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokespecial
 
       if (appInfo.hasSubtyping()
-          && !appInfo.withSubtyping().isSubtype(context, initialResolutionHolder.type)) {
-        DexClass contextClass = appInfo.definitionFor(context);
+          && !appInfo.withSubtyping().isSubtype(context.type, initialResolutionHolder.type)) {
         throw new CompilationError(
             "Illegal invoke-super to " + method.toSourceString() + " from class " + context,
-            contextClass != null ? contextClass.getOrigin() : Origin.unknown());
+            context.getOrigin());
       }
 
       // According to
@@ -189,12 +282,11 @@
         return appInfo.resolveMethodOnInterface(initialResolutionHolder, method).getSingleTarget();
       }
       // Then, resume on the search, but this time, starting from the holder of the caller.
-      DexClass contextClass = appInfo.definitionFor(context);
-      if (contextClass == null || contextClass.superType == null) {
+      if (context.superType == null) {
         return null;
       }
       SingleResolutionResult resolution =
-          appInfo.resolveMethodOnClass(contextClass.superType, method).asSingleResolution();
+          appInfo.resolveMethodOnClass(context.superType, method).asSingleResolution();
       return resolution != null && !resolution.resolvedMethod.isStatic()
           ? resolution.resolvedMethod
           : null;
@@ -305,7 +397,13 @@
   abstract static class EmptyResult extends ResolutionResult {
 
     @Override
-    public final DexEncodedMethod lookupInvokeSuperTarget(DexType context, AppInfo appInfo) {
+    public final DexEncodedMethod lookupInvokeSpecialTarget(
+        DexProgramClass context, AppInfoWithSubtyping appInfo) {
+      return null;
+    }
+
+    @Override
+    public final DexEncodedMethod lookupInvokeSuperTarget(DexClass context, AppInfo appInfo) {
       return null;
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 3548caf..f4f02d3 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -2181,7 +2181,11 @@
       return;
     }
 
+    // We should be passing the full program context and not looking it up again here.
+    DexProgramClass fromHolder = appInfo.definitionFor(from.method.holder).asProgramClass();
+
     DexEncodedMethod resolutionTarget = resolution.getResolvedMethod();
+    // TODO(b/145187573): Check access.
     if (resolutionTarget.accessFlags.isPrivate() || resolutionTarget.accessFlags.isStatic()) {
       brokenSuperInvokes.add(resolutionTarget.method);
     }
@@ -2190,9 +2194,8 @@
       markMethodAsTargeted(
           resolutionTargetClass, resolutionTarget, KeepReason.targetedBySuperFrom(from));
     }
-
     // Now we need to compute the actual target in the context.
-    DexEncodedMethod target = resolution.lookupInvokeSuperTarget(from.method.holder, appInfo);
+    DexEncodedMethod target = resolution.lookupInvokeSuperTarget(fromHolder, appInfo);
     if (target == null) {
       // The actual implementation in the super class is missing.
       reportMissingMethod(method);
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 77de8e2..9a477cc 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -1147,7 +1147,7 @@
           // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
           boolean resolutionSucceeds =
               holder.lookupVirtualMethod(signatureInHolder) != null
-                  || appInfo.lookupSuperTarget(signatureInHolder, holder.type) != null;
+                  || appInfo.lookupSuperTarget(signatureInHolder, holder) != null;
           if (resolutionSucceeds) {
             deferredRenamings.mapVirtualMethodToDirectInType(
                 signatureInHolder, new GraphLenseLookupResult(newTarget, DIRECT), target.type);
@@ -1169,7 +1169,7 @@
               // its super classes declared the method.
               boolean resolutionSucceededBeforeMerge =
                   renamedMembersLense.hasMappingForSignatureInContext(holder.type, signatureInType)
-                      || appInfo.lookupSuperTarget(signatureInHolder, holder.type) != null;
+                      || appInfo.lookupSuperTarget(signatureInHolder, holder) != null;
               if (resolutionSucceededBeforeMerge) {
                 deferredRenamings.mapVirtualMethodToDirectInType(
                     signatureInType, new GraphLenseLookupResult(newTarget, DIRECT), target.type);
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessTest.java
new file mode 100644
index 0000000..bc15e9e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessTest.java
@@ -0,0 +1,236 @@
+// Copyright (c) 2019, 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.resolution.access;
+
+import static com.android.tools.r8.TestRuntime.CfVm.JDK11;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.graph.ResolutionResult.NoSuchMethodResult;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Opcodes;
+
+/** Tests the behavior of invoke-special on interfaces with a direct private definition. */
+@RunWith(Parameterized.class)
+public class NestInvokeSpecialInterfaceMethodAccessTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("I::bar");
+
+  private final TestParameters parameters;
+
+  // If true, all classes are in the same nest, otherwise each is in its own.
+  private final boolean inSameNest;
+
+  // If true, the invoke will reference the actual type defining the method.
+  private final boolean symbolicReferenceIsDefiningType;
+
+  @Parameterized.Parameters(name = "{0}, in-same-nest:{1}, sym-ref-is-def-type:{2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters()
+            .withCfRuntimesStartingFromIncluding(JDK11)
+            .withDexRuntimes()
+            .withAllApiLevels()
+            .build(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
+  }
+
+  public NestInvokeSpecialInterfaceMethodAccessTest(
+      TestParameters parameters, boolean inSameNest, boolean symbolicReferenceIsDefiningType) {
+    this.parameters = parameters;
+    this.inSameNest = inSameNest;
+    this.symbolicReferenceIsDefiningType = symbolicReferenceIsDefiningType;
+  }
+
+  public Collection<Class<?>> getClasses() {
+    return ImmutableList.of(Main.class);
+  }
+
+  public Collection<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        withNest(I.class).setPrivate(I.class.getDeclaredMethod("bar")).transform(),
+        withNest(A.class)
+            .transformMethodInsnInMethod(
+                "foo",
+                (opcode, owner, name, descriptor, isInterface, continuation) -> {
+                  assertEquals(Opcodes.INVOKEVIRTUAL, opcode);
+                  assertEquals(DescriptorUtils.getBinaryNameFromJavaType(A.class.getName()), owner);
+                  String newOwner =
+                      symbolicReferenceIsDefiningType
+                          ? DescriptorUtils.getBinaryNameFromJavaType(I.class.getName())
+                          : DescriptorUtils.getBinaryNameFromJavaType(A.class.getName());
+                  boolean newIsInterface = symbolicReferenceIsDefiningType;
+                  continuation.apply(
+                      Opcodes.INVOKESPECIAL, newOwner, name, descriptor, newIsInterface);
+                })
+            .transform());
+  }
+
+  private ClassFileTransformer withNest(Class<?> clazz) throws Exception {
+    if (inSameNest) {
+      // If in the same nest make A host and B a member.
+      return transformer(clazz).setNest(I.class, A.class);
+    }
+    // Otherwise, set the class to be its own host and no additional members.
+    return transformer(clazz).setNest(clazz);
+  }
+
+  @Test
+  public void testResolutionAccess() throws Exception {
+    // White-box test of the R8 resolution and lookup methods.
+    Class<?> definingClass = I.class;
+    Class<?> declaredClass = symbolicReferenceIsDefiningType ? definingClass : A.class;
+    Class<?> callerClass = A.class;
+
+    AppView<AppInfoWithLiveness> appView = getAppView();
+    AppInfoWithLiveness appInfo = appView.appInfo();
+
+    DexProgramClass definingClassDefinition = getDexProgramClass(definingClass, appInfo);
+    DexProgramClass declaredClassDefinition = getDexProgramClass(declaredClass, appInfo);
+    DexProgramClass callerClassDefinition = getDexProgramClass(callerClass, appInfo);
+
+    DexMethod method = getTargetMethodSignature(declaredClass, appInfo);
+    assertCallingClassCallsTarget(callerClass, appInfo, method);
+
+    // Resolve the method from the point of the declared holder.
+    assertEquals(method.holder, declaredClassDefinition.type);
+    ResolutionResult resolutionResult = appInfo.resolveMethod(declaredClassDefinition, method);
+
+    if (!symbolicReferenceIsDefiningType) {
+      // The targeted method is a private interface method and thus not a maximally specific method.
+      assertTrue(resolutionResult instanceof NoSuchMethodResult);
+      return;
+    }
+
+    assertEquals(inSameNest, resolutionResult.isAccessibleFrom(callerClassDefinition, appInfo));
+    DexEncodedMethod targetSpecial =
+        resolutionResult.lookupInvokeSpecialTarget(callerClassDefinition, appInfo);
+    DexEncodedMethod targetSuper =
+        resolutionResult.lookupInvokeSuperTarget(callerClassDefinition, appInfo);
+    if (inSameNest) {
+      assertEquals(definingClassDefinition.type, targetSpecial.method.holder);
+      assertEquals(targetSpecial, targetSuper);
+    } else {
+      assertNull(targetSpecial);
+      // TODO(b/145775365): Invoke super currently returns the resolution target.
+      assertNotNull(targetSuper);
+    }
+  }
+
+  private void assertCallingClassCallsTarget(
+      Class<?> callerClass, AppInfoWithLiveness appInfo, DexMethod target) {
+    CodeInspector inspector = new CodeInspector(appInfo.app());
+    MethodSubject foo = inspector.clazz(callerClass).uniqueMethodWithName("foo");
+    assertTrue(
+        foo.streamInstructions().anyMatch(i -> i.isInvokeSpecial() && i.getMethod() == target));
+  }
+
+  private DexMethod getTargetMethodSignature(Class<?> declaredClass, AppInfoWithLiveness appInfo) {
+    return buildMethod(
+        Reference.method(Reference.classFromClass(declaredClass), "bar", ImmutableList.of(), null),
+        appInfo.dexItemFactory());
+  }
+
+  private DexProgramClass getDexProgramClass(Class<?> clazz, AppInfoWithLiveness appInfo) {
+    return appInfo.definitionFor(buildType(clazz, appInfo.dexItemFactory())).asProgramClass();
+  }
+
+  private AppView<AppInfoWithLiveness> getAppView() throws Exception {
+    return computeAppViewWithLiveness(
+        buildClasses(getClasses())
+            .addClassProgramData(getTransformedClasses())
+            .addLibraryFile(TestBase.runtimeJar(parameters.getBackend()))
+            .build(),
+        Main.class);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), Main.class)
+        .apply(result -> checkExpectedResult(result, false));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(result -> checkExpectedResult(result, true));
+  }
+
+  private void checkExpectedResult(TestRunResult<?> result, boolean isR8) {
+    if (!symbolicReferenceIsDefiningType) {
+      result.assertFailureWithErrorThatMatches(containsString(NoSuchMethodError.class.getName()));
+    } else if (isDesugaring()) {
+      // TODO(b/145775365): Desugaring results in an incorrect program.
+      result.assertFailureWithErrorThatMatches(containsString(NoSuchMethodError.class.getName()));
+    } else if (!inSameNest) {
+      result.assertFailureWithErrorThatMatches(containsString(IllegalAccessError.class.getName()));
+    } else if (parameters.isDexRuntime()) {
+      // TODO(b/145187969): Incorrect nest desugaring.
+      result.assertFailureWithErrorThatMatches(containsString(IllegalAccessError.class.getName()));
+    } else {
+      result.assertSuccessWithOutput(EXPECTED);
+    }
+  }
+
+  private boolean isDesugaring() {
+    return parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.N);
+  }
+
+  interface I {
+    /* will be private */ default void bar() {
+      System.out.println("I::bar");
+    }
+  }
+
+  static class A implements I {
+    public void foo() {
+      // Rewritten to invoke-special A.bar or I.bar which resolves to private method A.bar
+      // When targeting B.bar => throws NoSuchMethodError.
+      // When targeting A.bar:
+      //   - in same nest => success.
+      //   - not in nest => throws IllegalAccessError.
+      bar();
+    }
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessWithIntermediateTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessWithIntermediateTest.java
new file mode 100644
index 0000000..bae2756
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessWithIntermediateTest.java
@@ -0,0 +1,220 @@
+// Copyright (c) 2019, 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.resolution.access;
+
+import static com.android.tools.r8.TestRuntime.CfVm.JDK11;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.graph.ResolutionResult.NoSuchMethodResult;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Opcodes;
+
+/** Tests the behavior of invoke-special on interfaces with an indirect private definition. */
+@RunWith(Parameterized.class)
+public class NestInvokeSpecialInterfaceMethodAccessWithIntermediateTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  // If true, all classes are in the same nest, otherwise each is in its own.
+  private final boolean inSameNest;
+
+  // If true, the invoke will reference the actual type defining the method.
+  private final boolean symbolicReferenceIsDefiningType;
+
+  @Parameterized.Parameters(name = "{0}, in-same-nest:{1}, sym-ref-is-def-type:{2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters()
+            .withCfRuntimesStartingFromIncluding(JDK11)
+            .withDexRuntimes()
+            .withAllApiLevels()
+            .build(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
+  }
+
+  public NestInvokeSpecialInterfaceMethodAccessWithIntermediateTest(
+      TestParameters parameters, boolean inSameNest, boolean symbolicReferenceIsDefiningType) {
+    this.parameters = parameters;
+    this.inSameNest = inSameNest;
+    this.symbolicReferenceIsDefiningType = symbolicReferenceIsDefiningType;
+  }
+
+  public Collection<Class<?>> getClasses() {
+    return ImmutableList.of(Main.class);
+  }
+
+  public Collection<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        withNest(I.class).setPrivate(I.class.getDeclaredMethod("bar")).transform(),
+        withNest(J.class).transform(),
+        withNest(A.class)
+            .transformMethodInsnInMethod(
+                "foo",
+                (opcode, owner, name, descriptor, isInterface, continuation) -> {
+                  assertEquals(Opcodes.INVOKEVIRTUAL, opcode);
+                  assertEquals(DescriptorUtils.getBinaryNameFromJavaType(A.class.getName()), owner);
+                  String newOwner =
+                      symbolicReferenceIsDefiningType
+                          ? DescriptorUtils.getBinaryNameFromJavaType(I.class.getName())
+                          : DescriptorUtils.getBinaryNameFromJavaType(J.class.getName());
+                  continuation.apply(Opcodes.INVOKESPECIAL, newOwner, name, descriptor, true);
+                })
+            .transform());
+  }
+
+  private ClassFileTransformer withNest(Class<?> clazz) throws Exception {
+    if (inSameNest) {
+      // If in the same nest make A host and B a member.
+      return transformer(clazz).setNest(I.class, J.class, A.class);
+    }
+    // Otherwise, set the class to be its own host and no additional members.
+    return transformer(clazz).setNest(clazz);
+  }
+
+  @Test
+  public void testResolutionAccess() throws Exception {
+    assumeFalse(
+        "b/144410139. Don't test internals for non-verifying input",
+        symbolicReferenceIsDefiningType);
+
+    // White-box test of the R8 resolution and lookup methods.
+    Class<?> definingClass = I.class;
+    Class<?> declaredClass = symbolicReferenceIsDefiningType ? definingClass : J.class;
+    Class<?> callerClass = A.class;
+
+    AppView<AppInfoWithLiveness> appView = getAppView();
+    AppInfoWithLiveness appInfo = appView.appInfo();
+
+    DexProgramClass declaredClassDefinition = getDexProgramClass(declaredClass, appInfo);
+
+    DexMethod method = getTargetMethodSignature(declaredClass, appInfo);
+
+    assertCallingClassCallsTarget(callerClass, appInfo, method);
+
+    // Resolve the method from the point of the declared holder.
+    assertEquals(method.holder, declaredClassDefinition.type);
+    ResolutionResult resolutionResult = appInfo.resolveMethod(declaredClassDefinition, method);
+
+    // The targeted method is a private interface method and thus not a maximally specific method.
+    assertTrue(resolutionResult instanceof NoSuchMethodResult);
+  }
+
+  private void assertCallingClassCallsTarget(
+      Class<?> callerClass, AppInfoWithLiveness appInfo, DexMethod method) {
+    CodeInspector inspector = new CodeInspector(appInfo.app());
+    MethodSubject foo = inspector.clazz(callerClass).uniqueMethodWithName("foo");
+    assertTrue(
+        foo.streamInstructions().anyMatch(i -> i.isInvokeSpecial() && i.getMethod() == method));
+  }
+
+  private DexMethod getTargetMethodSignature(Class<?> declaredClass, AppInfoWithLiveness appInfo) {
+    return buildMethod(
+        Reference.method(Reference.classFromClass(declaredClass), "bar", ImmutableList.of(), null),
+        appInfo.dexItemFactory());
+  }
+
+  private DexProgramClass getDexProgramClass(Class<?> definingClass, AppInfoWithLiveness appInfo) {
+    return appInfo
+        .definitionFor(buildType(definingClass, appInfo.dexItemFactory()))
+        .asProgramClass();
+  }
+
+  private AppView<AppInfoWithLiveness> getAppView() throws Exception {
+    return computeAppViewWithLiveness(
+        buildClasses(getClasses())
+            .addClassProgramData(getTransformedClasses())
+            .addLibraryFile(TestBase.runtimeJar(parameters.getBackend()))
+            .build(),
+        Main.class);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), Main.class)
+        .apply(result -> checkExpectedResult(result, false));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(result -> checkExpectedResult(result, true));
+  }
+
+  private void checkExpectedResult(TestRunResult<?> result, boolean isR8) {
+    if (symbolicReferenceIsDefiningType) {
+      assumeTrue(
+          "TODO(b/144410139): Input does not verify. Should compilation throw an error?",
+          parameters.isCfRuntime() && !isR8);
+      result.assertFailureWithErrorThatMatches(containsString(VerifyError.class.getName()));
+    } else if (isDesugaring()) {
+      // TODO(b/145775365): Desugaring results in a reference to a non-existent companion class.
+      result.assertFailureWithErrorThatMatches(
+          containsString(NoClassDefFoundError.class.getName()));
+    } else {
+      result.assertFailureWithErrorThatMatches(containsString(NoSuchMethodError.class.getName()));
+    }
+  }
+
+  private boolean isDesugaring() {
+    return parameters.isDexRuntime() && parameters.getApiLevel().isLessThan(AndroidApiLevel.N);
+  }
+
+  interface I {
+    /* will be private */ default void bar() {
+      System.out.println("I::bar");
+    }
+  }
+
+  interface J extends I {
+    // Intentionally empty.
+  }
+
+  static class A implements J {
+    public void foo() {
+      // Rewritten to invoke-special I.bar or J.bar which resolves to private method I.bar
+      // With sym-ref I.bar the classfile fails verification.
+      // With sym-ref J.bar results in a NoSuchMethodError.
+      bar();
+    }
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      new A().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessTest.java
new file mode 100644
index 0000000..67d6b4e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessTest.java
@@ -0,0 +1,201 @@
+// Copyright (c) 2019, 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.resolution.access;
+
+import static com.android.tools.r8.TestRuntime.CfVm.JDK11;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class NestInvokeSpecialMethodAccessTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("A::bar");
+
+  private final TestParameters parameters;
+  private final boolean inSameNest;
+
+  @Parameterized.Parameters(name = "{0}, in-same-nest:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters()
+            .withCfRuntimesStartingFromIncluding(JDK11)
+            .withDexRuntimes()
+            .withAllApiLevels()
+            .build(),
+        BooleanUtils.values());
+  }
+
+  public NestInvokeSpecialMethodAccessTest(TestParameters parameters, boolean inSameNest) {
+    this.parameters = parameters;
+    this.inSameNest = inSameNest;
+  }
+
+  private Collection<Class<?>> getClasses() {
+    return ImmutableList.of(Main.class);
+  }
+
+  private Collection<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        withNest(A.class).setPrivate(A.class.getDeclaredMethod("bar")).transform(),
+        withNest(B.class).transform());
+  }
+
+  private ClassFileTransformer withNest(Class<?> clazz) throws Exception {
+    if (inSameNest) {
+      // If in the same nest make A host and B a member.
+      return transformer(clazz).setNest(A.class, B.class);
+    }
+    // Otherwise, set the class to be its own host and no additional members.
+    return transformer(clazz).setNest(clazz);
+  }
+
+  @Test
+  public void testResolutionAccess() throws Exception {
+    // White-box test of the R8 resolution and lookup methods.
+    Class<?> definingClass = A.class;
+    Class<?> declaredClass = A.class;
+    Class<?> callerClass = B.class;
+
+    AppView<AppInfoWithLiveness> appView = getAppView();
+    AppInfoWithLiveness appInfo = appView.appInfo();
+
+    DexClass definingClassDefinition = getDexProgramClass(definingClass, appInfo);
+    DexClass declaredClassDefinition = getDexProgramClass(declaredClass, appInfo);
+    DexProgramClass callerClassDefinition = getDexProgramClass(callerClass, appInfo);
+
+    DexMethod method = getTargetMethodSignature(declaredClass, appInfo);
+
+    assertCallingClassCallsTarget(callerClass, appInfo, method);
+
+    // Resolve the method from the point of the declared holder.
+    assertEquals(method.holder, declaredClassDefinition.type);
+    ResolutionResult resolutionResult = appInfo.resolveMethod(declaredClassDefinition, method);
+
+    // Verify that the resolved method is on the defining class.
+    assertEquals(
+        definingClassDefinition, resolutionResult.asSingleResolution().getResolvedHolder());
+
+    // Verify that the resolved method is accessible if in the same nest.
+    assertEquals(inSameNest, resolutionResult.isAccessibleFrom(callerClassDefinition, appInfo));
+
+    // Verify that looking up the dispatch target returns the defining method.
+    DexEncodedMethod targetSpecial =
+        resolutionResult.lookupInvokeSpecialTarget(callerClassDefinition, appInfo);
+    DexEncodedMethod targetSuper =
+        resolutionResult.lookupInvokeSuperTarget(callerClassDefinition, appInfo);
+    if (inSameNest) {
+      assertEquals(definingClassDefinition.type, targetSpecial.method.holder);
+      assertEquals(targetSpecial, targetSuper);
+    } else {
+      assertNull(targetSpecial);
+      // TODO(b/145775365): Invoke super currently returns the resolution target.
+      assertNotNull(targetSuper);
+    }
+  }
+
+  private void assertCallingClassCallsTarget(
+      Class<?> callerClass, AppInfoWithLiveness appInfo, DexMethod target) {
+    CodeInspector inspector = new CodeInspector(appInfo.app());
+    MethodSubject foo = inspector.clazz(callerClass).uniqueMethodWithName("foo");
+    assertTrue(
+        foo.streamInstructions().anyMatch(i -> i.isInvokeSpecial() && i.getMethod() == target));
+  }
+
+  private DexMethod getTargetMethodSignature(Class<?> declaredClass, AppInfoWithLiveness appInfo) {
+    return buildMethod(
+        Reference.method(Reference.classFromClass(declaredClass), "bar", ImmutableList.of(), null),
+        appInfo.dexItemFactory());
+  }
+
+  private DexProgramClass getDexProgramClass(Class<?> definingClass, AppInfoWithLiveness appInfo) {
+    return appInfo
+        .definitionFor(buildType(definingClass, appInfo.dexItemFactory()))
+        .asProgramClass();
+  }
+
+  private AppView<AppInfoWithLiveness> getAppView() throws Exception {
+    return computeAppViewWithLiveness(
+        buildClasses(getClasses()).addClassProgramData(getTransformedClasses()).build(),
+        Main.class);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkExpectedResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::checkExpectedResult);
+  }
+
+  private void checkExpectedResult(TestRunResult<?> result) {
+    if (inSameNest) {
+      if (parameters.isDexRuntime()) {
+        // TODO(b/145187969): D8/R8 incorrectly compiles the nest based access away.
+        result.assertFailureWithErrorThatMatches(
+            containsString(IllegalAccessError.class.getName()));
+      } else {
+        result.assertSuccessWithOutput(EXPECTED);
+      }
+    } else {
+      result.assertFailureWithErrorThatMatches(containsString(IllegalAccessError.class.getName()));
+    }
+  }
+
+  static class A {
+    /* will be private */ void bar() {
+      System.out.println("A::bar");
+    }
+  }
+
+  static class B extends A {
+    public void foo() {
+      // invoke-special to private method.
+      super.bar();
+    }
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      new B().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java
new file mode 100644
index 0000000..20e58a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java
@@ -0,0 +1,232 @@
+// Copyright (c) 2019, 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.resolution.access;
+
+import static com.android.tools.r8.TestRuntime.CfVm.JDK11;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Opcodes;
+
+/** Tests the behavior of invoke-special among related (non-interface) classes. */
+@RunWith(Parameterized.class)
+public class NestInvokeSpecialMethodAccessWithIntermediateTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("A::bar");
+
+  private final TestParameters parameters;
+
+  // If true, all classes are in the same nest, otherwise each is in its own.
+  private final boolean inSameNest;
+
+  // If true, the invoke will reference the actual type defining the method.
+  private final boolean symbolicReferenceIsDefiningType;
+
+  @Parameterized.Parameters(name = "{0}, in-same-nest:{1}, sym-ref-is-def-type:{2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters()
+            .withCfRuntimesStartingFromIncluding(JDK11)
+            .withDexRuntimes()
+            .withAllApiLevels()
+            .build(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
+  }
+
+  public NestInvokeSpecialMethodAccessWithIntermediateTest(
+      TestParameters parameters, boolean inSameNest, boolean symbolicReferenceIsDefiningType) {
+    this.parameters = parameters;
+    this.inSameNest = inSameNest;
+    this.symbolicReferenceIsDefiningType = symbolicReferenceIsDefiningType;
+  }
+
+  public Collection<Class<?>> getClasses() {
+    return ImmutableList.of(Main.class);
+  }
+
+  public Collection<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        withNest(A.class).setPrivate(A.class.getDeclaredMethod("bar")).transform(),
+        withNest(B.class).transform(),
+        withNest(C.class)
+            .transformMethodInsnInMethod(
+                "foo",
+                (opcode, owner, name, descriptor, isInterface, continuation) -> {
+                  assertEquals(Opcodes.INVOKESPECIAL, opcode);
+                  assertEquals(DescriptorUtils.getBinaryNameFromJavaType(B.class.getName()), owner);
+                  String newOwner =
+                      symbolicReferenceIsDefiningType
+                          ? DescriptorUtils.getBinaryNameFromJavaType(A.class.getName())
+                          : DescriptorUtils.getBinaryNameFromJavaType(B.class.getName());
+                  continuation.apply(opcode, newOwner, name, descriptor, isInterface);
+                })
+            .transform());
+  }
+
+  private ClassFileTransformer withNest(Class<?> clazz) throws Exception {
+    if (inSameNest) {
+      // If in the same nest make A host and B a member.
+      return transformer(clazz).setNest(A.class, B.class, C.class);
+    }
+    // Otherwise, set the class to be its own host and no additional members.
+    return transformer(clazz).setNest(clazz);
+  }
+
+  @Test
+  public void testResolutionAccess() throws Exception {
+    // White-box test of the R8 resolution and lookup methods.
+    Class<?> definingClass = A.class;
+    Class<?> declaredClass = symbolicReferenceIsDefiningType ? definingClass : B.class;
+    Class<?> callerClass = C.class;
+
+    AppView<AppInfoWithLiveness> appView = getAppView();
+    AppInfoWithLiveness appInfo = appView.appInfo();
+
+    DexClass definingClassDefinition = getDexProgramClass(definingClass, appInfo);
+    DexClass declaredClassDefinition = getDexProgramClass(declaredClass, appInfo);
+    DexProgramClass callerClassDefinition = getDexProgramClass(callerClass, appInfo);
+
+    DexMethod method = getTargetMethodSignature(declaredClass, appInfo);
+
+    assertCallingClassCallsTarget(callerClass, appInfo, method);
+
+    // Resolve the method from the point of the declared holder.
+    assertEquals(method.holder, declaredClassDefinition.type);
+    ResolutionResult resolutionResult = appInfo.resolveMethod(declaredClassDefinition, method);
+
+    // Verify that the resolved method is on the defining class.
+    assertEquals(
+        definingClassDefinition, resolutionResult.asSingleResolution().getResolvedHolder());
+
+    // Verify that the resolved method is accessible only when in the same nest.
+    assertEquals(inSameNest, resolutionResult.isAccessibleFrom(callerClassDefinition, appInfo));
+
+    // Verify that looking up the dispatch target returns a valid target
+    // iff in the same nest and declaredHolder == definingHolder.
+    DexEncodedMethod targetSpecial =
+        resolutionResult.lookupInvokeSpecialTarget(callerClassDefinition, appInfo);
+    DexEncodedMethod targetSuper =
+        resolutionResult.lookupInvokeSuperTarget(callerClassDefinition, appInfo);
+    if (inSameNest && symbolicReferenceIsDefiningType) {
+      assertEquals(definingClassDefinition.type, targetSpecial.method.holder);
+      assertEquals(targetSpecial, targetSuper);
+    } else {
+      assertNull(targetSpecial);
+      // TODO(b/145775365): The current invoke-super will return the resolution target.
+      assertNotNull(targetSuper);
+    }
+  }
+
+  private void assertCallingClassCallsTarget(
+      Class<?> callerClass, AppInfoWithLiveness appInfo, DexMethod target) {
+    CodeInspector inspector = new CodeInspector(appInfo.app());
+    MethodSubject foo = inspector.clazz(callerClass).uniqueMethodWithName("foo");
+    assertTrue(
+        foo.streamInstructions().anyMatch(i -> i.isInvokeSpecial() && i.getMethod() == target));
+  }
+
+  private DexMethod getTargetMethodSignature(Class<?> declaredClass, AppInfoWithLiveness appInfo) {
+    return buildMethod(
+        Reference.method(Reference.classFromClass(declaredClass), "bar", ImmutableList.of(), null),
+        appInfo.dexItemFactory());
+  }
+
+  private DexProgramClass getDexProgramClass(Class<?> clazz, AppInfoWithLiveness appInfo) {
+    return appInfo.definitionFor(buildType(clazz, appInfo.dexItemFactory())).asProgramClass();
+  }
+
+  private AppView<AppInfoWithLiveness> getAppView() throws Exception {
+    return computeAppViewWithLiveness(
+        buildClasses(getClasses()).addClassProgramData(getTransformedClasses()).build(),
+        Main.class);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), Main.class)
+        .apply(result -> checkExpectedResult(result, false));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(result -> checkExpectedResult(result, true));
+  }
+
+  private void checkExpectedResult(TestRunResult<?> result, boolean isR8) {
+    if (inSameNest && parameters.isCfRuntime()) {
+      if (isR8) {
+        // TODO(b/145775365): R8 incorrectly compiles the input.
+        result.assertSuccessWithOutput(EXPECTED);
+      } else if (symbolicReferenceIsDefiningType) {
+        result.assertSuccessWithOutput(EXPECTED);
+      } else {
+        result.assertFailureWithErrorThatMatches(containsString(NoSuchMethodError.class.getName()));
+      }
+    } else {
+      result.assertFailureWithErrorThatMatches(containsString(IllegalAccessError.class.getName()));
+    }
+  }
+
+  static class A {
+    /* will be private */ void bar() {
+      System.out.println("A::bar");
+    }
+  }
+
+  static class B extends A {
+    // Intentionally empty.
+  }
+
+  static class C extends B {
+    public void foo() {
+      // invoke-special A.bar or B.bar which resolves to private method A.bar
+      // Without nests, results in an IllegalAccessError.
+      // With nests and sym-ref B.bar, results in a NoSuchMethodError.
+      // With nests and sym-ref A.bar runs without error.
+      super.bar();
+    }
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      new C().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodPublicAccessWithIntermediateTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodPublicAccessWithIntermediateTest.java
new file mode 100644
index 0000000..c45ec8b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodPublicAccessWithIntermediateTest.java
@@ -0,0 +1,190 @@
+// Copyright (c) 2019, 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.resolution.access;
+
+import static com.android.tools.r8.TestRuntime.CfVm.JDK11;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class NestInvokeSpecialMethodPublicAccessWithIntermediateTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("A::bar");
+
+  private final TestParameters parameters;
+  private final boolean inSameNest;
+
+  @Parameterized.Parameters(name = "{0}, in-same-nest:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters()
+            .withCfRuntimesStartingFromIncluding(JDK11)
+            .withDexRuntimes()
+            .withAllApiLevels()
+            .build(),
+        BooleanUtils.values());
+  }
+
+  public NestInvokeSpecialMethodPublicAccessWithIntermediateTest(
+      TestParameters parameters, boolean inSameNest) {
+    this.parameters = parameters;
+    this.inSameNest = inSameNest;
+  }
+
+  public Collection<Class<?>> getClasses() {
+    return ImmutableList.of(Main.class);
+  }
+
+  public Collection<byte[]> getTransformedClasses() throws Exception {
+    return ImmutableList.of(
+        withNest(A.class).transform(),
+        withNest(B.class).transform(),
+        withNest(C.class).transform());
+  }
+
+  private ClassFileTransformer withNest(Class<?> clazz) throws Exception {
+    if (inSameNest) {
+      // If in the same nest make A host and B a member.
+      return transformer(clazz).setNest(A.class, B.class, C.class);
+    }
+    // Otherwise, set the class to be its own host and no additional members.
+    return transformer(clazz).setNest(clazz);
+  }
+
+  @Test
+  public void testResolutionAccess() throws Exception {
+    // White-box test of the R8 resolution and lookup methods.
+    Class<?> definingClass = A.class;
+    Class<?> declaredClass = B.class;
+    Class<?> callerClass = C.class;
+
+    AppView<AppInfoWithLiveness> appView = getAppView();
+
+    AppInfoWithLiveness appInfo = appView.appInfo();
+
+    DexClass definingClassDefinition = getDexProgramClass(definingClass, appInfo);
+    DexClass declaredClassDefinition = getDexProgramClass(declaredClass, appInfo);
+    DexProgramClass callerClassDefinition = getDexProgramClass(callerClass, appInfo);
+
+    DexMethod method = getTargetMethodSignature(declaredClass, appInfo);
+
+    assertCallingClassCallsTarget(callerClass, appInfo, method);
+
+    // Resolve the method from the point of the declared holder.
+    assertEquals(method.holder, declaredClassDefinition.type);
+    ResolutionResult resolutionResult = appInfo.resolveMethod(declaredClassDefinition, method);
+
+    // Verify that the resolved method is on the defining class.
+    assertEquals(
+        definingClassDefinition, resolutionResult.asSingleResolution().getResolvedHolder());
+
+    // Verify that the resolved method is accessible (it is public).
+    assertTrue(resolutionResult.isAccessibleFrom(callerClassDefinition, appInfo));
+
+    // Verify that looking up the dispatch target returns the defining method.
+    DexEncodedMethod targetSpecial =
+        resolutionResult.lookupInvokeSpecialTarget(callerClassDefinition, appInfo);
+    assertEquals(definingClassDefinition.type, targetSpecial.method.holder);
+
+    DexEncodedMethod targetSuper =
+        resolutionResult.lookupInvokeSuperTarget(callerClassDefinition, appInfo);
+    assertEquals(targetSpecial, targetSuper);
+  }
+
+  private void assertCallingClassCallsTarget(
+      Class<?> callerClass, AppInfoWithLiveness appInfo, DexMethod target) {
+    CodeInspector inspector = new CodeInspector(appInfo.app());
+    MethodSubject foo = inspector.clazz(callerClass).uniqueMethodWithName("foo");
+    assertTrue(
+        foo.streamInstructions().anyMatch(i -> i.isInvokeSpecial() && i.getMethod() == target));
+  }
+
+  private DexMethod getTargetMethodSignature(Class<?> declaredClass, AppInfoWithLiveness appInfo) {
+    return buildMethod(
+        Reference.method(Reference.classFromClass(declaredClass), "bar", ImmutableList.of(), null),
+        appInfo.dexItemFactory());
+  }
+
+  private DexProgramClass getDexProgramClass(Class<?> clazz, AppInfoWithLiveness appInfo) {
+    return appInfo.definitionFor(buildType(clazz, appInfo.dexItemFactory())).asProgramClass();
+  }
+
+  private AppView<AppInfoWithLiveness> getAppView() throws Exception {
+    return computeAppViewWithLiveness(
+        buildClasses(getClasses()).addClassProgramData(getTransformedClasses()).build(),
+        Main.class);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), Main.class)
+        .apply(result -> checkExpectedResult(result, false));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(getClasses())
+        .addProgramClassFileData(getTransformedClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(result -> checkExpectedResult(result, true));
+  }
+
+  private void checkExpectedResult(TestRunResult<?> result, boolean isR8) {
+    result.assertSuccessWithOutput(EXPECTED);
+  }
+
+  static class A {
+    public void bar() {
+      System.out.println("A::bar");
+    }
+  }
+
+  static class B extends A {
+    // Intentionally empty.
+  }
+
+  static class C extends B {
+    public void foo() {
+      // invoke-special B.bar which resolves to private method A.bar
+      // Without nests, results in an IllegalAccessError.
+      // With nests, results in a NoSuchMethodError.
+      super.bar();
+    }
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      new C().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
index a9ea1b7..b2c8b6f 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
@@ -261,6 +261,7 @@
         && ((CfSwitch) instruction).getKind() == CfSwitch.Kind.LOOKUP;
   }
 
+  @Override
   public boolean isInvokeSpecial() {
     return instruction instanceof CfInvoke
         && ((CfInvoke) instruction).getOpcode() == Opcodes.INVOKESPECIAL;
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
index c46c123..af63ce8 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -192,6 +192,11 @@
     return instruction instanceof InvokeStatic || instruction instanceof InvokeStaticRange;
   }
 
+  @Override
+  public boolean isInvokeSpecial() {
+    return false;
+  }
+
   public boolean isInvokeSuper() {
     return instruction instanceof InvokeSuper || instruction instanceof InvokeSuperRange;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
index 9c67ece..ebe2f02 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
@@ -36,6 +36,8 @@
 
   boolean isInvokeStatic();
 
+  boolean isInvokeSpecial();
+
   DexMethod getMethod();
 
   boolean isNop();