Enum unboxing: Support enums with static methods

- Include support for Javac generated methods valueOf and values.
- Enable enum unboxing for enum with switches in the general case.

Bug: 150172351
Bug: 150370354
Change-Id: I3cb16e2ebda59d75c2e0d07cf2293d8a5c5cddc3
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 200b7f0..f2bf9dd 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
@@ -481,7 +481,7 @@
   private void synthesizeEnumUnboxingUtilityClass(
       Builder<?> builder, ExecutorService executorService) throws ExecutionException {
     if (enumUnboxer != null) {
-      enumUnboxer.synthesizeUtilityClass(builder, this, executorService);
+      enumUnboxer.synthesizeUtilityMethods(builder, this, executorService);
     }
   }
 
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 d878322..56e1df5 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
@@ -7,7 +7,7 @@
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClass.FieldSetter;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue.DexValueInt;
 import com.android.tools.r8.graph.DexValue.DexValueNull;
@@ -57,6 +58,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.IdentityHashMap;
@@ -152,20 +154,10 @@
         } else if (instruction.isCheckCast()) {
           analyzeCheckCast(instruction.asCheckCast(), eligibleEnums);
         } else if (instruction.isInvokeStatic()) {
-          // TODO(b/150370354): Since we temporary allow enum unboxing on enums with values and
-          // valueOf static methods only if such methods are unused, such methods cannot be
-          // called. the long term solution is to simply move called methods to a companion class,
-          // as any static helper method, and remove these checks.
           DexMethod invokedMethod = instruction.asInvokeStatic().getInvokedMethod();
           DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder);
           if (enumClass != null) {
-            if (factory.enumMethods.isValueOfMethod(invokedMethod, enumClass)) {
-              markEnumAsUnboxable(Reason.VALUE_OF_INVOKE, enumClass);
-            } else if (factory.enumMethods.isValuesMethod(invokedMethod, enumClass)) {
-              markEnumAsUnboxable(Reason.VALUES_INVOKE, enumClass);
-            } else {
-              assert false; // We do not allow any other static call in unboxing candidates.
-            }
+            eligibleEnums.add(enumClass.type);
           }
         }
       }
@@ -304,8 +296,8 @@
       return;
     }
     ImmutableSet<DexType> enumsToUnbox = ImmutableSet.copyOf(this.enumsUnboxingCandidates.keySet());
-    NestedGraphLense enumUnboxingLens = new TreeFixer(enumsToUnbox).fixupTypeReferences();
     enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumsToUnbox);
+    NestedGraphLense enumUnboxingLens = new TreeFixer(enumsToUnbox).fixupTypeReferences();
     appView.setUnboxedEnums(enumUnboxerRewriter.getEnumsToUnbox());
     if (enumUnboxingLens != null) {
       appView.setGraphLense(enumUnboxingLens);
@@ -599,12 +591,11 @@
     return Sets.newIdentityHashSet();
   }
 
-  public void synthesizeUtilityClass(
-      DexApplication.Builder<?> appBuilder, IRConverter converter, ExecutorService executorService)
+  public void synthesizeUtilityMethods(
+      Builder<?> builder, IRConverter converter, ExecutorService executorService)
       throws ExecutionException {
     if (enumUnboxerRewriter != null) {
-      enumUnboxerRewriter.synthesizeEnumUnboxingUtilityClass(
-          appBuilder, converter, executorService);
+      enumUnboxerRewriter.synthesizeEnumUnboxingUtilityMethods(builder, converter, executorService);
     }
   }
 
