Remove MyEnum.values().length

- Add AssumeNonNull for array type.
- javac generated values() method for enum is now side-effect free.

Bug: 147860220
Change-Id: I206631a44145b4706ece770f939beac11fbaac9a
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index c2a890e..96eb447 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -513,7 +513,7 @@
       assert appView.dexItemFactory().verifyNoCachedTypeLatticeElements();
 
       // Collect switch maps and ordinals maps.
-      if (options.enableEnumValueOptimization) {
+      if (options.enableEnumSwitchMapRemoval) {
         appViewWithLiveness.setAppInfo(new SwitchMapCollector(appViewWithLiveness).run());
       }
       if (options.enableEnumValueOptimization || options.enableEnumUnboxing) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 00a5cc3..0f317ec 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -737,14 +737,17 @@
 
   public class ObjectMethods {
 
+    public final DexMethod clone;
     public final DexMethod getClass;
     public final DexMethod constructor;
     public final DexMethod finalize;
     public final DexMethod toString;
 
     private ObjectMethods() {
-      getClass = createMethod(objectDescriptor, getClassMethodName, classDescriptor,
-          DexString.EMPTY_ARRAY);
+      // The clone method is installed on each array, so one has to use method.match(clone).
+      clone = createMethod(objectType, createProto(objectType), cloneMethodName);
+      getClass = createMethod(objectDescriptor,
+          getClassMethodName, classDescriptor, DexString.EMPTY_ARRAY);
       constructor = createMethod(objectDescriptor,
           constructorMethodName, voidType.descriptor, DexString.EMPTY_ARRAY);
       finalize = createMethod(objectDescriptor,
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index abc0440..c28bc4a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.optimize.MemberRebindingAnalysis.isClassTypeVisibleFromContext;
+
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.cf.code.CfCheckCast;
@@ -11,12 +14,15 @@
 import com.android.tools.r8.code.MoveObjectFrom16;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.AbstractError;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 public class CheckCast extends Instruction {
 
@@ -89,6 +95,44 @@
   }
 
   @Override
+  public boolean instructionMayHaveSideEffects(AppView<?> appView, DexType context) {
+    return instructionInstanceCanThrow(appView, context).isThrowing();
+  }
+
+  @Override
+  public AbstractError instructionInstanceCanThrow(AppView<?> appView, DexType context) {
+    if (appView.options().debug || !appView.appInfo().hasLiveness()) {
+      return AbstractError.top();
+    }
+    if (type.isPrimitiveType()) {
+      return AbstractError.top();
+    }
+    DexType baseType = type.toBaseType(appView.dexItemFactory());
+    if (baseType.isClassType()) {
+      DexClass dexClass = appView.definitionFor(baseType);
+      // * NoClassDefFoundError (resolution failure).
+      if (dexClass == null || !dexClass.isResolvable(appView)) {
+        return AbstractError.specific(appView.dexItemFactory().noClassDefFoundErrorType);
+      }
+      // * IllegalAccessError (not visible from the access context).
+      if (!isClassTypeVisibleFromContext(appView, context, dexClass)) {
+        return AbstractError.specific(appView.dexItemFactory().illegalAccessErrorType);
+      }
+    }
+    AppView<? extends AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+    TypeLatticeElement castType =
+        TypeLatticeElement.fromDexType(type, definitelyNotNull(), appView);
+    if (object()
+        .getDynamicUpperBoundType(appViewWithLiveness)
+        .lessThanOrEqualUpToNullability(castType, appView)) {
+      // This is a check-cast that has to be there for bytecode correctness, but R8 has proven
+      // that this cast will never throw.
+      return AbstractError.bottom();
+    }
+    return AbstractError.top();
+  }
+
+  @Override
   public boolean instructionTypeCanThrow() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index ff88eed..7f8d5d3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -50,6 +50,11 @@
   }
 
   @Override
+  public void removeInstructionIgnoreOutValue() {
+    instructionIterator.removeInstructionIgnoreOutValue();
+  }
+
+  @Override
   public void replaceCurrentInstructionWithThrowNull(
       AppView<? extends AppInfoWithSubtyping> appView,
       IRCode code,
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index 8db09e9..5c488c4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -65,6 +65,10 @@
     return get(Opcodes.AND);
   }
 
+  public boolean mayHaveArrayLength() {
+    return get(Opcodes.ARRAY_LENGTH);
+  }
+
   public boolean mayHaveCheckCast() {
     return get(Opcodes.CHECK_CAST);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
index 05bb7c5..e0043b9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
@@ -156,6 +156,11 @@
       return true;
     }
 
+    if (getInvokedMethod().holder.isArrayType()
+        && getInvokedMethod().match(appView.dexItemFactory().objectMethods.clone)) {
+      return false;
+    }
+
     // Check if it is a call to one of library methods that are known to be side-effect free.
     Predicate<InvokeMethod> noSideEffectsPredicate =
         appView.dexItemFactory().libraryMethodsWithoutSideEffects.get(getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
index 06c5dfe..e48fb6f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
@@ -4,9 +4,12 @@
 
 package com.android.tools.r8.ir.optimize;
 
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
@@ -62,27 +65,35 @@
       ClassTypeLatticeElement dynamicLowerBoundType;
       if (current.isInvokeMethod()) {
         InvokeMethod invoke = current.asInvokeMethod();
+        DexMethod invokedMethod = invoke.getInvokedMethod();
 
-        DexType staticReturnTypeRaw = invoke.getInvokedMethod().proto.returnType;
+        DexType staticReturnTypeRaw = invokedMethod.proto.returnType;
         if (!staticReturnTypeRaw.isReferenceType()) {
           continue;
         }
 
-        DexEncodedMethod singleTarget =
-            invoke.lookupSingleTarget(appView, code.method.method.holder);
-        if (singleTarget == null) {
-          continue;
-        }
+        if (invokedMethod.holder.isArrayType()
+            && invokedMethod.match(appView.dexItemFactory().objectMethods.clone)) {
+          dynamicUpperBoundType =
+              TypeLatticeElement.fromDexType(invokedMethod.holder, definitelyNotNull(), appView);
+          dynamicLowerBoundType = null;
+        } else {
+          DexEncodedMethod singleTarget =
+              invoke.lookupSingleTarget(appView, code.method.method.holder);
+          if (singleTarget == null) {
+            continue;
+          }
 
-        MethodOptimizationInfo optimizationInfo = singleTarget.getOptimizationInfo();
-        if (optimizationInfo.returnsArgument()) {
-          // Don't insert an assume-instruction since we will replace all usages of the out-value by
-          // the corresponding argument.
-          continue;
-        }
+          MethodOptimizationInfo optimizationInfo = singleTarget.getOptimizationInfo();
+          if (optimizationInfo.returnsArgument()) {
+            // Don't insert an assume-instruction since we will replace all usages of the out-value
+            // by the corresponding argument.
+            continue;
+          }
 
-        dynamicUpperBoundType = optimizationInfo.getDynamicUpperBoundType();
-        dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType();
+          dynamicUpperBoundType = optimizationInfo.getDynamicUpperBoundType();
+          dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType();
+        }
       } else if (current.isStaticGet()) {
         StaticGet staticGet = current.asStaticGet();
         DexEncodedField encodedField = appView.appInfo().resolveField(staticGet.getField());
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
index 8c07d3d..4fa0312 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
@@ -127,7 +127,7 @@
           // Case (5), field-get instructions that are guaranteed to read a non-null value.
           FieldInstruction fieldInstruction = current.asFieldInstruction();
           DexField field = fieldInstruction.getField();
-          if (field.type.isClassType() && isNullableReferenceTypeWithUsers(outValue)) {
+          if (field.type.isReferenceType() && isNullableReferenceTypeWithUsers(outValue)) {
             DexEncodedField encodedField = appView.appInfo().resolveField(field);
             if (encodedField != null) {
               FieldOptimizationInfo optimizationInfo = encodedField.getOptimizationInfo();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index ddf697d..06f4eae 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.ConstString;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRMetadata;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.IntSwitch;
@@ -46,7 +47,9 @@
 
   @SuppressWarnings("ConstantConditions")
   public void rewriteConstantEnumMethodCalls(IRCode code) {
-    if (!code.metadata().mayHaveInvokeMethodWithReceiver()) {
+    IRMetadata metadata = code.metadata();
+    if (!metadata.mayHaveInvokeMethodWithReceiver()
+        && !(metadata.mayHaveInvokeStatic() && metadata.mayHaveArrayLength())) {
       return;
     }
 
@@ -54,65 +57,77 @@
     while (iterator.hasNext()) {
       Instruction current = iterator.next();
 
-      if (!current.isInvokeMethodWithReceiver()) {
-        continue;
-      }
-      InvokeMethodWithReceiver methodWithReceiver = current.asInvokeMethodWithReceiver();
-      DexMethod invokedMethod = methodWithReceiver.getInvokedMethod();
-      boolean isOrdinalInvoke = invokedMethod == factory.enumMethods.ordinal;
-      boolean isNameInvoke = invokedMethod == factory.enumMethods.name;
-      boolean isToStringInvoke = invokedMethod == factory.enumMethods.toString;
-      if (!isOrdinalInvoke && !isNameInvoke && !isToStringInvoke) {
-        continue;
-      }
-
-      Value receiver = methodWithReceiver.getReceiver().getAliasedValue();
-      if (receiver.isPhi()) {
-        continue;
-      }
-      Instruction definition = receiver.getDefinition();
-      if (!definition.isStaticGet()) {
-        continue;
-      }
-      DexField enumField = definition.asStaticGet().getField();
-
-      EnumValueInfoMap valueInfoMap =
-          appView.appInfo().withLiveness().getEnumValueInfoMap(enumField.type);
-      if (valueInfoMap == null) {
-        continue;
-      }
-
-      // The receiver value is identified as being from a constant enum field lookup by the fact
-      // that it is a static-get to a field whose type is the same as the enclosing class (which
-      // is known to be an enum type). An enum may still define a static field using the enum type
-      // so ensure the field is present in the ordinal map for final validation.
-      EnumValueInfo valueInfo = valueInfoMap.getEnumValueInfo(enumField);
-      if (valueInfo == null) {
-        continue;
-      }
-
-      Value outValue = methodWithReceiver.outValue();
-      if (isOrdinalInvoke) {
-        iterator.replaceCurrentInstruction(new ConstNumber(outValue, valueInfo.ordinal));
-      } else if (isNameInvoke) {
-        iterator.replaceCurrentInstruction(
-            new ConstString(outValue, enumField.name, ThrowingInfo.NO_THROW));
-      } else {
-        assert isToStringInvoke;
-        DexClass enumClazz = appView.appInfo().definitionFor(enumField.type);
-        if (!enumClazz.accessFlags.isFinal()) {
+      if (current.isInvokeMethodWithReceiver()) {
+        InvokeMethodWithReceiver methodWithReceiver = current.asInvokeMethodWithReceiver();
+        DexMethod invokedMethod = methodWithReceiver.getInvokedMethod();
+        boolean isOrdinalInvoke = invokedMethod == factory.enumMethods.ordinal;
+        boolean isNameInvoke = invokedMethod == factory.enumMethods.name;
+        boolean isToStringInvoke = invokedMethod == factory.enumMethods.toString;
+        if (!isOrdinalInvoke && !isNameInvoke && !isToStringInvoke) {
           continue;
         }
-        DexEncodedMethod singleTarget =
-            appView
-                .appInfo()
-                .resolveMethodOnClass(valueInfo.type, factory.objectMethods.toString)
-                .getSingleTarget();
-        if (singleTarget != null && singleTarget.method != factory.enumMethods.toString) {
+
+        Value receiver = methodWithReceiver.getReceiver().getAliasedValue();
+        if (receiver.isPhi()) {
           continue;
         }
-        iterator.replaceCurrentInstruction(
-            new ConstString(outValue, enumField.name, ThrowingInfo.NO_THROW));
+        Instruction definition = receiver.getDefinition();
+        if (!definition.isStaticGet()) {
+          continue;
+        }
+        DexField enumField = definition.asStaticGet().getField();
+        EnumValueInfoMap valueInfoMap =
+            appView.appInfo().withLiveness().getEnumValueInfoMap(enumField.type);
+        if (valueInfoMap == null) {
+          continue;
+        }
+
+        // The receiver value is identified as being from a constant enum field lookup by the fact
+        // that it is a static-get to a field whose type is the same as the enclosing class (which
+        // is known to be an enum type). An enum may still define a static field using the enum type
+        // so ensure the field is present in the ordinal map for final validation.
+        EnumValueInfo valueInfo = valueInfoMap.getEnumValueInfo(enumField);
+        if (valueInfo == null) {
+          continue;
+        }
+
+        Value outValue = methodWithReceiver.outValue();
+        if (isOrdinalInvoke) {
+          iterator.replaceCurrentInstruction(new ConstNumber(outValue, valueInfo.ordinal));
+        } else if (isNameInvoke) {
+          iterator.replaceCurrentInstruction(
+              new ConstString(outValue, enumField.name, ThrowingInfo.NO_THROW));
+        } else {
+          assert isToStringInvoke;
+          DexClass enumClazz = appView.appInfo().definitionFor(enumField.type);
+          if (!enumClazz.accessFlags.isFinal()) {
+            continue;
+          }
+          DexEncodedMethod singleTarget =
+              appView
+                  .appInfo()
+                  .resolveMethodOnClass(valueInfo.type, factory.objectMethods.toString)
+                  .getSingleTarget();
+          if (singleTarget != null && singleTarget.method != factory.enumMethods.toString) {
+            continue;
+          }
+          iterator.replaceCurrentInstruction(
+              new ConstString(outValue, enumField.name, ThrowingInfo.NO_THROW));
+        }
+      } else if (current.isArrayLength()) {
+        // Rewrites MyEnum.values().length to a constant int.
+        Instruction arrayDefinition = current.asArrayLength().array().definition;
+        if (arrayDefinition != null && arrayDefinition.isInvokeStatic()) {
+          DexMethod invokedMethod = arrayDefinition.asInvokeStatic().getInvokedMethod();
+          if (factory.enumMethods.isValuesMethod(
+              invokedMethod, appView.definitionForProgramType(invokedMethod.holder))) {
+            EnumValueInfoMap enumValueInfoMap =
+                appView.appInfo().withLiveness().getEnumValueInfoMap(invokedMethod.holder);
+            if (enumValueInfoMap != null) {
+              iterator.replaceCurrentInstructionWithConstInt(code, enumValueInfoMap.size());
+            }
+          }
+        }
       }
     }
     assert code.isConsistentSSA();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index b5215f6..d015712 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -96,7 +96,6 @@
 import com.android.tools.r8.utils.Pair;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.BitSet;
@@ -961,10 +960,14 @@
       // {v0}, T`.
       mayHaveSideEffects = true;
     } else {
+      mayHaveSideEffects = false;
       // Otherwise, check if there is an instruction that has side effects.
-      mayHaveSideEffects =
-          Streams.stream(code.instructions())
-              .anyMatch(instruction -> instruction.instructionMayHaveSideEffects(appView, context));
+      for (Instruction instruction : code.instructions()) {
+        if (instruction.instructionMayHaveSideEffects(appView, context)) {
+          mayHaveSideEffects = true;
+          break;
+        }
+      }
     }
     if (!mayHaveSideEffects) {
       feedback.methodMayNotHaveSideEffects(method);
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 8d04c47..173d169 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -256,6 +256,7 @@
   public boolean enableStringSwitchConversion =
       System.getProperty("com.android.tools.r8.stringSwitchConversion") != null;
   public boolean enableEnumValueOptimization = true;
+  public boolean enableEnumSwitchMapRemoval = true;
   public final OutlineOptions outline = new OutlineOptions();
   public boolean enableInitializedClassesInInstanceMethodsAnalysis = true;
   public boolean enableRedundantFieldLoadElimination = true;
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
index 399b6b0..1b56b1b 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
@@ -68,6 +68,7 @@
   void enableEnumOptions(InternalOptions options, boolean enumValueOptimization) {
     options.enableEnumUnboxing = true;
     options.enableEnumValueOptimization = enumValueOptimization;
+    options.enableEnumSwitchMapRemoval = enumValueOptimization;
     options.testing.enableEnumUnboxingDebugLogs = true;
   }
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
index 374f91d..d7eeae4 100644
--- a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
@@ -42,7 +42,10 @@
         .addProgramFiles(Paths.get(ToolHelper.EXAMPLES_KOTLIN_BUILD_DIR, "enumswitch.jar"))
         .addKeepMainRule("enumswitch.EnumSwitchKt")
         .addOptionsModification(
-            options -> options.enableEnumValueOptimization = enableSwitchMapRemoval)
+            options -> {
+              options.enableEnumValueOptimization = enableSwitchMapRemoval;
+              options.enableEnumSwitchMapRemoval = enableSwitchMapRemoval;
+            })
         .setMinApi(parameters.getRuntime())
         .noMinification()
         .compile()
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
index 2f9a1ec..78a36f2 100644
--- a/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumOptimizationTest.java
@@ -48,6 +48,7 @@
 
   private void configure(InternalOptions options) {
     options.enableEnumValueOptimization = enableOptimization;
+    options.enableEnumSwitchMapRemoval = enableOptimization;
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java
new file mode 100644
index 0000000..7a5300d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java
@@ -0,0 +1,157 @@
+// 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.rewrite.enums;
+
+import static junit.framework.TestCase.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+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 EnumValuesLengthTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EnumValuesLengthTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testValuesLengthRemoved() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepMainRule(Main.class)
+        .addInnerClasses(EnumValuesLengthTest.class)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(
+            opt -> {
+              opt.enableEnumValueOptimization = true;
+              // We need to keep the switch map to ensure kept switch maps have their
+              // values array length rewritten.
+              opt.enableEnumSwitchMapRemoval = false;
+            })
+        .compile()
+        .inspect(this::assertValuesLengthRemoved)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("0", "2", "5", "a", "D", "c", "D");
+  }
+
+  @Test
+  public void testValuesLengthSwitchMapRemoved() throws Exception {
+    // Make sure SwitchMap can still be removed with valuesLength optimization.
+    testForR8(parameters.getBackend())
+        .addKeepMainRule(Main.class)
+        .addInnerClasses(EnumValuesLengthTest.class)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(
+            opt -> {
+              opt.enableEnumValueOptimization = true;
+            })
+        .compile()
+        .inspect(this::assertSwitchMapRemoved)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("0", "2", "5", "a", "D", "c", "D");
+  }
+
+  private void assertSwitchMapRemoved(CodeInspector inspector) {
+    assertTrue(
+        inspector.allClasses().stream()
+            .noneMatch(c -> !c.getDexClass().isEnum() && !c.getFinalName().endsWith("Main")));
+  }
+
+  private void assertValuesLengthRemoved(CodeInspector inspector) {
+    for (FoundClassSubject clazz : inspector.allClasses()) {
+      clazz.forAllMethods(this::assertValuesLengthRemoved);
+    }
+  }
+
+  private void assertValuesLengthRemoved(FoundMethodSubject method) {
+    assertTrue(method.streamInstructions().noneMatch(InstructionSubject::isArrayLength));
+    assertTrue(
+        method
+            .streamInstructions()
+            .noneMatch(
+                instr ->
+                    instr.isInvokeStatic() && instr.getMethod().name.toString().equals("values")));
+  }
+
+  public static class Main {
+
+    @NeverClassInline
+    enum E0 {}
+
+    @NeverClassInline
+    enum E2 {
+      A,
+      B
+    }
+
+    @NeverClassInline
+    enum E5 {
+      A,
+      B,
+      C,
+      D,
+      E
+    }
+
+    @NeverClassInline
+    enum EUnusedValues {
+      A,
+      B,
+      C
+    }
+
+    @NeverClassInline
+    enum ESwitch {
+      A,
+      B,
+      C,
+      D
+    }
+
+    @SuppressWarnings("ResultOfMethodCallIgnored")
+    public static void main(String[] args) {
+      EUnusedValues.values();
+      System.out.println(E0.values().length);
+      System.out.println(E2.values().length);
+      System.out.println(E5.values().length);
+      System.out.println(switchOn(ESwitch.A));
+      System.out.println(switchOn(ESwitch.B));
+      System.out.println(switchOn(ESwitch.C));
+      System.out.println(switchOn(ESwitch.D));
+    }
+
+    // SwitchMaps feature an array length on values, and some of them are not removed.
+    @NeverInline
+    static char switchOn(ESwitch e) {
+      switch (e) {
+        case A:
+          return 'a';
+        case C:
+          return 'c';
+        default:
+          return 'D';
+      }
+    }
+  }
+}