Fix access flags for merged virtual methods

Bug: 176819913
Change-Id: Iaa15d53aa93e2c72a8552f9ab722830dff52df58
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index 98806f6..70d7cdb 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -320,6 +320,33 @@
       return self();
     }
 
+    public B setPrivate(boolean value) {
+      if (value) {
+        flags.setPrivate();
+      } else {
+        flags.unsetPrivate();
+      }
+      return self();
+    }
+
+    public B setProtected(boolean value) {
+      if (value) {
+        flags.setProtected();
+      } else {
+        flags.unsetProtected();
+      }
+      return self();
+    }
+
+    public B setPublic(boolean value) {
+      if (value) {
+        flags.setPublic();
+      } else {
+        flags.unsetPublic();
+      }
+      return self();
+    }
+
     public B setStatic() {
       flags.setStatic();
       return self();
diff --git a/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java b/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
index 61e54e9..7d6eac0 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
@@ -151,6 +151,10 @@
     set(Constants.ACC_VARARGS);
   }
 
+  public void unsetVarargs() {
+    unset(Constants.ACC_VARARGS);
+  }
+
   public boolean isNative() {
     return isSet(Constants.ACC_NATIVE);
   }
@@ -179,6 +183,10 @@
     set(Constants.ACC_STRICT);
   }
 
+  public void unsetStrict() {
+    unset(Constants.ACC_STRICT);
+  }
+
   public boolean isConstructor() {
     return isSet(Constants.ACC_CONSTRUCTOR);
   }
@@ -207,7 +215,7 @@
     set(Constants.ACC_DECLARED_SYNCHRONIZED);
   }
 
-  private void unsetDeclaredSynchronized() {
+  public void unsetDeclaredSynchronized() {
     unset(Constants.ACC_DECLARED_SYNCHRONIZED);
   }
 
@@ -222,6 +230,24 @@
       return this;
     }
 
+    public Builder setStrict(boolean value) {
+      if (value) {
+        flags.setStrict();
+      } else {
+        flags.unsetStrict();
+      }
+      return this;
+    }
+
+    public Builder setSynchronized(boolean value) {
+      if (value) {
+        flags.setSynchronized();
+      } else {
+        flags.unsetSynchronized();
+      }
+      return this;
+    }
+
     @Override
     public Builder self() {
       return this;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
index 24a1b84..e8130ed 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
@@ -116,16 +116,29 @@
   }
 
   private MethodAccessFlags getAccessFlags() {
-    // TODO(b/164998929): ensure this behaviour is correct, should probably calculate upper bound
-    MethodAccessFlags flags = methods.iterator().next().getAccessFlags().copy();
-    if (flags.isAbstract()
-        && Iterables.any(methods, method -> !method.getAccessFlags().isAbstract())) {
-      flags.unsetAbstract();
+    Iterable<MethodAccessFlags> allFlags =
+        Iterables.transform(methods, ProgramMethod::getAccessFlags);
+    MethodAccessFlags result = allFlags.iterator().next().copy();
+    assert Iterables.all(allFlags, flags -> !flags.isNative());
+    assert !result.isStrict() || Iterables.all(allFlags, MethodAccessFlags::isStrict);
+    assert !result.isSynchronized() || Iterables.all(allFlags, MethodAccessFlags::isSynchronized);
+    if (result.isAbstract() && Iterables.any(allFlags, flags -> !flags.isAbstract())) {
+      result.unsetAbstract();
     }
-    if (flags.isFinal() && Iterables.any(methods, method -> !method.getAccessFlags().isFinal())) {
-      flags.unsetFinal();
+    if (result.isBridge() && Iterables.any(allFlags, flags -> !flags.isBridge())) {
+      result.unsetBridge();
     }
-    return flags;
+    if (result.isFinal() && Iterables.any(allFlags, flags -> !flags.isFinal())) {
+      result.unsetFinal();
+    }
+    if (result.isSynthetic() && Iterables.any(allFlags, flags -> !flags.isSynthetic())) {
+      result.unsetSynthetic();
+    }
+    if (result.isVarargs() && Iterables.any(allFlags, flags -> !flags.isVarargs())) {
+      result.unsetVarargs();
+    }
+    result.unsetDeclaredSynchronized();
+    return result;
   }
 
   private DexMethod getNewMethodReference() {
@@ -181,19 +194,24 @@
       }
     }
 
+    DexEncodedMethod newMethod;
     if (representative.getHolder() == group.getTarget()) {
-      classMethodsBuilder.addVirtualMethod(representative.getDefinition());
+      newMethod = representative.getDefinition();
     } else {
       // If the method is not in the target type, move it.
       OptionalBool isLibraryMethodOverride =
           representative.getDefinition().isLibraryMethodOverride();
-      classMethodsBuilder.addVirtualMethod(
+      newMethod =
           representative
               .getDefinition()
               .toTypeSubstitutedMethod(
                   newMethodReference,
-                  builder -> builder.setIsLibraryMethodOverrideIfKnown(isLibraryMethodOverride)));
+                  builder -> builder.setIsLibraryMethodOverrideIfKnown(isLibraryMethodOverride));
     }