@@ -662,6 +653,7 @@
 
   private class TreeFixer {
 
+    private final List<DexEncodedMethod> unboxedEnumsMethods = new ArrayList<>();
     private final EnumUnboxingLens.Builder lensBuilder = EnumUnboxingLens.builder();
     private final Set<DexType> enumsToUnbox;
 
@@ -670,27 +662,26 @@
     }
 
     private NestedGraphLense fixupTypeReferences() {
+      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;
-          // TODO(b/150370354): Remove when static methods are supported.
-          if (appView.options().testing.enumUnboxingRewriteJavaCGeneratedMethod) {
-            // Clear only the initializers.
-            clazz
-                .methods()
-                .forEach(
-                    m -> {
-                      if (m.isInitializer()) {
-                        clearEnumToUnboxMethod(m);
-                      }
-                    });
-            clazz.getMethodCollection().replaceMethods(this::fixupMethod);
-          } else {
-            clazz.methods().forEach(this::clearEnumToUnboxMethod);
-          }
+          // Clear the initializers and move the static methods to the utility class.
+          clazz
+              .methods()
+              .forEach(
+                  m -> {
+                    if (m.isInitializer()) {
+                      clearEnumToUnboxMethod(m);
+                    } else {
+                      assert m.isStatic();
+                      unboxedEnumsMethods.add(
+                          fixupEncodedMethodToUtility(m, factory.enumUnboxingUtilityType));
+                    }
+                  });
         } else {
-          clazz.getMethodCollection().replaceMethods(this::fixupMethod);
+          clazz.getMethodCollection().replaceMethods(this::fixupEncodedMethod);
           fixupFields(clazz.staticFields(), clazz::setStaticField);
           fixupFields(clazz.instanceFields(), clazz::setInstanceField);
         }
@@ -698,6 +689,10 @@
       for (DexType toUnbox : enumsToUnbox) {
         lensBuilder.map(toUnbox, factory.intType);
       }
+      DexProgramClass utilityClass =
+          appView.definitionForProgramType(factory.enumUnboxingUtilityType);
+      assert utilityClass != null : "Should have been synthesized in the enqueuer";
+      utilityClass.addDirectMethods(unboxedEnumsMethods);
       return lensBuilder.build(factory, appView.graphLense());
     }
 
@@ -714,7 +709,19 @@
           appView);
     }
 
-    private DexEncodedMethod fixupMethod(DexEncodedMethod encodedMethod) {
+    private DexEncodedMethod fixupEncodedMethodToUtility(
+        DexEncodedMethod encodedMethod, DexType newHolder) {
+      DexMethod method = encodedMethod.method;
+      DexString newMethodName =
+          factory.createString(
+              enumUnboxerRewriter.compatibleName(method.holder) + "$$" + method.name.toString());
+      DexMethod newMethod = fixupMethod(method, newHolder, newMethodName);
+      lensBuilder.move(method, newMethod, encodedMethod.isStatic());
+      encodedMethod.accessFlags.promoteToPublic();
+      return encodedMethod.toTypeSubstitutedMethod(newMethod);
+    }
+
+    private DexEncodedMethod fixupEncodedMethod(DexEncodedMethod encodedMethod) {
       DexMethod newMethod = fixupMethod(encodedMethod.method);
       if (newMethod != encodedMethod.method) {
         lensBuilder.move(encodedMethod.method, newMethod, encodedMethod.isStatic());
@@ -746,7 +753,11 @@
     }
 
     private DexMethod fixupMethod(DexMethod method) {
-      return factory.createMethod(method.holder, fixupProto(method.proto), method.name);
+      return fixupMethod(method, method.holder, method.name);
+    }
+
+    private DexMethod fixupMethod(DexMethod method, DexType newHolder, DexString newMethodName) {
+      return factory.createMethod(newHolder, fixupProto(method.proto), newMethodName);
     }
 
     private DexProto fixupProto(DexProto proto) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index 5412037..a6763fc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -82,23 +82,10 @@
       enumUnboxer.reportFailure(clazz.type, Reason.MISSING_INFO_MAP);
       return false;
     }
-    // Methods values, valueOf, init, clinit are present on each enum.
-    // Methods init and clinit are required if the enum is used.
-    // Methods valueOf and values are normally kept by the commonly used/recommended enum keep rule
-    // -keepclassmembers,allowoptimization enum * {
-    //     public static **[] values();
-    //     public static ** valueOf(java.lang.String);
-    // }
-    // In general there will be 4 methods, unless the enum keep rule is not present.
-    if (clazz.directMethods().size() > 4) {
-      enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_DIRECT_METHOD);
-      return false;
-    }
+    // TODO(b/155036467): Fail lazily when an unsupported method is not only present but also used.
+    // Only Enums with default initializers and static methods can be unboxed at the moment.
     for (DexEncodedMethod directMethod : clazz.directMethods()) {
-      if (!(factory.enumMethods.isValuesMethod(directMethod.method, clazz)
-          || factory.enumMethods.isValueOfMethod(directMethod.method, clazz)
-          || isStandardEnumInitializer(directMethod)
-          || directMethod.isClassInitializer())) {
+      if (!(directMethod.isStatic() || isStandardEnumInitializer(directMethod))) {
         enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_DIRECT_METHOD);
         return false;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index 383b03b..2133252 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -45,7 +45,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -67,7 +66,6 @@
   private final Map<DexMethod, DexEncodedMethod> extraUtilityMethods = new ConcurrentHashMap<>();
   private final Map<DexField, DexEncodedField> extraUtilityFields = new ConcurrentHashMap<>();
 
-  private final DexType utilityClassType;
   private final DexMethod ordinalUtilityMethod;
   private final DexMethod valuesUtilityMethod;
 
@@ -83,15 +81,14 @@
     }
     this.enumsToUnbox = builder.build();
 
-    this.utilityClassType = factory.enumUnboxingUtilityType;
     this.ordinalUtilityMethod =
         factory.createMethod(
-            utilityClassType,
+            factory.enumUnboxingUtilityType,
             factory.createProto(factory.intType, factory.intType),
             ENUM_UNBOXING_UTILITY_ORDINAL);
     this.valuesUtilityMethod =
         factory.createMethod(
-            utilityClassType,
+            factory.enumUnboxingUtilityType,
             factory.createProto(factory.intArrayType, factory.intType),
             ENUM_UNBOXING_UTILITY_VALUES);
   }
