support custom JSON keys for fields

Change-Id: I72c2a4f09b012c4100fe9286967d182831507160
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/350740
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Auto-Submit: Jake Macdonald <jakemac@google.com>
Reviewed-by: Morgan :) <davidmorgan@google.com>
Commit-Queue: Jake Macdonald <jakemac@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/macros/api/diagnostic.dart b/pkg/_fe_analyzer_shared/lib/src/macros/api/diagnostic.dart
index ec4d191..f520062 100644
--- a/pkg/_fe_analyzer_shared/lib/src/macros/api/diagnostic.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/macros/api/diagnostic.dart
@@ -77,6 +77,18 @@
       new TypeAnnotationDiagnosticTarget(this);
 }
 
+/// A [DiagnosticMessage] target which is a [MetadataAnnotation].
+final class MetadataAnnotationDiagnosticTarget extends DiagnosticTarget {
+  final MetadataAnnotation metadataAnnotation;
+
+  MetadataAnnotationDiagnosticTarget(this.metadataAnnotation);
+}
+
+extension MetadataAnnotationAsTarget on MetadataAnnotation {
+  MetadataAnnotationDiagnosticTarget get asDiagnosticTarget =>
+      new MetadataAnnotationDiagnosticTarget(this);
+}
+
 /// The severities supported for [Diagnostic]s.
 enum Severity {
   /// Informational message only, for example a style guideline is not being
diff --git a/pkg/_fe_analyzer_shared/lib/src/macros/executor/serialization_extensions.dart b/pkg/_fe_analyzer_shared/lib/src/macros/executor/serialization_extensions.dart
index be984c8..ebb7b95 100644
--- a/pkg/_fe_analyzer_shared/lib/src/macros/executor/serialization_extensions.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/macros/executor/serialization_extensions.dart
@@ -654,6 +654,9 @@
         (target.declaration as DeclarationImpl).serialize(serializer);
       case TypeAnnotationDiagnosticTarget target:
         (target.typeAnnotation as TypeAnnotationImpl).serialize(serializer);
+      case MetadataAnnotationDiagnosticTarget target:
+        (target.metadataAnnotation as MetadataAnnotationImpl)
+            .serialize(serializer);
     }
   }
 }
diff --git a/pkg/analyzer/lib/src/generated/error_verifier.dart b/pkg/analyzer/lib/src/generated/error_verifier.dart
index 31eedcf..8de08c4 100644
--- a/pkg/analyzer/lib/src/generated/error_verifier.dart
+++ b/pkg/analyzer/lib/src/generated/error_verifier.dart
@@ -6091,6 +6091,9 @@
         case TypeAnnotationMacroDiagnosticTarget():
           // TODO(scheglov): Handle this case.
           throw UnimplementedError();
+        case ElementAnnotationMacroDiagnosticTarget():
+          // TODO(scheglov): Handle this case.
+          throw UnimplementedError();
       }
     }
 
@@ -6184,6 +6187,9 @@
                       diagnostic.contextMessages.map(convertMessage).toList(),
                 );
               }
+            case ElementAnnotationMacroDiagnosticTarget():
+              // TODO(scheglov): Handle this case.
+              throw UnimplementedError();
           }
       }
     }
diff --git a/pkg/analyzer/lib/src/summary2/bundle_writer.dart b/pkg/analyzer/lib/src/summary2/bundle_writer.dart
index 32a8fe3..22f2d63 100644
--- a/pkg/analyzer/lib/src/summary2/bundle_writer.dart
+++ b/pkg/analyzer/lib/src/summary2/bundle_writer.dart
@@ -1021,6 +1021,9 @@
       case TypeAnnotationMacroDiagnosticTarget():
         writeEnum(MacroDiagnosticTargetKind.type);
         writeTypeAnnotationLocation(target.location);
+      case ElementAnnotationMacroDiagnosticTarget():
+        // TODO(scheglov): Implement this
+        throw UnimplementedError('');
     }
   }
 
