Enum unboxing: fix accessibility

Bug: 160939354
Change-Id: I2821fc2c505905709f51051f5bd8a0678f510c8d
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 019d295..cab8542 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -180,6 +180,10 @@
     obsolete = true;
   }
 
+  public CompilationState getCompilationState() {
+    return compilationState;
+  }
+
   public DexEncodedMethod getDefaultInterfaceMethodImplementation() {
     return defaultInterfaceMethodImplementation;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 3f74b74..6cbf350 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -739,7 +739,11 @@
     }
     if (enumUnboxer != null) {
       enumUnboxer.finishAnalysis();
-      enumUnboxer.unboxEnums(postMethodProcessorBuilder, executorService, feedback);
+      enumUnboxer.unboxEnums(
+          postMethodProcessorBuilder,
+          executorService,
+          feedback,
+          classStaticizer == null ? Collections.emptySet() : classStaticizer.getCandidates());
     }
     if (!options.debug) {
       new TrivialFieldAccessReprocessor(appView.withLiveness(), postMethodProcessorBuilder)
@@ -1763,6 +1767,11 @@
         || definition.getOptimizationInfo().isReachabilitySensitive()) {
       return false;
     }
+    if (appView.options().enableEnumUnboxing && method.getHolder().isEnum()) {
+      // Although the method is pinned, we compute the inlining constraint for enum unboxing,
+      // but the inliner won't be able to inline the method (marked as pinned).
+      return true;
+    }
     if (appView.appInfo().hasLiveness()
         && appView.appInfo().withLiveness().isPinned(method.getReference())) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index e98ecfa..a74b4b6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -51,6 +51,7 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.conversion.PostOptimization;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.info.FieldOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
@@ -68,6 +69,9 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -83,6 +87,7 @@
   // Map the enum candidates with their dependencies, i.e., the methods to reprocess for the given
   // enum if the optimization eventually decides to unbox it.
   private final Map<DexType, ProgramMethodSet> enumsUnboxingCandidates;
+  private final Set<DexType> enumsToUnboxWithPackageRequirement = Sets.newIdentityHashSet();
 
   private EnumUnboxingRewriter enumUnboxerRewriter;
 
@@ -328,7 +333,8 @@
   public void unboxEnums(
       PostMethodProcessor.Builder postBuilder,
       ExecutorService executorService,
-      OptimizationFeedbackDelayed feedback)
+      OptimizationFeedbackDelayed feedback,
+      Set<DexType> hostsToAvoidIfPossible)
       throws ExecutionException {
     // At this point the enumsToUnbox are no longer candidates, they will all be unboxed.
     if (enumsUnboxingCandidates.isEmpty()) {
@@ -338,7 +344,10 @@
     // Update keep info on any of the enum methods of the removed classes.
     updatePinnedItems(enumsToUnbox);
     enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumsToUnbox);
-    NestedGraphLens enumUnboxingLens = new TreeFixer(enumsToUnbox).fixupTypeReferences();
+    Map<DexType, DexType> newMethodLocation =
+        findNewMethodLocationOfUnboxableEnums(enumsToUnbox, hostsToAvoidIfPossible);
+    NestedGraphLens enumUnboxingLens =
+        new TreeFixer(enumsToUnbox).fixupTypeReferences(newMethodLocation);
     appView.setUnboxedEnums(enumUnboxerRewriter.getEnumsToUnbox());
     GraphLens previousLens = appView.graphLens();
     appView.rewriteWithLens(enumUnboxingLens);
@@ -378,6 +387,84 @@
     postBuilder.rewrittenWithLens(appView, previousLens);
   }
 
