[ddc] Adding support for static setters of const fields

Fixes #48717

Change-Id: I45145bc992bb129d54962b1ed9cfebbdbac41f92
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/239730
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Commit-Queue: Mark Zhou <markzipan@google.com>
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index 0f4ece3..7a7f204 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -748,7 +748,7 @@
     }
 
     body = [classDef];
-    _emitStaticFields(c, body);
+    _emitStaticFieldsAndAccessors(c, body);
     if (finishGenericTypeTest != null) body.add(finishGenericTypeTest);
     for (var peer in jsPeerNames) {
       _registerExtensionType(c, peer, body);
@@ -1323,12 +1323,16 @@
 
   /// Emits static fields for a class, and initialize them eagerly if possible,
   /// otherwise define them as lazy properties.
-  void _emitStaticFields(Class c, List<js_ast.Statement> body) {
+  void _emitStaticFieldsAndAccessors(Class c, List<js_ast.Statement> body) {
     var fields = c.fields
         .where((f) => f.isStatic && !isRedirectingFactoryField(f))
         .toList();
+    var fieldNames = Set.from(fields.map((f) => f.name));
+    var staticSetters = c.procedures.where(
+        (p) => p.isStatic && p.isAccessor && fieldNames.contains(p.name));
+    var members = [...fields, ...staticSetters];
     if (fields.isNotEmpty) {
-      body.add(_emitLazyFields(_emitTopLevelName(c), fields,
+      body.add(_emitLazyMembers(_emitTopLevelName(c), members,
           (n) => _emitStaticMemberName(n.name.text)));
     }
   }
@@ -1847,10 +1851,13 @@
     }
 
     Set<Member> redirectingFactories;
+    var staticFieldNames = <Name>{};
     for (var m in c.fields) {
       if (m.isStatic) {
         if (isRedirectingFactoryField(m)) {
           redirectingFactories = getRedirectingFactories(m).toSet();
+        } else {
+          staticFieldNames.add(m.name);
         }
       } else if (_extensionTypes.isNativeClass(c)) {
         jsMethods.addAll(_emitNativeFieldAccessors(m));
@@ -1872,6 +1879,11 @@
 
     var savedUri = _currentUri;
     for (var m in c.procedures) {
+      // Static accessors on static/lazy fields are emitted earlier in
+      // `_emitStaticFieldsAndAccessors`.
+      if (m.isStatic && m.isAccessor && staticFieldNames.contains(m.name)) {
+        continue;
+      }
       _staticTypeContext.enterMember(m);
       // For the Dart SDK, we use the member URI because it may be different
       // from the class (because of patch files). User code does not need this.
@@ -2310,36 +2322,51 @@
     }
 
     if (fields.isEmpty) return;
-    moduleItems.add(_emitLazyFields(
+    moduleItems.add(_emitLazyMembers(
         emitLibraryName(_currentLibrary), fields, _emitTopLevelMemberName));
   }
 
-  js_ast.Statement _emitLazyFields(
-      js_ast.Expression objExpr,
-      Iterable<Field> fields,
-      js_ast.LiteralString Function(Field f) emitFieldName) {
+  js_ast.Statement _emitLazyMembers(
+    js_ast.Expression objExpr,
+    Iterable<Member> members,
+    js_ast.LiteralString Function(Member) emitMemberName,
+  ) {
     var accessors = <js_ast.Method>[];
     var savedUri = _currentUri;
 
-    for (var field in fields) {
-      _currentUri = field.fileUri;
-      _staticTypeContext.enterMember(field);
-      var access = emitFieldName(field);
-      memberNames[field] = access.valueWithoutQuotes;
-      accessors.add(js_ast.Method(access, _emitStaticFieldInitializer(field),
-          isGetter: true)
-        ..sourceInformation = _hoverComment(
-            js_ast.PropertyAccess(objExpr, access),
-            field.fileOffset,
-            field.name.text.length));
+    for (var member in members) {
+      _currentUri = member.fileUri;
+      _staticTypeContext.enterMember(member);
+      var access = emitMemberName(member);
+      memberNames[member] = access.valueWithoutQuotes;
 
-      // TODO(jmesserly): currently uses a dummy setter to indicate writable.
-      if (!field.isFinal && !field.isConst) {
+      if (member is Field) {
+        accessors.add(js_ast.Method(access, _emitStaticFieldInitializer(member),
+            isGetter: true)
+          ..sourceInformation = _hoverComment(
+              js_ast.PropertyAccess(objExpr, access),
+              member.fileOffset,
+              member.name.text.length));
+        if (!member.isFinal && !member.isConst) {
+          // TODO(jmesserly): currently uses a dummy setter to indicate
+          // writable.
+          accessors.add(js_ast.Method(
+              access, js.call('function(_) {}') as js_ast.Fun,
+              isSetter: true));
+        }
+      } else if (member is Procedure) {
         accessors.add(js_ast.Method(
-            access, js.call('function(_) {}') as js_ast.Fun,
-            isSetter: true));
+            access, _emitFunction(member.function, member.name.text),
+            isGetter: member.isGetter, isSetter: member.isSetter)
+          ..sourceInformation = _hoverComment(
+              js_ast.PropertyAccess(objExpr, access),
+              member.fileOffset,
+              member.name.text.length));
+      } else {
+        throw UnsupportedError(
+            'Unsupported lazy member type ${member.runtimeType}: $member');
       }
-      _staticTypeContext.leaveMember(field);
+      _staticTypeContext.leaveMember(member);
     }
     _currentUri = savedUri;
 
@@ -4777,9 +4804,9 @@
   @override
   js_ast.Expression visitStaticSet(StaticSet node) {
     var target = node.target;
+    var result = _emitStaticTarget(target);
     var value = isJsMember(target) ? _assertInterop(node.value) : node.value;
-    return _visitExpression(value)
-        .toAssignExpression(_emitStaticTarget(target));
+    return _visitExpression(value).toAssignExpression(result);
   }
 
   @override
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
index 9c9e4b9..6a2f7b6 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart
@@ -858,7 +858,8 @@
     return value;
   };
   $desc.configurable = true;
-  if ($desc.set != null) {
+  let setter = $desc.set;
+  if (setter != null) {
     $desc.set = function(x) {
       if (!savedLocals) {
         $resetFields.push(() => {
@@ -871,6 +872,7 @@
       }
       init = null;
       value = x;
+      setter(x);
     };
   }
   return ${defineProperty(to, name, desc)};
@@ -914,7 +916,8 @@
     }
   };
   $desc.configurable = true;
-  if ($desc.set != null) {
+  let setter = $desc.set;
+  if (setter != null) {
     $desc.set = function(x) {
       if (!savedLocals) {
         $resetFields.push(() => {
@@ -926,6 +929,7 @@
       }
       init = null;
       value = x;
+      setter(x);
     };
   }
   return ${defineProperty(to, name, desc)};
diff --git a/tests/dartdevc/hot_restart_static_test.dart b/tests/dartdevc/hot_restart_static_test.dart
index c26c298..5b62495 100644
--- a/tests/dartdevc/hot_restart_static_test.dart
+++ b/tests/dartdevc/hot_restart_static_test.dart
@@ -23,7 +23,20 @@
   static int withInitializer = 3;
 }
 
-main() {
+class StaticsSetter {
+  static int counter = 0;
+  static const field = 5;
+  static const field2 = null;
+  static set field(int value) => StaticsSetter.counter += 1;
+  static set field2(value) => 42;
+}
+
+void main() {
+  testStaticFields();
+  testStaticSetterOfConstField();
+}
+
+void testStaticFields() {
   var resetFieldCount = dart.resetFields.length;
 
   // Set static fields without explicit initializers. Avoid calling getters for
@@ -82,3 +95,49 @@
   expectedResets = resetFieldCount + 6;
   Expect.equals(expectedResets, dart.resetFields.length);
 }
+
+void testStaticSetterOfConstField() {
+  var resetFieldCount = dart.resetFields.length;
+
+  // Static setters of const fields should behave according to spec.
+  Expect.equals(StaticsSetter.counter, 0);
+  Expect.equals(StaticsSetter.field, 5);
+  StaticsSetter.field = 100;
+  StaticsSetter.field = 100;
+  StaticsSetter.field = 100;
+  Expect.equals(StaticsSetter.field, 5);
+  Expect.equals(StaticsSetter.counter, 3);
+
+  // 2 new field resets: [StaticsSetter.counter] and [StaticsSetter.field].
+  var expectedResets = resetFieldCount + 2;
+  Expect.equals(expectedResets, dart.resetFields.length);
+
+  dart.hotRestart();
+  resetFieldCount = dart.resetFields.length;
+
+  // Static setters of const fields should be properly reset.
+  Expect.equals(StaticsSetter.counter, 0);
+  Expect.equals(StaticsSetter.field, 5);
+  StaticsSetter.field = 100;
+  StaticsSetter.field = 100;
+  StaticsSetter.field = 100;
+  Expect.equals(StaticsSetter.field, 5);
+  Expect.equals(StaticsSetter.counter, 3);
+
+  // 2 new field resets: [StaticsSetter.counter] and [StaticsSetter.field].
+  expectedResets = resetFieldCount + 2;
+  Expect.equals(expectedResets, dart.resetFields.length);
+
+  dart.hotRestart();
+  dart.hotRestart();
+  resetFieldCount = dart.resetFields.length;
+
+  // Invoke the static setter but not the getter.
+  StaticsSetter.field2 = 100;
+  StaticsSetter.field2 = 100;
+  StaticsSetter.field2 = 100;
+
+  // 1 new field reset: [StaticsSetter.field2].
+  expectedResets = resetFieldCount + 1;
+  Expect.equals(expectedResets, dart.resetFields.length);
+}
diff --git a/tests/language/static/static_setter_on_const_field_test.dart b/tests/language/static/static_setter_on_const_field_test.dart
new file mode 100644
index 0000000..48b1700
--- /dev/null
+++ b/tests/language/static/static_setter_on_const_field_test.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2022, the Dart 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.
+
+import "package:expect/expect.dart";
+
+// Tests that static setters on const fields are properly invoked on set.
+
+class A {
+  static const int field = 0;
+  static void set field(int value) {
+    Expect.equals(value, 100);
+    B.count += 1;
+  }
+}
+
+class B {
+  static int count = 0;
+}
+
+main() {
+  Expect.equals(B.count, 0);
+  A.field = 100;
+  Expect.equals(B.count, 1);
+  Expect.equals(A.field, 0);
+  A.field = 100;
+  Expect.equals(B.count, 2);
+  Expect.equals(A.field, 0);
+}
diff --git a/tests/language_2/static/static_setter_on_const_field_test.dart b/tests/language_2/static/static_setter_on_const_field_test.dart
new file mode 100644
index 0000000..354e724
--- /dev/null
+++ b/tests/language_2/static/static_setter_on_const_field_test.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2022, the Dart 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.
+
+// @dart = 2.9
+
+import "package:expect/expect.dart";
+
+// Tests that static setters of const fields are properly invoked on set.
+
+class A {
+  static const int field = 0;
+  static void set field(int value) {
+    Expect.equals(value, 100);
+    B.count += 1;
+  }
+}
+
+class B {
+  static int count = 0;
+}
+
+main() {
+  Expect.equals(B.count, 0);
+  A.field = 100;
+  Expect.equals(B.count, 1);
+  Expect.equals(A.field, 0);
+  A.field = 100;
+  Expect.equals(B.count, 2);
+  Expect.equals(A.field, 0);
+}