diff --git a/pkg/analyzer/lib/src/summary2/macro_application.dart b/pkg/analyzer/lib/src/summary2/macro_application.dart
index 170af8c..25191af 100644
--- a/pkg/analyzer/lib/src/summary2/macro_application.dart
+++ b/pkg/analyzer/lib/src/summary2/macro_application.dart
@@ -544,6 +544,10 @@
           target = ElementMacroDiagnosticTarget(element: element);
         case macro.TypeAnnotationDiagnosticTarget macroTarget:
           target = _typeAnnotationTarget(application, macroTarget);
+        case macro.MetadataAnnotationDiagnosticTarget macroTarget:
+          target = ElementAnnotationMacroDiagnosticTarget(
+              annotation:
+                  macroTarget.metadataAnnotation as ElementAnnotationImpl);
         case null:
           target = ApplicationMacroDiagnosticTarget(
             annotationIndex: application.annotationIndex,
diff --git a/pkg/analyzer/lib/src/summary2/macro_application_error.dart b/pkg/analyzer/lib/src/summary2/macro_application_error.dart
index 522d3f2..b37821e 100644
--- a/pkg/analyzer/lib/src/summary2/macro_application_error.dart
+++ b/pkg/analyzer/lib/src/summary2/macro_application_error.dart
@@ -56,6 +56,15 @@
   });
 }
 
+final class ElementAnnotationMacroDiagnosticTarget
+    extends MacroDiagnosticTarget {
+  final ElementAnnotationImpl annotation;
+
+  ElementAnnotationMacroDiagnosticTarget({
+    required this.annotation,
+  });
+}
+
 final class ElementMacroDiagnosticTarget extends MacroDiagnosticTarget {
   final ElementImpl element;
 
diff --git a/pkg/analyzer/test/src/summary/element_text.dart b/pkg/analyzer/test/src/summary/element_text.dart
index 3de5ee9..bacdc98 100644
--- a/pkg/analyzer/test/src/summary/element_text.dart
+++ b/pkg/analyzer/test/src/summary/element_text.dart
@@ -830,6 +830,9 @@
           _sink.withIndent(() {
             writeTypeAnnotationLocation(target.location);
           });
+        case ElementAnnotationMacroDiagnosticTarget():
+          // TODO(scheglov): Implement this
+          throw UnimplementedError();
       }
     }
 
diff --git a/tests/language/macros/augment/impl/class_declarations_macro.dart b/tests/language/macros/augment/impl/class_declarations_macro.dart
index 8837a1b..44a7ab2 100644
--- a/tests/language/macros/augment/impl/class_declarations_macro.dart
+++ b/tests/language/macros/augment/impl/class_declarations_macro.dart
@@ -16,7 +16,8 @@
   }
 }
 