+  // Some enums may have methods which require to stay in the current package for accessibility,
+  // in this case we find another class than the enum unboxing utility class to host these methods.
+  private Map<DexType, DexType> findNewMethodLocationOfUnboxableEnums(
+      Set<DexType> enumsToUnbox, Set<DexType> hostsToAvoidIfPossible) {
+    if (enumsToUnboxWithPackageRequirement.isEmpty()) {
+      return Collections.emptyMap();
+    }
+    Map<DexType, DexType> newMethodLocationMap = new IdentityHashMap<>();
+    Map<String, DexProgramClass> packageToClassMap =
+        getPackageToClassMapExcluding(enumsToUnbox, hostsToAvoidIfPossible);
+    for (DexType toUnbox : enumsToUnboxWithPackageRequirement) {
+      DexProgramClass packageClass = packageToClassMap.get(toUnbox.getPackageDescriptor());
+      if (packageClass != null) {
+        newMethodLocationMap.put(toUnbox, packageClass.type);
+      }
+    }
+    enumsToUnboxWithPackageRequirement.clear();
+    return newMethodLocationMap;
+  }
+
+  // We are looking for another class in the same package as the unboxed enum to host the unboxed
+  // enum methods. We go through all classes, and for each package where a host is needed, we
+  // select a class.
+  private Map<String, DexProgramClass> getPackageToClassMapExcluding(
+      Set<DexType> enumsToUnbox, Set<DexType> hostsToAvoidIfPossible) {
+    HashSet<String> relevantPackages = new HashSet<>();
+    for (DexType toUnbox : enumsToUnbox) {
+      relevantPackages.add(toUnbox.getPackageDescriptor());
+    }
+
+    Map<String, DexProgramClass> packageToClassMap = new HashMap<>();
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      String packageDescriptor = clazz.type.getPackageDescriptor();
+      if (relevantPackages.contains(packageDescriptor) && !enumsToUnbox.contains(clazz.type)) {
+        DexProgramClass previousClass = packageToClassMap.get(packageDescriptor);
+        if (previousClass == null) {
+          packageToClassMap.put(packageDescriptor, clazz);
+        } else {
+          packageToClassMap.put(
+              packageDescriptor, selectHost(clazz, previousClass, hostsToAvoidIfPossible));
+        }
+      }
+    }
+
+    return packageToClassMap;
+  }
+
+  // We are trying to select a host for the enum unboxing methods, but multiple candidates are
+  // available. We need to pick one of the two classes and the result has to be deterministic.
+  // We follow the heuristics, in order:
+  //  1. don't pick a class from hostToAvoidIfPossible if possible
+  //  2. pick the class with the least number of methods
+  //  3. pick the first class name in alphabetical order for determinism.
+  private DexProgramClass selectHost(
+      DexProgramClass class1, DexProgramClass class2, Set<DexType> hostsToAvoidIfPossible) {
+    boolean avoid1 = hostsToAvoidIfPossible.contains(class1.type);
+    boolean avoid2 = hostsToAvoidIfPossible.contains(class2.type);
+    if (avoid1 && !avoid2) {
+      return class2;
+    }
+    if (avoid2 && !avoid1) {
+      return class1;
+    }
+    int size1 = class1.getMethodCollection().size();
+    int size2 = class2.getMethodCollection().size();
+    if (size1 < size2) {
+      return class1;
+    }
+    if (size2 < size1) {
+      return class2;
+    }
+    assert class1.type != class2.type;
+    if (class1.type.slowCompareTo(class2.type) < 0) {
+      return class1;
+    }
+    return class2;
+  }
+
   private void updatePinnedItems(Set<DexType> enumsToUnbox) {
     appView
         .appInfo()
@@ -394,6 +481,85 @@
   }
 
   public void finishAnalysis() {
+    analyzeInitializers();
+    analyzeAccessibility();
+    if (debugLogEnabled) {
+      reportEnumsAnalysis();
+    }
+  }
+
+  private void analyzeAccessibility() {
+    // Unboxing an enum will require to move its methods to a different class, which may impact
+    // accessibility. For a quick analysis we simply reuse the inliner analysis.
+    for (DexType toUnbox : enumsUnboxingCandidates.keySet()) {
+      DexProgramClass enumClass = appView.definitionFor(toUnbox).asProgramClass();
+      Constraint classConstraint = analyzeAccessibilityInClass(enumClass);
+      if (classConstraint == Constraint.NEVER) {
+        markEnumAsUnboxable(Reason.ACCESSIBILITY, enumClass);
+      } else if (classConstraint == Constraint.PACKAGE) {
+        enumsToUnboxWithPackageRequirement.add(toUnbox);
+      }
+    }
+  }
+
+  private Constraint analyzeAccessibilityInClass(DexProgramClass enumClass) {
+    Constraint classConstraint = Constraint.ALWAYS;
+    for (DexEncodedMethod method : enumClass.methods()) {
+      // Enum initializer are analyzed in analyzeInitializers instead.
+      if (!method.isInitializer()) {
+        Constraint methodConstraint = constraintForEnumUnboxing(method);
+        classConstraint = meet(classConstraint, methodConstraint);
+        if (classConstraint == Constraint.NEVER) {
+          return classConstraint;
+        }
+      }
+    }
+    return classConstraint;
+  }
+
+  private Constraint meet(Constraint constraint1, Constraint constraint2) {
+    if (constraint1 == Constraint.NEVER || constraint2 == Constraint.NEVER) {
+      return Constraint.NEVER;
+    }
+    if (constraint1 == Constraint.ALWAYS) {
+      return constraint2;
+    }
+    if (constraint2 == Constraint.ALWAYS) {
+      return constraint1;
+    }
+    assert constraint1 == Constraint.PACKAGE && constraint2 == Constraint.PACKAGE;
+    return Constraint.PACKAGE;
+  }
+
+  public Constraint constraintForEnumUnboxing(DexEncodedMethod method) {
+    // TODO(b/160939354): Use a UseRegistry instead of inlining constraints.
+    assert appView.definitionForProgramType(method.holder()) != null;
+    assert appView.definitionForProgramType(method.holder()).isEnum();
+    assert appView.definitionForProgramType(method.holder()).isEffectivelyFinal(appView);
+    assert appView.definitionForProgramType(method.holder()).superType
+        == appView.dexItemFactory().enumType;
+    assert !appView.definitionForProgramType(method.holder()).isInANest()
+        : "Unboxable enum is in a nest, this should not happen in cf to dex compilation, R8 needs"
+            + " to take care of nest access control when relocating unboxable enums methods";
+    switch (method.getCompilationState()) {
+      case PROCESSED_INLINING_CANDIDATE_ANY:
+        return Constraint.ALWAYS;
+      case PROCESSED_INLINING_CANDIDATE_SAME_NEST:
+        assert false;
+        // fall through
+      case PROCESSED_INLINING_CANDIDATE_SUBCLASS:
+        // There is no such thing as subclass access in this context, enums analyzed here have no
+        // subclasses, inherit directly from java.lang.Enum and are not analyzed if they call
+        // the protected methods in java.lang.Enum and java.lang.Object.
+      case PROCESSED_INLINING_CANDIDATE_SAME_CLASS:
+      case PROCESSED_INLINING_CANDIDATE_SAME_PACKAGE:
+        return Constraint.PACKAGE;
+      default:
+        return Constraint.NEVER;
+    }
+  }
+
+  private void analyzeInitializers() {
     for (DexType toUnbox : enumsUnboxingCandidates.keySet()) {
       DexProgramClass enumClass = appView.definitionForProgramType(toUnbox);
       assert enumClass != null;
@@ -417,9 +583,6 @@
         markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
       }
     }