@@ -222,13 +219,13 @@
     return enumsToUnbox.containsEnum(type.asClassType().getClassType());
   }
 
-  private String compatibleName(DexType type) {
+  public String compatibleName(DexType type) {
     return type.toSourceString().replace('.', '$');
   }
 
   private DexField createValuesField(DexType type) {
     return factory.createField(
-        utilityClassType,
+        factory.enumUnboxingUtilityType,
         factory.intArrayType,
         factory.enumValuesFieldName + "$field$" + compatibleName(type));
   }
@@ -244,7 +241,7 @@
 
   private DexMethod createValuesMethod(DexType type) {
     return factory.createMethod(
-        utilityClassType,
+        factory.enumUnboxingUtilityType,
         factory.createProto(factory.intArrayType),
         factory.enumValuesFieldName + "$method$" + compatibleName(type));
   }
@@ -253,7 +250,11 @@
       DexMethod method, DexField fieldValues, int numEnumInstances) {
     CfCode cfCode =
         new EnumUnboxingCfCodeProvider.EnumUnboxingValuesCfCodeProvider(
-                appView, utilityClassType, fieldValues, numEnumInstances, valuesUtilityMethod)
+                appView,
+                factory.enumUnboxingUtilityType,
+                fieldValues,
+                numEnumInstances,
+                valuesUtilityMethod)
             .generateCfCode();
     return synthesizeUtilityMethod(cfCode, method, true);
   }
@@ -262,7 +263,7 @@
     assert enumsToUnbox.containsEnum(type);
     DexMethod valueOf =
         factory.createMethod(
-            utilityClassType,
+            factory.enumUnboxingUtilityType,
             factory.createProto(factory.intType, factory.stringType),
             "valueOf" + compatibleName(type));
     extraUtilityMethods.computeIfAbsent(valueOf, m -> synthesizeValueOfUtilityMethod(m, type));
@@ -283,9 +284,8 @@
         && enumsToUnbox.containsEnum(baseType.asClassType().getClassType());
   }
 