-macro class ClassDeclarationsDeclareInLibrary implements ClassDeclarationsMacro {
+macro class ClassDeclarationsDeclareInLibrary
+    implements ClassDeclarationsMacro {
   final String code;
 
   const ClassDeclarationsDeclareInLibrary(this.code);
diff --git a/tests/language/macros/augment/impl/impl.dart b/tests/language/macros/augment/impl/impl.dart
index 7981e95..26b5d24 100644
--- a/tests/language/macros/augment/impl/impl.dart
+++ b/tests/language/macros/augment/impl/impl.dart
@@ -1,7 +1,9 @@
 // Copyright (c) 2023, 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.
+//
 // ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
 
 import 'package:_fe_analyzer_shared/src/macros/api.dart';
 
diff --git a/tests/language/macros/error/impl/throw_diagnostic_exception_macro.dart b/tests/language/macros/error/impl/throw_diagnostic_exception_macro.dart
index b0ebe00..14b79e7 100644
--- a/tests/language/macros/error/impl/throw_diagnostic_exception_macro.dart
+++ b/tests/language/macros/error/impl/throw_diagnostic_exception_macro.dart
@@ -8,14 +8,16 @@
   final String atTypeDeclaration;
   final String withMessage;
 
-  const ThrowDiagnosticException({
-    required this.atTypeDeclaration, required this.withMessage});
+  const ThrowDiagnosticException(
+      {required this.atTypeDeclaration, required this.withMessage});
 
   Future<void> buildDeclarationsForClass(
       ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
-    final identifier = await builder.resolveIdentifier(clazz.library.uri, atTypeDeclaration);
+    final identifier =
+        await builder.resolveIdentifier(clazz.library.uri, atTypeDeclaration);
     final declaration = await builder.typeDeclarationOf(identifier);
-    throw DiagnosticException(Diagnostic(DiagnosticMessage(
-      withMessage, target: declaration.asDiagnosticTarget), Severity.error));
+    throw DiagnosticException(Diagnostic(
+        DiagnosticMessage(withMessage, target: declaration.asDiagnosticTarget),
+        Severity.error));
   }
 }
diff --git a/tests/language/macros/introspect/impl/assert_in_declarations_phase_macro.dart b/tests/language/macros/introspect/impl/assert_in_declarations_phase_macro.dart
index ba176e9..3ec4b07 100644
--- a/tests/language/macros/introspect/impl/assert_in_declarations_phase_macro.dart
+++ b/tests/language/macros/introspect/impl/assert_in_declarations_phase_macro.dart
@@ -1,7 +1,10 @@
 // Copyright (c) 2023, 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.
+//
 // ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+
 import 'dart:async';
 
 import 'package:_fe_analyzer_shared/src/macros/api.dart';
diff --git a/tests/language/macros/introspect/impl/assert_in_definitions_phase_macro.dart b/tests/language/macros/introspect/impl/assert_in_definitions_phase_macro.dart
index ba63772..56972dc 100644
--- a/tests/language/macros/introspect/impl/assert_in_definitions_phase_macro.dart
+++ b/tests/language/macros/introspect/impl/assert_in_definitions_phase_macro.dart
@@ -1,7 +1,10 @@
 // Copyright (c) 2023, 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.
+//
 // ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+
 import 'dart:async';
 
 import 'package:_fe_analyzer_shared/src/macros/api.dart';
diff --git a/tests/language/macros/introspect/impl/assert_in_types_phase_macro.dart b/tests/language/macros/introspect/impl/assert_in_types_phase_macro.dart
index 2bdb5c5..1a96cb5 100644
--- a/tests/language/macros/introspect/impl/assert_in_types_phase_macro.dart
+++ b/tests/language/macros/introspect/impl/assert_in_types_phase_macro.dart
@@ -1,7 +1,10 @@
 // Copyright (c) 2023, 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.
+//
 // ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+
 import 'dart:async';
 
 import 'package:_fe_analyzer_shared/src/macros/api.dart';
diff --git a/tests/language/macros/json/json_key.dart b/tests/language/macros/json/json_key.dart
new file mode 100644
index 0000000..d73dd7b
--- /dev/null
+++ b/tests/language/macros/json/json_key.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2024, 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:meta/meta_meta.dart';
+
+/// An annotation used to specify how a field is serialized.
+@Target({TargetKind.field, TargetKind.getter})
+class JsonKey {
+  /// The key in a JSON map to use when reading and writing values corresponding
+  /// to the annotated fields.
+  ///
+  /// If `null`, the field name is used.
+  final String? name;
+
+  const JsonKey({this.name});
+}
diff --git a/tests/language/macros/json/json_serializable.dart b/tests/language/macros/json/json_serializable.dart
index 48ad15d..0f0d7bd 100644
--- a/tests/language/macros/json/json_serializable.dart
+++ b/tests/language/macros/json/json_serializable.dart
@@ -35,10 +35,7 @@
     var mapStringObject = NamedTypeAnnotationCode(
         name: map, typeArguments: [string, object.asNullable]);
 
-    // TODO: This only works because the macro file lives right next to the file
-    // it is applied to.
-    var jsonSerializableUri =
-        clazz.library.uri.resolve('json_serializable.dart');
+    var jsonSerializableUri = clazz.jsonSerializableUri;
 
     builder.declareInType(DeclarationCode.fromParts([
       '  @',
@@ -119,7 +116,9 @@
               field.type,
               RawCode.fromParts([
                 jsonParam,
-                '["${field.identifier.name}"]',
+                '[',
+                await field._jsonKeyName(builder),
+                ']',
               ]),
               builder,
               fromJsonData),
@@ -253,6 +252,31 @@
   }
 }
 
+extension _ on FieldDeclaration {
+  // TODO: Support `IdentifierMetadataAnnotation`s once we can do constant eval.
+  Future<Code> _jsonKeyName(DefinitionBuilder builder) async {
+    ConstructorMetadataAnnotation? jsonKey;
+    for (var annotation in metadata) {
+      if (annotation is! ConstructorMetadataAnnotation) continue;
+      if (annotation.type.name != 'JsonKey') continue;
+      var declaration = await builder.typeDeclarationOf(annotation.type);
+      if (declaration.library.uri != jsonKeyUri) continue;
+
+      if (jsonKey != null) {
+        builder.report(Diagnostic(
+            DiagnosticMessage(
+                'Only one JsonKey annotation is allowed.',
+                target: annotation.asDiagnosticTarget),
+            Severity.error));
+      } else {
+        jsonKey = annotation;
+      }
+    }
+    return jsonKey?.namedArguments['name'] ??
+        RawCode.fromString('\'${identifier.name}\'');
+  }
+}
+
 final class _FromJsonData {
   final NamedTypeAnnotationCode jsonListCode;
   final NamedTypeAnnotationCode jsonMapCode;
@@ -368,9 +392,8 @@
       if (superclassHasToJson) '\n    ...super.toJson(),',
       for (var field in fields)
         RawCode.fromParts([
-          '\n    \'',
-          field.identifier.name,
-          '\'',
+          '\n    ',
+          await field._jsonKeyName(builder),
           ': ',
           await _convertTypeToJson(field.type,
               RawCode.fromParts([field.identifier]), builder, toJsonData),
@@ -561,3 +584,11 @@
     }
   }
 }
+
+// TODO: These only work because the macro file lives right next to the file
+// it is applied to, we need a better solution at some point.
+extension _RelativeUris on Declaration {
+  Uri get jsonKeyUri => library.uri.resolve('json_key.dart');
+
+  Uri get jsonSerializableUri => library.uri.resolve('json_serializable.dart');
+}
diff --git a/tests/language/macros/json/json_serializable_test.dart b/tests/language/macros/json/json_serializable_test.dart
index 7a20464..15c46cd 100644
--- a/tests/language/macros/json/json_serializable_test.dart
+++ b/tests/language/macros/json/json_serializable_test.dart
@@ -6,6 +6,7 @@
 
 import 'package:expect/expect.dart';
 
+import 'json_key.dart';
 import 'json_serializable.dart';
 
 void main() {
@@ -33,12 +34,12 @@
   var rogerAccountJson = {
     ...rogerJson,
     'login': {
-      'username': 'roger1',
+      'account': 'roger1',
       'password': 'theGoat',
     },
   };
   (rogerAccountJson['friends'] as dynamic)[0]['login'] = {
-    'username': 'felixTheCat',
+    'account': 'felixTheCat',
     'password': '9Lives',
   };
   var rogerAccount = UserAccount.fromJson(rogerAccountJson);
@@ -63,6 +64,8 @@
 
 @JsonSerializable()
 class Login {
+  @JsonKey(name: 'account')
   final String username;
+
   final String password;
 }