Fix enum unboxing and invalid equals

Bug: b/275522360
Change-Id: I9cccb56b206a577a8ad467041d91e0c35aced3e1
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index a649af6..587beaf 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -91,6 +91,7 @@
 import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingEnumStaticFieldValuesReason;
 import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingInstanceFieldValueForEnumInstanceReason;
 import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingObjectStateForEnumInstanceReason;
+import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnboxedValueNonComparable;
 import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedInstanceFieldValueForEnumInstanceReason;
 import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedLibraryInvokeReason;
 import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
@@ -1296,6 +1297,24 @@
     return reason;
   }
 
+  private Reason comparableAsUnboxedValues(InvokeMethod invoke) {
+    assert invoke.inValues().size() == 2;
+    TypeElement type1 = invoke.getFirstArgument().getType();
+    TypeElement type2 = invoke.getLastArgument().getType();
+    DexProgramClass candidate1 = getEnumUnboxingCandidateOrNull(type1);
+    DexProgramClass candidate2 = getEnumUnboxingCandidateOrNull(type2);
+    assert candidate1 != null || candidate2 != null;
+    if (type1.isNullType() || type2.isNullType()) {
+      // Comparing an unboxed enum to null is always allowed.
+      return Reason.ELIGIBLE;
+    }
+    if (candidate1 == candidate2) {
+      // Comparing two unboxed enum values is valid only if they come from the same enum.
+      return Reason.ELIGIBLE;
+    }
+    return new UnboxedValueNonComparable(invoke.getInvokedMethod(), type1, type2);
+  }
+
   private Reason analyzeLibraryInvoke(
       InvokeMethod invoke,
       IRCode code,
@@ -1309,13 +1328,9 @@
       // TODO(b/147860220): EnumSet and EnumMap may be interesting to model.
       if (singleTargetReference == factory.enumMembers.compareTo
           || singleTargetReference == factory.enumMembers.compareToWithObject) {
-        DexProgramClass otherEnumClass =
-            getEnumUnboxingCandidateOrNull(invoke.getLastArgument().getType());
-        if (otherEnumClass == enumClass || invoke.getLastArgument().getType().isNullType()) {
-          return Reason.ELIGIBLE;
-        }
+        return comparableAsUnboxedValues(invoke);
       } else if (singleTargetReference == factory.enumMembers.equals) {
-        return Reason.ELIGIBLE;
+        return comparableAsUnboxedValues(invoke);
       } else if (singleTargetReference == factory.enumMembers.nameMethod
           || singleTargetReference == factory.enumMembers.toString) {
         assert invoke.asInvokeMethodWithReceiver().getReceiver() == enumValue;
@@ -1342,6 +1357,17 @@
         // This is a hidden null check.
         return Reason.ELIGIBLE;
       }
+      if (singleTargetReference == factory.objectMembers.toString) {
+        assert invoke.asInvokeMethodWithReceiver().getReceiver() == enumValue;
+        addRequiredNameData(enumClass);
+        return Reason.ELIGIBLE;
+      }
+      if (singleTargetReference == factory.objectMembers.hashCode) {
+        return Reason.ELIGIBLE;
+      }
+      if (singleTargetReference == factory.objectMembers.equals) {
+        return comparableAsUnboxedValues(invoke);
+      }
       return new UnsupportedLibraryInvokeReason(singleTargetReference);
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
index 3e17a0a..6054468 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.google.common.collect.ImmutableList;
 
 public abstract class Reason {
@@ -231,6 +232,43 @@
     }
   }
 
