Mark fields as final with -allowaccessmodification

Change-Id: I8f936ff0936a88bd79b903f1af89234f8e22a84b
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index ab6b1a0..c75fbf2 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -67,7 +67,7 @@
 import com.android.tools.r8.naming.ProguardMapMinifier;
 import com.android.tools.r8.naming.RecordRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
-import com.android.tools.r8.optimize.ClassAndMemberPublicizer;
+import com.android.tools.r8.optimize.AccessModifier;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLensFactory;
 import com.android.tools.r8.optimize.VisibilityBridgeRemover;
@@ -462,7 +462,7 @@
       if (options.getProguardConfiguration().isAccessModificationAllowed()) {
         SubtypingInfo subtypingInfo = appViewWithLiveness.appInfo().computeSubtypingInfo();
         GraphLens publicizedLens =
-            ClassAndMemberPublicizer.run(
+            AccessModifier.run(
                 executorService,
                 timing,
                 appViewWithLiveness.appInfo().app(),
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 51a4a4a..b785927 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -447,6 +447,10 @@
     return isInstanceInitializer() || isClassInitializer();
   }
 
+  public boolean isInitializer(boolean isStatic) {
+    return isStatic ? isClassInitializer() : isInstanceInitializer();
+  }
+
   public boolean isInstanceInitializer() {
     checkIfObsolete();
     return accessFlags.isConstructor() && !accessFlags.isStatic();
diff --git a/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java b/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
rename to src/main/java/com/android/tools/r8/optimize/AccessModifier.java
index ad17739..7661768 100644
--- a/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
@@ -10,11 +10,12 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
-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.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.FieldAccessInfo;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.MethodAccessFlags;
@@ -26,7 +27,8 @@
 import com.android.tools.r8.ir.optimize.MethodPoolCollection;
 import com.android.tools.r8.optimize.PublicizerLens.PublicizedLensBuilder;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.KeepInfoCollection;
+import com.android.tools.r8.shaking.KeepFieldInfo;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.Timing;
@@ -36,23 +38,23 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
-public final class ClassAndMemberPublicizer {
+public final class AccessModifier {
 
   private final DexApplication application;
   private final AppView<AppInfoWithLiveness> appView;
-  private final KeepInfoCollection keepInfo;
+  private final InternalOptions options;
   private final SubtypingInfo subtypingInfo;
   private final MethodPoolCollection methodPoolCollection;
 
   private final PublicizedLensBuilder lensBuilder = PublicizerLens.createBuilder();
 
-  private ClassAndMemberPublicizer(
+  private AccessModifier(
       DexApplication application,
       AppView<AppInfoWithLiveness> appView,
       SubtypingInfo subtypingInfo) {
     this.application = application;
     this.appView = appView;
-    this.keepInfo = appView.appInfo().getKeepInfo();
+    this.options = appView.options();
     this.subtypingInfo = subtypingInfo;
     this.methodPoolCollection =
         // We will add private instance methods when we promote them.
@@ -73,8 +75,7 @@
       AppView<AppInfoWithLiveness> appView,
       SubtypingInfo subtypingInfo)
       throws ExecutionException {
-    return new ClassAndMemberPublicizer(application, appView, subtypingInfo)
-        .run(executorService, timing);
+    return new AccessModifier(application, appView, subtypingInfo).run(executorService, timing);
   }
 
   private GraphLens run(ExecutorService executorService, Timing timing) throws ExecutionException {
@@ -83,8 +84,8 @@
 
     // Phase 2: Visit classes and promote class/member to public if possible.
     timing.begin("Phase 2: promoteToPublic");
-    appView.appInfo().forEachReachableInterface(clazz -> publicizeType(clazz.getType()));
-    publicizeType(appView.dexItemFactory().objectType);
+    appView.appInfo().forEachReachableInterface(clazz -> processType(clazz.getType()));
+    processType(appView.dexItemFactory().objectType);
     timing.end();
 
     return lensBuilder.build(appView);
@@ -94,21 +95,21 @@
     definition.getAccessFlags().promoteToPublic();
   }
 
-  private void publicizeType(DexType type) {
+  private void processType(DexType type) {
     DexProgramClass clazz = asProgramClassOrNull(application.definitionFor(type));
     if (clazz != null) {
-      publicizeClass(clazz);
+      processClass(clazz);
     }
-    subtypingInfo.forAllImmediateExtendsSubtypes(type, this::publicizeType);
+    subtypingInfo.forAllImmediateExtendsSubtypes(type, this::processType);
   }
 
-  private void publicizeClass(DexProgramClass clazz) {
+  private void processClass(DexProgramClass clazz) {
     if (appView.appInfo().isAccessModificationAllowed(clazz)) {
       doPublicize(clazz);
     }
 
     // Publicize fields.
-    clazz.forEachProgramField(this::publicizeField);
+    clazz.forEachProgramField(this::processField);
 
     // Publicize methods.
     Set<DexEncodedMethod> privateInstanceMethods = new LinkedHashSet<>();
@@ -132,19 +133,43 @@
     }
   }
 
+  private void processField(ProgramField field) {
+    finalizeField(field);
+    publicizeField(field);
+  }
+
+  private void finalizeField(ProgramField field) {
+    FieldAccessFlags flags = field.getAccessFlags();
+    FieldAccessInfo accessInfo =
+        appView.appInfo().getFieldAccessInfoCollection().get(field.getReference());
+    KeepFieldInfo keepInfo = appView.getKeepInfo(field);
+    if (keepInfo.isAccessModificationAllowed(options)
+        && !keepInfo.isPinned(options)
+        && !accessInfo.hasReflectiveWrite()
+        && !accessInfo.isWrittenFromMethodHandle()
+        && accessInfo.isWrittenOnlyInMethodSatisfying(
+            method ->
+                method.getDefinition().isInitializer(flags.isStatic())
+                    && method.getHolder() == field.getHolder())
+        && !flags.isFinal()
+        && !flags.isVolatile()) {
+      flags.promoteToFinal();
+    }
+  }
+
   private void publicizeField(ProgramField field) {
-    DexEncodedField definition = field.getDefinition();
-    if (definition.isPublic()) {
+    FieldAccessFlags flags = field.getAccessFlags();
+    if (flags.isPublic()) {
       return;
     }
     if (!appView.appInfo().isAccessModificationAllowed(field)) {
       // TODO(b/131130038): Also do not publicize package-private and protected fields that
       //  are kept.
-      if (definition.isPrivate()) {
+      if (flags.isPrivate()) {
         return;
       }
     }
-    doPublicize(field);
+    flags.promoteToPublic();
   }
 
   private boolean publicizeMethod(ProgramMethod method) {
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java
new file mode 100644
index 0000000..b1c5fc8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/EffectiveFinalFieldMarkedFinalTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2022, 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.accessrelaxation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isFinal;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.util.List;
+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 EffectiveFinalFieldMarkedFinalTest extends TestBase {
+
+  @Parameter(0)
+  public boolean allowAccessModification;
+
+  @Parameter(1)
+  public TestParameters parameters;
+
+  @Parameters(name = "{1}, allowaccessmodification: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .allowAccessModification(allowAccessModification)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+              assertThat(
+                  mainClassSubject.uniqueFieldWithName("instanceField"),
+                  allOf(isPresent(), onlyIf(allowAccessModification, isFinal())));
+              assertThat(
+                  mainClassSubject.uniqueFieldWithName("staticField"),
+                  allOf(isPresent(), onlyIf(allowAccessModification, isFinal())));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  static class Main {
+
+    static String staticField = System.currentTimeMillis() > 0 ? "Hello" : null;
+
+    String instanceField = System.currentTimeMillis() > 0 ? ", world!" : null;
+
+    public static void main(String[] args) {
+      System.out.print(staticField);
+      System.out.println(new Main().instanceField);
+    }
+  }
+}