-  // TODO(b/150172351): Synthesize the utility class upfront in the enqueuer.
-  void synthesizeEnumUnboxingUtilityClass(
-      DexApplication.Builder<?> appBuilder, IRConverter converter, ExecutorService executorService)
+  void synthesizeEnumUnboxingUtilityMethods(
+      Builder<?> builder, IRConverter converter, ExecutorService executorService)
       throws ExecutionException {
     // Synthesize a class which holds various utility methods that may be called from the IR
     // rewriting. If any of these methods are not used, they will be removed by the Enqueuer.
@@ -302,38 +302,50 @@
     if (requiredMethods.isEmpty()) {
       return;
     }
-    DexEncodedField[] fields = extraUtilityFields.values().toArray(DexEncodedField.EMPTY_ARRAY);
-    Arrays.sort(fields, (f1, f2) -> f1.field.name.slowCompareTo(f2.field.name));
+    List<DexEncodedField> fields = new ArrayList<>(extraUtilityFields.values());
+    fields.sort((f1, f2) -> f1.field.name.slowCompareTo(f2.field.name));
     DexProgramClass utilityClass =
-        new DexProgramClass(
-            utilityClassType,
-            null,
-            new SynthesizedOrigin("EnumUnboxing ", getClass()),
-            ClassAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC),
-            factory.objectType,
-            DexTypeList.empty(),
-            factory.createString("enumunboxing"),
-            null,
-            Collections.emptyList(),
-            null,
-            Collections.emptyList(),
-            DexAnnotationSet.empty(),
-            fields,
-            DexEncodedField.EMPTY_ARRAY,
-            // All synthesized methods are static in this case.
-            requiredMethods.toArray(DexEncodedMethod.EMPTY_ARRAY),
-            DexEncodedMethod.EMPTY_ARRAY,
-            factory.getSkipNameValidationForTesting(),
-            DexProgramClass::checksumFromType);
-    appBuilder.addSynthesizedClass(utilityClass, utilityClassInMainDexList());
-    appView.appInfo().addSynthesizedClass(utilityClass);
-    converter.optimizeSynthesizedClass(utilityClass, executorService);
+        appView.definitionForProgramType(factory.enumUnboxingUtilityType);
+    assert utilityClass != null : "Should have been synthesized in the enqueuer.";
+    utilityClass.appendStaticFields(fields);
+    utilityClass.addDirectMethods(requiredMethods);
+    assert requiredMethods.stream().allMatch(DexEncodedMethod::isPublic);
+    if (utilityClassInMainDexList()) {
+      builder.addToMainDexList(Collections.singletonList(utilityClass.type));
+    }
+    converter.processMethodsConcurrently(requiredMethods, executorService);
+  }
+
+  public static DexProgramClass synthesizeEmptyEnumUnboxingUtilityClass(AppView<?> appView) {
+    DexItemFactory factory = appView.dexItemFactory();
+    return new DexProgramClass(
+        factory.enumUnboxingUtilityType,
+        null,
+        new SynthesizedOrigin("EnumUnboxing ", EnumUnboxingRewriter.class),
+        ClassAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC),
+        factory.objectType,
+        DexTypeList.empty(),
+        factory.createString("enumunboxing"),
+        null,
+        Collections.emptyList(),
+        null,
+        Collections.emptyList(),
+        DexAnnotationSet.empty(),
+        DexEncodedField.EMPTY_ARRAY,
+        DexEncodedField.EMPTY_ARRAY,
+        DexEncodedMethod.EMPTY_ARRAY,
+        DexEncodedMethod.EMPTY_ARRAY,
+        factory.getSkipNameValidationForTesting(),
+        DexProgramClass::checksumFromType);
   }
 
   private DexEncodedMethod synthesizeValueOfUtilityMethod(DexMethod method, DexType enumType) {
     CfCode cfCode =
         new EnumUnboxingCfCodeProvider.EnumUnboxingValueOfCfCodeProvider(
-                appView, utilityClassType, enumType, enumsToUnbox.getEnumValueInfoMap(enumType))
+                appView,
+                factory.enumUnboxingUtilityType,
+                enumType,
+                enumsToUnbox.getEnumValueInfoMap(enumType))
             .generateCfCode();
     return new DexEncodedMethod(
         method,
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 5540e34..f1fcd4b 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -84,6 +84,7 @@
 import com.android.tools.r8.ir.desugar.LambdaClass;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import com.android.tools.r8.ir.optimize.enums.EnumUnboxingRewriter;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.DelayedRootSetActionItem.InterfaceMethodSyntheticBridgeAction;
@@ -2632,6 +2633,7 @@
         new IdentityHashMap<>();
 
     Map<DexMethod, ProgramMethod> liveMethods = new IdentityHashMap<>();
+    Set<DexProgramClass> liveTypes = Sets.newIdentityHashSet();
 
     Map<DexType, DexClasspathClass> syntheticClasspathClasses = new IdentityHashMap<>();
 
@@ -2642,7 +2644,8 @@
     Set<DexType> mainDexTypes = Sets.newIdentityHashSet();
 
     boolean isEmpty() {
-      boolean empty = syntheticInstantiations.isEmpty() && liveMethods.isEmpty();
+      boolean empty =
+          syntheticInstantiations.isEmpty() && liveMethods.isEmpty() && liveTypes.isEmpty();
       assert !empty || (pinnedMethods.isEmpty() && mainDexTypes.isEmpty());
       return empty;
     }
@@ -2656,6 +2659,14 @@
       }
     }
 
