Mark effectively final methods as final

Change-Id: I7234e9c6ae11b8309482435db36c9c9df7293406
Fixes: 66369976
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
index 29a5e40..6e90810 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
@@ -28,10 +28,16 @@
 
   static class VirtualRootMethod {
 
+    private final VirtualRootMethod parent;
     private final ProgramMethod root;
     private final ProgramMethodSet overrides = ProgramMethodSet.create();
 
     VirtualRootMethod(ProgramMethod root) {
+      this(root, null);
+    }
+
+    VirtualRootMethod(ProgramMethod root, VirtualRootMethod parent) {
+      this.parent = parent;
       this.root = root;
     }
 
@@ -39,6 +45,17 @@
       assert override != root;
       assert override.getMethodSignature().equals(root.getMethodSignature());
       overrides.add(override);
+      if (hasParent()) {
+        getParent().addOverride(override);
+      }
+    }
+
+    boolean hasParent() {
+      return parent != null;
+    }
+
+    VirtualRootMethod getParent() {
+      return parent;
     }
 
     ProgramMethod getRoot() {
@@ -116,13 +133,14 @@
               virtualRootMethodsPerClass.get(superclass);
           virtualRootMethodsForSuperclass.forEach(
               (signature, info) ->
-                  virtualRootMethodsForClass.computeIfAbsent(signature, ignoreKey(() -> info)));
+                  virtualRootMethodsForClass.computeIfAbsent(
+                      signature, ignoreKey(() -> new VirtualRootMethod(info.getRoot(), info))));
         });
     clazz.forEachProgramVirtualMethod(
         method -> {
           DexMethodSignature signature = method.getMethodSignature();
           if (virtualRootMethodsForClass.containsKey(signature)) {
-            virtualRootMethodsForClass.get(signature).addOverride(method);
+            virtualRootMethodsForClass.get(signature).getParent().addOverride(method);
           } else {
             virtualRootMethodsForClass.put(signature, new VirtualRootMethod(method));
           }
@@ -139,6 +157,12 @@
         rootCandidate -> {
           VirtualRootMethod virtualRootMethod =
               virtualRootMethodsForClass.remove(rootCandidate.getMethodSignature());
+          if (!clazz.isInterface()
+              && !rootCandidate.getAccessFlags().isAbstract()
+              && !virtualRootMethod.hasOverrides()
+              && appView.getKeepInfo(rootCandidate).isOptimizationAllowed(appView.options())) {
+            rootCandidate.getAccessFlags().promoteToFinal();
+          }
           if (!rootCandidate.isStructurallyEqualTo(virtualRootMethod.getRoot())) {
             return;
           }
diff --git a/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeKeptMethodTest.java b/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeKeptMethodTest.java
new file mode 100644
index 0000000..84ee636
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeKeptMethodTest.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2021, 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.optimize.finalize;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isFinal;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.AllOf.allOf;
+
+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.ClassSubject;
+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 FinalizeKeptMethodTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(Main.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(
+                  aClassSubject.uniqueMethodWithName("m"), allOf(isPresent(), not(isFinal())));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Main.m()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new Main().m();
+    }
+
+    // @Keep
+    void m() {
+      System.out.println("Main.m()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualMethodTest.java b/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualMethodTest.java
new file mode 100644
index 0000000..b8f5492
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualMethodTest.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2021, 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.optimize.finalize;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isFinal;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.AllOf.allOf;
+
+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.ClassSubject;
+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 FinalizeVirtualMethodTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject.uniqueMethodWithName("m"), allOf(isPresent(), isFinal()));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A.m()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().m();
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    // Should be made final.
+    @NeverInline
+    public void m() {
+      System.out.println("A.m()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualMethodWithSiblingTest.java b/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualMethodWithSiblingTest.java
new file mode 100644
index 0000000..d134ecb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualMethodWithSiblingTest.java
@@ -0,0 +1,100 @@
+// Copyright (c) 2021, 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.optimize.finalize;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isFinal;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.AllOf.allOf;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoVerticalClassMerging;
+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.ClassSubject;
+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 FinalizeVirtualMethodWithSiblingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject iClassSubject = inspector.clazz(I.class);
+              assertThat(iClassSubject, isPresent());
+              assertThat(
+                  iClassSubject.uniqueMethodWithName("m"), allOf(isPresent(), not(isFinal())));
+
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject.uniqueMethodWithName("m"), allOf(isPresent(), isFinal()));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A.m()", "C.m()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      test(new B());
+      test(new C());
+    }
+
+    static void test(I i) {
+      i.m();
+    }
+  }
+
+  interface I {
+
+    void m();
+  }
+
+  @NoVerticalClassMerging
+  static class A {
+
+    // Should be made final.
+    @NeverInline
+    public void m() {
+      System.out.println("A.m()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class B extends A implements I {}
+
+  // To prevent the call i.m() from being devirtualized.
+  @NoHorizontalClassMerging
+  static class C implements I {
+    @Override
+    public void m() {
+      System.out.println("C.m()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualOverrideTest.java b/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualOverrideTest.java
new file mode 100644
index 0000000..dd316ac
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/finalize/FinalizeVirtualOverrideTest.java
@@ -0,0 +1,104 @@
+// Copyright (c) 2021, 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.optimize.finalize;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isFinal;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.AllOf.allOf;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+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.ClassSubject;
+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 FinalizeVirtualOverrideTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(
+                  aClassSubject.uniqueMethodWithName("m"), allOf(isPresent(), not(isFinal())));
+
+              ClassSubject bClassSubject = inspector.clazz(B.class);
+              assertThat(bClassSubject, isPresent());
+              assertThat(
+                  bClassSubject.uniqueMethodWithName("m"), allOf(isPresent(), not(isFinal())));
+
+              ClassSubject cClassSubject = inspector.clazz(C.class);
+              assertThat(cClassSubject, isPresent());
+              assertThat(cClassSubject.uniqueMethodWithName("m"), allOf(isPresent(), isFinal()));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A.m()", "B.m()", "C.m()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().m();
+      new B().m();
+      new C().m();
+    }
+  }
+
+  @NeverClassInline
+  @NoVerticalClassMerging
+  static class A {
+
+    @NeverInline
+    public void m() {
+      System.out.println("A.m()");
+    }
+  }
+
+  @NeverClassInline
+  static class B extends A {
+
+    @NeverInline
+    public void m() {
+      System.out.println("B.m()");
+    }
+  }
+
+  @NeverClassInline
+  static class C extends B {
+
+    // Should be made final.
+    @NeverInline
+    public void m() {
+      System.out.println("C.m()");
+    }
+  }
+}