-    if (debugLogEnabled) {
-      reportEnumsAnalysis();
-    }
   }
 
   private Reason instructionAllowEnumUnboxing(
@@ -674,6 +837,7 @@
 
   public enum Reason {
     ELIGIBLE,
+    ACCESSIBILITY,
     ANNOTATION,
     PINNED,
     DOWN_CAST,
@@ -709,7 +873,8 @@
 
   private class TreeFixer {
 
-    private final List<DexEncodedMethod> unboxedEnumsMethods = new ArrayList<>();
+    private final Map<DexType, List<DexEncodedMethod>> unboxedEnumsMethods =
+        new IdentityHashMap<>();
     private final EnumUnboxingLens.Builder lensBuilder = EnumUnboxingLens.builder();
     private final Set<DexType> enumsToUnbox;
 
@@ -717,13 +882,13 @@
       this.enumsToUnbox = enumsToUnbox;
     }
 
-    private NestedGraphLens fixupTypeReferences() {
+    private NestedGraphLens fixupTypeReferences(Map<DexType, DexType> newMethodLocation) {
       assert enumUnboxerRewriter != null;
       // Fix all methods and fields using enums to unbox.
       for (DexProgramClass clazz : appView.appInfo().classes()) {
         if (enumsToUnbox.contains(clazz.type)) {
           assert clazz.instanceFields().size() == 0;
-          // Clear the initializers and move the static methods to the utility class.
+          // Clear the initializers and move the static methods to the new location.
           Set<DexEncodedMethod> methodsToRemove = Sets.newIdentityHashSet();
           clazz
               .methods()
@@ -732,8 +897,12 @@
                     if (m.isInitializer()) {
                       clearEnumToUnboxMethod(m);
                     } else {
-                      unboxedEnumsMethods.add(
-                          fixupEncodedMethodToUtility(m, factory.enumUnboxingUtilityType));
+                      DexType newHolder =
+                          newMethodLocation.getOrDefault(
+                              clazz.type, factory.enumUnboxingUtilityType);
+                      List<DexEncodedMethod> movedMethods =
+                          unboxedEnumsMethods.computeIfAbsent(newHolder, k -> new ArrayList<>());
+                      movedMethods.add(fixupEncodedMethodToUtility(m, newHolder));
                       methodsToRemove.add(m);
                     }
                   });
@@ -754,7 +923,11 @@
       DexProgramClass utilityClass =
           appView.definitionForProgramType(factory.enumUnboxingUtilityType);
       assert utilityClass != null : "Should have been synthesized upfront";
-      utilityClass.addDirectMethods(unboxedEnumsMethods);
+      unboxedEnumsMethods.forEach(
+          (newHolderType, movedMethods) -> {
+            DexProgramClass newHolderClass = appView.definitionFor(newHolderType).asProgramClass();
+            newHolderClass.addDirectMethods(movedMethods);
+          });
       return lensBuilder.build(factory, appView.graphLens(), enumsToUnbox);
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
index 617a12e..c4466cd 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
@@ -113,6 +113,11 @@
     this.converter = converter;
   }
 
+  // This set is an approximation and can be used only for heuristics.
+  public final Set<DexType> getCandidates() {
+    return candidates.keySet();
+  }
+
   // Before doing any usage-based analysis we collect a set of classes that can be
   // candidates for staticizing. This analysis is very simple, but minimizes the
   // set of eligible classes staticizer tracks and thus time and memory it needs.
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodsAccessibilityErrorEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodsAccessibilityErrorEnumUnboxingTest.java
new file mode 100644
index 0000000..52d8688
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodsAccessibilityErrorEnumUnboxingTest.java
@@ -0,0 +1,101 @@
+// 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.enumunboxing;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.transformers.ClassTransformer;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.MethodVisitor;
+
+@RunWith(Parameterized.class)
+public class VirtualMethodsAccessibilityErrorEnumUnboxingTest extends EnumUnboxingTestBase {
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public VirtualMethodsAccessibilityErrorEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    byte[] testClassData =
+        transformer(TestClass.class)
+            .addClassTransformer(
+                new ClassTransformer() {
+                  @Override
+                  public MethodVisitor visitMethod(
+                      int access,
+                      String name,
+                      String descriptor,
+                      String signature,
+                      String[] exceptions) {
+                    if (name.equals("privateMethod")) {
+                      return super.visitMethod(
+                          ACC_STATIC | ACC_PRIVATE, name, descriptor, signature, exceptions);
+                    } else {
+                      return super.visitMethod(access, name, descriptor, signature, exceptions);
+                    }
+                  }
+                })
+            .transform();
+    testForR8(parameters.getBackend())
+        .noMinification()
+        .addProgramClasses(MyEnum.class)
+        .addProgramClassFileData(testClassData)
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .enableNeverClassInliningAnnotations()
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .allowDiagnosticInfoMessages()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatMatches(containsString("java.lang.IllegalAccessError"));
+  }
+
+  public static class TestClass {
+    // The method privateMethod will be transformed into a private method.
+    @NeverInline
+    protected static int privateMethod() {
+      return System.currentTimeMillis() > 0 ? 4 : 3;
+    }
+
+    public static void main(String[] args) {
+      System.out.println(MyEnum.A.ordinal());
+      MyEnum.print();
+    }
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    A,
+    B;
+
+    @NeverInline
+    static void print() {
+      // This should raise an accessibility error, weither the enum is unboxed or not.
+      System.out.println(TestClass.privateMethod());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodsEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodsEnumUnboxingTest.java
index 2e3466f..3a1fbab 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodsEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/VirtualMethodsEnumUnboxingTest.java
@@ -1,11 +1,8 @@
 // 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.enumunboxing;
 
-import static org.hamcrest.CoreMatchers.containsString;
-
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestCompileResult;
@@ -19,7 +16,6 @@
 
 @RunWith(Parameterized.class)
 public class VirtualMethodsEnumUnboxingTest extends EnumUnboxingTestBase {
-
   private final TestParameters parameters;
   private final boolean enumValueOptimization;
   private final EnumKeepRules enumKeepRules;
@@ -43,7 +39,6 @@
         testForR8(parameters.getBackend())
             .addInnerClasses(VirtualMethodsEnumUnboxingTest.class)
             .addKeepMainRule(classToTest)
-            .addKeepMainRule(VirtualMethodsFail.class)
             .addKeepRules(enumKeepRules.getKeepRules())
             .enableNeverClassInliningAnnotations()
             .enableInliningAnnotations()
@@ -58,13 +53,15 @@
                   assertEnumIsUnboxed(MyEnumWithCollisions.class, classToTest.getSimpleName(), m);
                   assertEnumIsUnboxed(
                       MyEnumWithPackagePrivateCall.class, classToTest.getSimpleName(), m);
+                  assertEnumIsUnboxed(
+                      MyEnumWithProtectedCall.class, classToTest.getSimpleName(), m);
+                  assertEnumIsUnboxed(
+                      MyEnumWithPackagePrivateFieldAccess.class, classToTest.getSimpleName(), m);
+                  assertEnumIsUnboxed(
+                      MyEnumWithPackagePrivateAndPrivateCall.class, classToTest.getSimpleName(), m);
                 });
     R8TestRunResult run = compile.run(parameters.getRuntime(), classToTest).assertSuccess();
     assertLines2By2Correct(run.getStdOut());
-    // TODO(b/160854837): This test should actually be successful.
-    compile
-        .run(parameters.getRuntime(), VirtualMethodsFail.class)
-        .assertFailureWithErrorThatMatches(containsString("IllegalAccessError"));
   }
 
   @SuppressWarnings("SameParameterValue")
@@ -110,7 +107,6 @@
       printPrivate();
     }
   }
-
   // Use two enums to test collision.
   @NeverClassInline
   enum MyEnum2 {
@@ -160,10 +156,30 @@
   }
 
   @NeverClassInline
-  static class PackagePrivateClass {
+  static class PackagePrivateClassWithPublicMembers {
+    @NeverInline
+    public static void print() {
+      System.out.println("print1");
+    }
+
+    public static int item = System.currentTimeMillis() > 0 ? 42 : 41;
+  }
+
+  @NeverClassInline
+  public static class PublicClassWithPackagePrivateMembers {
     @NeverInline
     static void print() {
-      System.out.println("print");
+      System.out.println("print2");
+    }
+
+    static int item = System.currentTimeMillis() > 0 ? 4 : 1;
+  }
+
+  @NeverClassInline
+  public static class PublicClassWithProtectedMembers {
+    @NeverInline
+    protected static void print() {
+      System.out.println("print3");
     }
   }
 
@@ -175,41 +191,75 @@
 
     @NeverInline
     public static void callPackagePrivate() {
-      PackagePrivateClass.print();
+      PackagePrivateClassWithPublicMembers.print();
+      System.out.println("print1");
+      PublicClassWithPackagePrivateMembers.print();
+      System.out.println("print2");
     }
   }
 
-  static class VirtualMethodsFail {
-    public static void main(String[] args) {
-      testCollisions();
-      testPackagePrivate();
+  @NeverClassInline
+  enum MyEnumWithPackagePrivateAndPrivateCall {
+    A,
+    B,
+    C;
+
+    @NeverInline
+    public static void privateMethod() {
+      System.out.println("private");
     }
 
     @NeverInline
-    private static void testPackagePrivate() {
-      System.out.println(MyEnumWithPackagePrivateCall.A.ordinal());
-      System.out.println(0);
-      MyEnumWithPackagePrivateCall.callPackagePrivate();
-      System.out.println("print");
+    public static void callPackagePrivateAndPrivate() {
+      // This method has "SAME_CLASS" as compilation state, yet,
+      // it also has a package-private call.
+      privateMethod();
+      System.out.println("private");
+      PackagePrivateClassWithPublicMembers.print();
+      System.out.println("print1");
+      PublicClassWithPackagePrivateMembers.print();
+      System.out.println("print2");
     }
+  }
+
+  @NeverClassInline
+  enum MyEnumWithProtectedCall {
+    A,
+    B,
+    C;
 
     @NeverInline
-    private static void testCollisions() {
-      System.out.println(MyEnumWithCollisions.A.get());
-      System.out.println(5);
-      System.out.println(MyEnumWithCollisions.B.get());
-      System.out.println(2);
-      System.out.println(MyEnumWithCollisions.C.get());
-      System.out.println(1);
+    public static void callProtected() {
+      PublicClassWithProtectedMembers.print();
+      System.out.println("print3");
+    }
+  }
+
+  @NeverClassInline
+  enum MyEnumWithPackagePrivateFieldAccess {
+    A,
+    B,
+    C;
+
+    @NeverInline
+    public static void accessPackagePrivate() {
+      System.out.println(PackagePrivateClassWithPublicMembers.item);
+      System.out.println("42");
+      System.out.println(PublicClassWithPackagePrivateMembers.item);
+      System.out.println("4");
     }
   }
 
   static class VirtualMethods {
-
     public static void main(String[] args) {
       testCustomMethods();
       testCustomMethods2();
       testNonPublicMethods();
+      testCollisions();
+      testPackagePrivateMethod();
+      testProtectedAccess();
+      testPackagePrivateAndPrivateMethod();
+      testPackagePrivateAccess();
     }
 
     @NeverInline
@@ -243,5 +293,43 @@
       System.out.println((MyEnum2.A.returnEnum(true).ordinal()));
       System.out.println(1);
     }
+
+    @NeverInline
+    private static void testPackagePrivateMethod() {
+      System.out.println(MyEnumWithPackagePrivateCall.A.ordinal());
+      System.out.println(0);
+      MyEnumWithPackagePrivateCall.callPackagePrivate();
+    }
+
+    @NeverInline
+    private static void testPackagePrivateAndPrivateMethod() {
+      System.out.println(MyEnumWithPackagePrivateAndPrivateCall.A.ordinal());
+      System.out.println(0);
+      MyEnumWithPackagePrivateAndPrivateCall.callPackagePrivateAndPrivate();
+    }
+
+    @NeverInline
+    private static void testPackagePrivateAccess() {
+      System.out.println(MyEnumWithPackagePrivateFieldAccess.A.ordinal());
+      System.out.println(0);
+      MyEnumWithPackagePrivateFieldAccess.accessPackagePrivate();
+    }
+
+    @NeverInline
+    private static void testProtectedAccess() {
+      System.out.println(MyEnumWithProtectedCall.A.ordinal());
+      System.out.println(0);
+      MyEnumWithProtectedCall.callProtected();
+    }
+
+    @NeverInline
+    private static void testCollisions() {
+      System.out.println(MyEnumWithCollisions.A.get());
+      System.out.println(5);
+      System.out.println(MyEnumWithCollisions.B.get());
+      System.out.println(2);
+      System.out.println(MyEnumWithCollisions.C.get());
+      System.out.println(1);
+    }
   }
 }