+    void addLiveType(DexProgramClass clazz, boolean isMainDexClass) {
+      assert !liveTypes.contains(clazz);
+      liveTypes.add(clazz);
+      if (isMainDexClass) {
+        mainDexTypes.add(clazz.type);
+      }
+    }
+
     void addClasspathClass(DexClasspathClass clazz) {
       DexClasspathClass old = syntheticClasspathClasses.put(clazz.type, clazz);
       assert old == null;
@@ -2678,6 +2689,9 @@
           syntheticInstantiations.values()) {
         appBuilder.addProgramClass(clazzAndContext.getFirst());
       }
+      for (DexProgramClass liveClass : liveTypes) {
+        appBuilder.addProgramClass(liveClass);
+      }
       appBuilder.addClasspathClasses(syntheticClasspathClasses.values());
       appBuilder.addToMainDexList(mainDexTypes);
     }
@@ -2697,6 +2711,12 @@
             InstantiationReason.SYNTHESIZED_CLASS,
             fakeReason);
       }
+      for (DexProgramClass liveType : liveTypes) {
+        // TODO(b/155373435): Replace with
+        //  enqueuer.workList.enqueueMarkTypeLiveAction(liveType, fakeReason);
+        //  Introduce a new class MarkTypeLiveAction as well as the enqueueMarkTypeLiveAction().
+        enqueuer.markTypeAsLive(liveType, fakeReason);
+      }
       for (ProgramMethod liveMethod : liveMethods.values()) {
         assert !enqueuer.targetedMethods.contains(liveMethod.getMethod());
         DexProgramClass holder = liveMethod.getHolder();
@@ -2718,6 +2738,7 @@
     synthesizeInterfaceMethodBridges(additions);
     synthesizeLambdas(additions);
     synthesizeLibraryConversionWrappers(additions);
+    synthesizeEnumUnboxingUtilityClass(additions);
     if (additions.isEmpty()) {
       return;
     }
@@ -2735,6 +2756,16 @@
     additions.enqueueWorkItems(this);
   }
 