+  public static class UnboxedValueNonComparable extends Reason {
+
+    private final DexMethod invokedMethod;
+    private final TypeElement type1;
+    private final TypeElement type2;
+
+    public UnboxedValueNonComparable(
+        DexMethod invokedMethod, TypeElement type1, TypeElement type2) {
+      this.invokedMethod = invokedMethod;
+      this.type1 = type1;
+      this.type2 = type2;
+    }
+
+    @Override
+    public Object getKind() {
+      return ImmutableList.of(getClass(), invokedMethod);
+    }
+
+    private static String typeInformation(TypeElement type) {
+      if (type.isClassType()) {
+        return type.asClassType().getClassType().toSourceString();
+      }
+      return type.toString();
+    }
+
+    @Override
+    public String toString() {
+      return "NonComparableElements("
+          + invokedMethod.toSourceString()
+          + " - "
+          + typeInformation(type1)
+          + " vs "
+          + typeInformation(type2)
+          + ")";
+    }
+  }
+
   public static class UnsupportedStaticFieldReason extends Reason {
 
     private final DexField field;
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/CallToOtherEnumCompareToMethodNegativeUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/CallToOtherEnumCompareToMethodNegativeUnboxingTest.java
new file mode 100644
index 0000000..f190a34
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/CallToOtherEnumCompareToMethodNegativeUnboxingTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2023, 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.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.codeinspector.EnumUnboxingInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CallToOtherEnumCompareToMethodNegativeUnboxingTest extends EnumUnboxingTestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.isCfRuntime()
+                || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V7_0_0),
+            runResult -> runResult.assertFailureWithErrorThatThrows(ClassCastException.class),
+            runResult -> runResult.assertSuccessWithOutputLines("0"));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules(Foo.class)
+        .addEnumUnboxingInspector(EnumUnboxingInspector::assertNoEnumsUnboxed)
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.isCfRuntime()
+                || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V7_0_0),
+            runResult -> runResult.assertFailureWithErrorThatThrows(ClassCastException.class),
+            runResult -> runResult.assertSuccessWithOutputLines("0"));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Enum<?> fooEnum = Foo.A;
+      Enum<Bar> fooEnumInDisguise = (Enum<Bar>) fooEnum;
+      System.out.println(fooEnumInDisguise.compareTo(Bar.B));
+    }
+  }
+
+  enum Foo {
+    A
+  }
+
+  enum Bar {
+    B
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/InvalidEqualsEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/InvalidEqualsEnumUnboxingTest.java
new file mode 100644
index 0000000..2ed2d40
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/InvalidEqualsEnumUnboxingTest.java
@@ -0,0 +1,75 @@
+// Copyright (c) 2023, 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.TestParameters;
+import com.android.tools.r8.utils.codeinspector.EnumUnboxingInspector;
+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 InvalidEqualsEnumUnboxingTest 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 InvalidEqualsEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(EnumEquals.class)
+        .addEnumUnboxingInspector(EnumUnboxingInspector::assertNoEnumsUnboxed)
+        .enableNeverClassInliningAnnotations()
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), EnumEquals.class)
+        .assertSuccessWithOutputLines("false", "false");
+  }
+
+  static class EnumEquals {
+
+    @NeverClassInline
+    enum MyEnumEquals {
+      A,
+      B
+    }
+
+    @NeverClassInline
+    enum MyEnumEquals1 {
+      A,
+      B
+    }
+
+    @NeverClassInline
+    enum MyEnumEquals2 {
+      A,
+      B
+    }
+
+    public static void main(String[] args) {
+      Object guineaPig = new Object();
+      System.out.println(MyEnumEquals.A.equals(guineaPig));
+      System.out.println(MyEnumEquals1.A.equals(MyEnumEquals2.A));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/EnumUnboxingInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/EnumUnboxingInspector.java
index c075406..3cbb42d 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/EnumUnboxingInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/EnumUnboxingInspector.java
@@ -22,6 +22,11 @@
     this.unboxedEnums = unboxedEnums;
   }
 
+  public EnumUnboxingInspector assertNoEnumsUnboxed() {
+    assertTrue(unboxedEnums.isEmpty());
+    return this;
+  }
+
   public EnumUnboxingInspector assertUnboxed(String typeName) {
     assertTrue(
         unboxedEnums.isUnboxedEnum(