+
+    newMethod.getAccessFlags().unsetFinal();
+
+    classMethodsBuilder.addVirtualMethod(newMethod);
   }
 
   public void merge(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
index 5165bd9..431fdb9 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.OptionalBool;
@@ -26,17 +27,24 @@
 
   static class MethodCharacteristics {
 
+    private final MethodAccessFlags accessFlags;
     private final OptionalBool isLibraryMethodOverride;
-    private final int visibilityOrdinal;
 
     private MethodCharacteristics(DexEncodedMethod method) {
+      this.accessFlags =
+          MethodAccessFlags.builder()
+              .setPrivate(method.getAccessFlags().isPrivate())
+              .setProtected(method.getAccessFlags().isProtected())
+              .setPublic(method.getAccessFlags().isPublic())
+              .setStrict(method.getAccessFlags().isStrict())
+              .setSynchronized(method.getAccessFlags().isSynchronized())
+              .build();
       this.isLibraryMethodOverride = method.isLibraryMethodOverride();
-      this.visibilityOrdinal = method.getAccessFlags().getVisibilityOrdinal();
     }
 
     @Override
     public int hashCode() {
-      return (visibilityOrdinal << 2) | isLibraryMethodOverride.ordinal();
+      return (accessFlags.hashCode() << 2) | isLibraryMethodOverride.ordinal();
     }
 
     @Override
@@ -49,7 +57,7 @@
       }
       MethodCharacteristics characteristics = (MethodCharacteristics) obj;
       return isLibraryMethodOverride == characteristics.isLibraryMethodOverride
-          && visibilityOrdinal == characteristics.visibilityOrdinal;
+          && accessFlags.equals(characteristics.accessFlags);
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NonFinalOverrideOfFinalMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NonFinalOverrideOfFinalMethodTest.java
new file mode 100644
index 0000000..d73ffcf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NonFinalOverrideOfFinalMethodTest.java
@@ -0,0 +1,98 @@
+// 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runners.Parameterized;
+
+public class NonFinalOverrideOfFinalMethodTest extends HorizontalClassMergingTestBase {
+
+  @Parameterized.Parameters(name = "{0}, horizontalClassMerging:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.trueValues());
+  }
+
+  public NonFinalOverrideOfFinalMethodTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+
+              MethodSubject synchronizedMethodSubject = aClassSubject.uniqueMethodWithName("foo");
+              assertThat(synchronizedMethodSubject, isPresent());
+              assertFalse(synchronizedMethodSubject.isFinal());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B", "BSub");
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A().foo();
+      new B().bar();
+      new BSub().foo();
+    }
+  }
+
+  @NeverClassInline
+  public static class A {
+
+    @NeverInline
+    public final void foo() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  @NoVerticalClassMerging
+  public static class B {
+
+    @NeverInline
+    public void bar() {
+      System.out.println("B");
+    }
+  }
+
+  @NeverClassInline
+  public static class BSub extends B {
+
+    @NeverInline
+    public void foo() {
+      System.out.println("BSub");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/StrictMethodMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/StrictMethodMergingTest.java
new file mode 100644
index 0000000..762824f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/StrictMethodMergingTest.java
@@ -0,0 +1,116 @@
+// 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runners.Parameterized;
+
+public class StrictMethodMergingTest extends HorizontalClassMergingTestBase {
+
+  @Parameterized.Parameters(name = "{0}, horizontalClassMerging:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.trueValues());
+  }
+
+  public StrictMethodMergingTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging,
+            inspector ->
+                inspector.assertMergedInto(B.class, A.class).assertMergedInto(D.class, C.class))
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+
+              MethodSubject synchronizedMethodSubject =
+                  aClassSubject.uniqueMethodWithName("m$bridge");
+              assertThat(synchronizedMethodSubject, isPresent());
+              assertTrue(synchronizedMethodSubject.getAccessFlags().isStrict());
+
+              ClassSubject cClassSubject = inspector.clazz(C.class);
+              assertThat(cClassSubject, isPresent());
+
+              MethodSubject unsynchronizedMethodSubject =
+                  cClassSubject.uniqueMethodWithName("m$bridge");
+              assertThat(unsynchronizedMethodSubject, isPresent());
+              assertFalse(unsynchronizedMethodSubject.getAccessFlags().isStrict());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B", "C", "D");
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A().m();
+      new B().m();
+      new C().m();
+      new D().m();
+    }
+  }
+
+  @NeverClassInline
+  public static class A {
+
+    @NeverInline
+    public strictfp void m() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+
+    @NeverInline
+    public strictfp void m() {
+      System.out.println("B");
+    }
+  }
+
+  @NeverClassInline
+  public static class C {
+
+    @NeverInline
+    public void m() {
+      System.out.println("C");
+    }
+  }
+
+  @NeverClassInline
+  public static class D {
+
+    @NeverInline
+    public void m() {
+      System.out.println("D");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedMethodMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedMethodMergingTest.java
new file mode 100644
index 0000000..8d802c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedMethodMergingTest.java
@@ -0,0 +1,117 @@
+// 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runners.Parameterized;
+
+public class SynchronizedMethodMergingTest extends HorizontalClassMergingTestBase {
+
+  @Parameterized.Parameters(name = "{0}, horizontalClassMerging:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.trueValues());
+  }
+
+  public SynchronizedMethodMergingTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging,
+            inspector ->
+                inspector.assertMergedInto(B.class, A.class).assertMergedInto(D.class, C.class))
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+
+              MethodSubject synchronizedMethodSubject =
+                  aClassSubject.uniqueMethodWithName("m$bridge");
+              assertThat(synchronizedMethodSubject, isPresent());
+              assertTrue(synchronizedMethodSubject.isSynchronized());
+
+              ClassSubject cClassSubject = inspector.clazz(C.class);
+              assertThat(cClassSubject, isPresent());
+
+              MethodSubject unsynchronizedMethodSubject =
+                  cClassSubject.uniqueMethodWithName("m$bridge");
+              assertThat(unsynchronizedMethodSubject, isPresent());
+              assertFalse(unsynchronizedMethodSubject.isSynchronized());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B", "C", "D");
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A().m();
+      new B().m();
+      new C().m();
+      new D().m();
+    }
+  }
+
+  @NeverClassInline
+  public static class A {
+
+    @NeverInline
+    public synchronized void m() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+
+    @NeverInline
+    public synchronized void m() {
+      System.out.println("B");
+    }
+  }
+
+  @NeverClassInline
+  public static class C {
+
+    @NeverInline
+    public void m() {
+      System.out.println("C");
+    }
+  }
+
+  @NeverClassInline
+  public static class D {
+
+    @NeverInline
+    public void m() {
+      System.out.println("D");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
index f81dffd..37f26db 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -94,6 +95,11 @@
   }
 
   @Override
+  public MethodAccessFlags getAccessFlags() {
+    throw new Unreachable("Cannot get the access flags for an absent method");
+  }
+
+  @Override
   public DexEncodedMethod getMethod() {
     return null;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
index 4d5e0d3..c2ae3ab 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.naming.MemberNaming;
@@ -142,6 +143,11 @@
   }
 
   @Override
+  public MethodAccessFlags getAccessFlags() {
+    return dexMethod.getAccessFlags();
+  }
+
+  @Override
   public DexEncodedMethod getMethod() {
     return dexMethod;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
index c6a63e2..4826876 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.utils.codeinspector;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -37,6 +38,8 @@
     return null;
   }
 
+  public abstract MethodAccessFlags getAccessFlags();
+
   @Override
   public abstract MethodSignature getOriginalSignature();