+  private void synthesizeEnumUnboxingUtilityClass(SyntheticAdditions additions) {
+    if (appView.options().enableEnumUnboxing
+        && appView.definitionFor(appView.dexItemFactory().enumUnboxingUtilityType) == null) {
+      DexProgramClass utilityClass =
+          EnumUnboxingRewriter.synthesizeEmptyEnumUnboxingUtilityClass(appView);
+      // The flag isMainDexClass may be set later, the compiler does not know at this point.
+      additions.addLiveType(utilityClass, false);
+    }
+  }
+
   private void synthesizeInterfaceMethodBridges(SyntheticAdditions additions) {
     for (ProgramMethod bridge : syntheticInterfaceMethodBridges.values()) {
       DexProgramClass holder = bridge.getHolder();
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/StaticMethodsEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/StaticMethodsEnumUnboxingTest.java
new file mode 100644
index 0000000..ca34533
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/StaticMethodsEnumUnboxingTest.java
@@ -0,0 +1,151 @@
+// 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 com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class StaticMethodsEnumUnboxingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final KeepRule enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public StaticMethodsEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, KeepRule enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<?> classToTest = StaticMethods.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(StaticMethodsEnumUnboxingTest.class)
+            .addKeepMainRule(classToTest)
+            .addKeepRules(enumKeepRules.getKeepRule())
+            .enableNeverClassInliningAnnotations()
+            .enableInliningAnnotations()
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m -> {
+                  assertEnumIsUnboxed(MyEnum.class, classToTest.getSimpleName(), m);
+                  assertEnumIsUnboxed(MyEnum2.class, classToTest.getSimpleName(), m);
+                })
+            .run(parameters.getRuntime(), classToTest)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  @SuppressWarnings("SameParameterValue")
+  @NeverClassInline
+  enum MyEnum {
+    A,
+    B,
+    C;
+
+    @NeverInline
+    public static void print(Object o) {
+      System.out.println(o);
+    }
+
+    @NeverInline
+    public static void printEnum(MyEnum e) {
+      System.out.println(e.ordinal());
+    }
+
+    @NeverInline
+    protected static void printProtected() {
+      System.out.println("protected");
+    }
+
+    @NeverInline
+    static void printPackagePrivate() {
+      System.out.println("package-private");
+    }
+
+    @NeverInline
+    private static void printPrivate() {
+      System.out.println("private");
+    }
+
+    @NeverInline
+    public static void callPrivate() {
+      System.out.print("call: ");
+      printPrivate();
+    }
+  }
+
+  // Use two enums to test collision between values and valueOf.
+  enum MyEnum2 {
+    A,
+    B,
+    C;
+  }
+
+  static class StaticMethods {
+
+    public static void main(String[] args) {
+      testCustomMethods();
+      testNonPublicMethods();
+      testGeneratedMethods();
+      testGeneratedMethods2();
+    }
+
+    @NeverInline
+    private static void testNonPublicMethods() {
+      MyEnum.printPrivate();
+      System.out.println("private");
+      MyEnum.printPackagePrivate();
+      System.out.println("package-private");
+      MyEnum.printProtected();
+      System.out.println("protected");
+      MyEnum.callPrivate();
+      System.out.println("call: private");
+    }
+
+    @NeverInline
+    private static void testCustomMethods() {
+      MyEnum.print("print");
+      System.out.println("print");
+      MyEnum.printEnum(MyEnum.A);
+      System.out.println(0);
+    }
+
+    @NeverInline
+    private static void testGeneratedMethods() {
+      System.out.println(MyEnum.valueOf("C").ordinal());
+      System.out.println(2);
+      System.out.println(MyEnum.values()[0].ordinal());
+      System.out.println(0);
+    }
+
+    @NeverInline
+    private static void testGeneratedMethods2() {
+      System.out.println(MyEnum2.valueOf("C").ordinal());
+      System.out.println(2);
+      System.out.println(MyEnum2.values()[0].ordinal());
+      System.out.println(0);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingTest.java
index 3ccdd37..dc2e217 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingTest.java
@@ -50,15 +50,7 @@
             .setMinApi(parameters.getApiLevel())
             .compile()
             .inspectDiagnosticMessages(
-                m -> {
-                  // TODO(b/150370354): We need to allow static helper method to re-enable unboxing
-                  // with switches.
-                  if (enumValueOptimization && enumKeepRules.getKeepRule().equals("")) {
-                    assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m);
-                  } else {
-                    assertEnumIsBoxed(ENUM_CLASS, classToTest.getSimpleName(), m);
-                  }
-                })
+                m -> assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m))
             .run(parameters.getRuntime(), classToTest)
             .assertSuccess();
     assertLines2By2Correct(run.getStdOut());