69565652cc
Closes https://github.com/dart-lang/sdk/pull/55799 GitOrigin-RevId: 297bb5fada9941d87bad51b70a6c8ec6f6c6e074 Change-Id: Ic21df065ac701764cb2e2d6b27a8364df7bf0c19 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/367361 Commit-Queue: Jake Macdonald <jakemac@google.com> Reviewed-by: Jake Macdonald <jakemac@google.com>
825 lines
28 KiB
Dart
825 lines
28 KiB
Dart
// 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.
|
|
|
|
// ignore_for_file: deprecated_member_use
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:macros/macros.dart';
|
|
|
|
/// A macro which adds a `fromJson(Map<String, Object?> json)` JSON decoding
|
|
/// constructor, and a `Map<String, Object?> toJson()` JSON encoding method to a
|
|
/// class.
|
|
///
|
|
/// To use this macro, annotate your class with `@JsonCodable()` and enable the
|
|
/// macros experiment (see README.md for full instructions).
|
|
///
|
|
/// The implementations are derived from the fields defined directly on the
|
|
/// annotated class, and field names are expected to exactly match the keys of
|
|
/// the maps that they are being decoded from.
|
|
///
|
|
/// If extending any class other than [Object], then the super class is expected
|
|
/// to also have a corresponding `toJson` method and `fromJson` constructor
|
|
/// (possibly via those classes also using the macro).
|
|
///
|
|
/// Annotated classes are not allowed to have a manually defined `toJson` method
|
|
/// or `fromJson` constructor.
|
|
///
|
|
/// See also [JsonEncodable] and [JsonDecodable] if you only want either the
|
|
/// `toJson` or `fromJson` functionality.
|
|
macro class JsonCodable
|
|
with _Shared, _FromJson, _ToJson
|
|
implements ClassDeclarationsMacro, ClassDefinitionMacro {
|
|
const JsonCodable();
|
|
|
|
/// Declares the `fromJson` constructor and `toJson` method, but does not
|
|
/// implement them.
|
|
@override
|
|
Future<void> buildDeclarationsForClass(
|
|
ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
|
|
final mapStringObject = await _setup(clazz, builder);
|
|
|
|
await (
|
|
_declareFromJson(clazz, builder, mapStringObject),
|
|
_declareToJson(clazz, builder, mapStringObject),
|
|
).wait;
|
|
}
|
|
|
|
/// Provides the actual definitions of the `fromJson` constructor and `toJson`
|
|
/// method, which were declared in the previous phase.
|
|
@override
|
|
Future<void> buildDefinitionForClass(
|
|
ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
|
|
final introspectionData =
|
|
await _SharedIntrospectionData.build(builder, clazz);
|
|
|
|
await (
|
|
_buildFromJson(clazz, builder, introspectionData),
|
|
_buildToJson(clazz, builder, introspectionData),
|
|
).wait;
|
|
}
|
|
}
|
|
|
|
/// A macro which adds a `Map<String, Object?> toJson()` JSON encoding method to
|
|
/// a class.
|
|
///
|
|
/// To use this macro, annotate your class with `@JsonEncodable()` and enable
|
|
/// the macros experiment (see README.md for full instructions).
|
|
///
|
|
/// The implementations are derived from the fields defined directly on the
|
|
/// annotated class, and field names are expected to exactly match the keys of
|
|
/// the maps that they are being decoded from.
|
|
///
|
|
/// If extending any class other than [Object], then the super class is expected
|
|
/// to also have a corresponding `toJson` method.
|
|
///
|
|
/// Annotated classes are not allowed to have a manually defined `toJson`
|
|
/// method.
|
|
macro class JsonEncodable
|
|
with _Shared, _ToJson
|
|
implements ClassDeclarationsMacro, ClassDefinitionMacro {
|
|
const JsonEncodable();
|
|
|
|
/// Declares the `toJson` method, but does not implement it.
|
|
@override
|
|
Future<void> buildDeclarationsForClass(
|
|
ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
|
|
final mapStringObject = await _setup(clazz, builder);
|
|
await _declareToJson(clazz, builder, mapStringObject);
|
|
}
|
|
|
|
/// Provides the actual definition of the `toJson` method, which was declared
|
|
/// in the previous phase.
|
|
@override
|
|
Future<void> buildDefinitionForClass(
|
|
ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
|
|
final introspectionData =
|
|
await _SharedIntrospectionData.build(builder, clazz);
|
|
await _buildToJson(clazz, builder, introspectionData);
|
|
}
|
|
}
|
|
|
|
/// A macro which adds a `fromJson(Map<String, Object?> json)` JSON decoding
|
|
/// constructor to a class.
|
|
///
|
|
/// To use this macro, annotate your class with `@JsonDecodable()` and enable
|
|
/// the macros experiment (see README.md for full instructions).
|
|
///
|
|
/// The implementations are derived from the fields defined directly on the
|
|
/// annotated class, and field names are expected to exactly match the keys of
|
|
/// the maps that they are being decoded from.
|
|
///
|
|
/// If extending any class other than [Object], then the super class is expected
|
|
/// to also have a corresponding `fromJson` constructor.
|
|
///
|
|
/// Annotated classes are not allowed to have a manually defined `fromJson`
|
|
/// constructor.
|
|
macro class JsonDecodable
|
|
with _Shared, _FromJson
|
|
implements ClassDeclarationsMacro, ClassDefinitionMacro {
|
|
const JsonDecodable();
|
|
|
|
/// Declares the `fromJson` constructor but does not implement it.
|
|
@override
|
|
Future<void> buildDeclarationsForClass(
|
|
ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
|
|
final mapStringObject = await _setup(clazz, builder);
|
|
await _declareFromJson(clazz, builder, mapStringObject);
|
|
}
|
|
|
|
/// Provides the actual definition of the `from` constructor, which was
|
|
/// declared in the previous phase.
|
|
@override
|
|
Future<void> buildDefinitionForClass(
|
|
ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
|
|
final introspectionData =
|
|
await _SharedIntrospectionData.build(builder, clazz);
|
|
await _buildFromJson(clazz, builder, introspectionData);
|
|
}
|
|
}
|
|
|
|
/// Shared logic for all macros which run in the declarations phase.
|
|
mixin _Shared {
|
|
/// Returns [type] as a [NamedTypeAnnotation] if it is one, otherwise returns
|
|
/// `null` and emits relevant error diagnostics.
|
|
NamedTypeAnnotation? _checkNamedType(TypeAnnotation type, Builder builder) {
|
|
if (type is NamedTypeAnnotation) return type;
|
|
if (type is OmittedTypeAnnotation) {
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Only fields with explicit types are allowed on serializable '
|
|
'classes, please add a type.',
|
|
target: type.asDiagnosticTarget),
|
|
Severity.error));
|
|
} else {
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Only fields with named types are allowed on serializable '
|
|
'classes.',
|
|
target: type.asDiagnosticTarget),
|
|
Severity.error));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Does some basic validation on [clazz], and shared setup logic.
|
|
///
|
|
/// Returns a code representation of the [Map<String, Object?>] class.
|
|
Future<NamedTypeAnnotationCode> _setup(
|
|
ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
|
|
if (clazz.typeParameters.isNotEmpty) {
|
|
throw DiagnosticException(Diagnostic(DiagnosticMessage(
|
|
// TODO: Target the actual type parameter, issue #55611
|
|
'Cannot be applied to classes with generic type parameters'),
|
|
Severity.error));
|
|
}
|
|
|
|
final (map, string, object) = await (
|
|
builder.resolveIdentifier(_dartCore, 'Map'),
|
|
builder.resolveIdentifier(_dartCore, 'String'),
|
|
builder.resolveIdentifier(_dartCore, 'Object'),
|
|
).wait;
|
|
return NamedTypeAnnotationCode(name: map, typeArguments: [
|
|
NamedTypeAnnotationCode(name: string),
|
|
NamedTypeAnnotationCode(name: object).asNullable
|
|
]);
|
|
}
|
|
}
|
|
|
|
/// Shared logic for macros that want to generate a `fromJson` constructor.
|
|
mixin _FromJson on _Shared {
|
|
/// Builds the actual `fromJson` constructor.
|
|
Future<void> _buildFromJson(
|
|
ClassDeclaration clazz,
|
|
TypeDefinitionBuilder typeBuilder,
|
|
_SharedIntrospectionData introspectionData) async {
|
|
final constructors = await typeBuilder.constructorsOf(clazz);
|
|
final fromJson =
|
|
constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson');
|
|
if (fromJson == null) return;
|
|
await _checkValidFromJson(fromJson, introspectionData, typeBuilder);
|
|
final builder = await typeBuilder.buildConstructor(fromJson.identifier);
|
|
|
|
// If extending something other than `Object`, it must have a `fromJson`
|
|
// constructor.
|
|
var superclassHasFromJson = false;
|
|
final superclassDeclaration = introspectionData.superclass;
|
|
if (superclassDeclaration != null &&
|
|
!superclassDeclaration.isExactly('Object', _dartCore)) {
|
|
final superclassConstructors =
|
|
await builder.constructorsOf(superclassDeclaration);
|
|
for (final superConstructor in superclassConstructors) {
|
|
if (superConstructor.identifier.name == 'fromJson') {
|
|
await _checkValidFromJson(
|
|
superConstructor, introspectionData, builder);
|
|
superclassHasFromJson = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!superclassHasFromJson) {
|
|
throw DiagnosticException(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Serialization of classes that extend other classes is only '
|
|
'supported if those classes have a valid '
|
|
'`fromJson(Map<String, Object?> json)` constructor.',
|
|
target: introspectionData.clazz.superclass?.asDiagnosticTarget),
|
|
Severity.error));
|
|
}
|
|
}
|
|
|
|
final fields = introspectionData.fields;
|
|
final jsonParam = fromJson.positionalParameters.single.identifier;
|
|
|
|
Future<Code> initializerForField(FieldDeclaration field) async {
|
|
return RawCode.fromParts([
|
|
field.identifier,
|
|
' = ',
|
|
await _convertTypeFromJson(
|
|
field.type,
|
|
RawCode.fromParts([
|
|
jsonParam,
|
|
"[r'",
|
|
field.identifier.name,
|
|
"']",
|
|
]),
|
|
builder,
|
|
introspectionData),
|
|
]);
|
|
}
|
|
|
|
final initializers = await Future.wait(fields.map(initializerForField));
|
|
|
|
if (superclassHasFromJson) {
|
|
initializers.add(RawCode.fromParts([
|
|
'super.fromJson(',
|
|
jsonParam,
|
|
')',
|
|
]));
|
|
}
|
|
|
|
builder.augment(initializers: initializers);
|
|
}
|
|
|
|
/// Emits an error [Diagnostic] if there is an existing `fromJson`
|
|
/// constructor on [clazz].
|
|
///
|
|
/// Returns `true` if the check succeeded (there was no `fromJson`) and false
|
|
/// if it didn't (a diagnostic was emitted).
|
|
Future<bool> _checkNoFromJson(
|
|
DeclarationBuilder builder, ClassDeclaration clazz) async {
|
|
final constructors = await builder.constructorsOf(clazz);
|
|
final fromJson =
|
|
constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson');
|
|
if (fromJson != null) {
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Cannot generate a fromJson constructor due to this existing '
|
|
'one.',
|
|
target: fromJson.asDiagnosticTarget),
|
|
Severity.error));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Checks that [constructor] is a valid `fromJson` constructor, and throws a
|
|
/// [DiagnosticException] if not.
|
|
Future<void> _checkValidFromJson(
|
|
ConstructorDeclaration constructor,
|
|
_SharedIntrospectionData introspectionData,
|
|
DefinitionBuilder builder) async {
|
|
if (constructor.namedParameters.isNotEmpty ||
|
|
constructor.positionalParameters.length != 1 ||
|
|
!(await (await builder
|
|
.resolve(constructor.positionalParameters.single.type.code))
|
|
.isExactly(introspectionData.jsonMapType))) {
|
|
throw DiagnosticException(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Expected exactly one parameter, with the type '
|
|
'Map<String, Object?>',
|
|
target: constructor.asDiagnosticTarget),
|
|
Severity.error));
|
|
}
|
|
}
|
|
|
|
/// Returns a [Code] object which is an expression that converts a JSON map
|
|
/// (referenced by [jsonReference]) into an instance of type [type].
|
|
Future<Code> _convertTypeFromJson(
|
|
TypeAnnotation rawType,
|
|
Code jsonReference,
|
|
DefinitionBuilder builder,
|
|
_SharedIntrospectionData introspectionData) async {
|
|
final type = _checkNamedType(rawType, builder);
|
|
if (type == null) {
|
|
return RawCode.fromString(
|
|
"throw 'Unable to deserialize type ${rawType.code.debugString}'");
|
|
}
|
|
|
|
// Follow type aliases until we reach an actual named type.
|
|
var classDecl = await type.classDeclaration(builder);
|
|
if (classDecl == null) {
|
|
return RawCode.fromString(
|
|
"throw 'Unable to deserialize type ${type.code.debugString}'");
|
|
}
|
|
|
|
var nullCheck = type.isNullable
|
|
? RawCode.fromParts([
|
|
jsonReference,
|
|
// `null` is a reserved word, we can just use it.
|
|
' == null ? null : ',
|
|
])
|
|
: null;
|
|
|
|
// Check for the supported core types, and deserialize them accordingly.
|
|
if (classDecl.library.uri == _dartCore) {
|
|
switch (classDecl.identifier.name) {
|
|
case 'List':
|
|
return RawCode.fromParts([
|
|
if (nullCheck != null) nullCheck,
|
|
'[ for (final item in ',
|
|
jsonReference,
|
|
' as ',
|
|
introspectionData.jsonListCode,
|
|
') ',
|
|
await _convertTypeFromJson(type.typeArguments.single,
|
|
RawCode.fromString('item'), builder, introspectionData),
|
|
']',
|
|
]);
|
|
case 'Set':
|
|
return RawCode.fromParts([
|
|
if (nullCheck != null) nullCheck,
|
|
'{ for (final item in ',
|
|
jsonReference,
|
|
' as ',
|
|
introspectionData.jsonListCode,
|
|
')',
|
|
await _convertTypeFromJson(type.typeArguments.single,
|
|
RawCode.fromString('item'), builder, introspectionData),
|
|
'}',
|
|
]);
|
|
case 'Map':
|
|
return RawCode.fromParts([
|
|
if (nullCheck != null) nullCheck,
|
|
'{ for (final ',
|
|
introspectionData.mapEntry,
|
|
'(:key, :value) in (',
|
|
jsonReference,
|
|
' as ',
|
|
introspectionData.jsonMapCode,
|
|
').entries) key: ',
|
|
await _convertTypeFromJson(type.typeArguments.last,
|
|
RawCode.fromString('value'), builder, introspectionData),
|
|
'}',
|
|
]);
|
|
case 'int' || 'double' || 'num' || 'String' || 'bool':
|
|
return RawCode.fromParts([
|
|
jsonReference,
|
|
' as ',
|
|
type.code,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Otherwise, check if `classDecl` has a `fromJson` constructor.
|
|
final constructors = await builder.constructorsOf(classDecl);
|
|
final fromJson = constructors
|
|
.firstWhereOrNull((c) => c.identifier.name == 'fromJson')
|
|
?.identifier;
|
|
if (fromJson != null) {
|
|
return RawCode.fromParts([
|
|
if (nullCheck != null) nullCheck,
|
|
fromJson,
|
|
'(',
|
|
jsonReference,
|
|
' as ',
|
|
introspectionData.jsonMapCode,
|
|
')',
|
|
]);
|
|
}
|
|
|
|
// Unsupported type, report an error and return valid code that throws.
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Unable to deserialize type, it must be a native JSON type or a '
|
|
'type with a `fromJson(Map<String, Object?> json)` constructor.',
|
|
target: type.asDiagnosticTarget),
|
|
Severity.error));
|
|
return RawCode.fromString(
|
|
"throw 'Unable to deserialize type ${type.code.debugString}'");
|
|
}
|
|
|
|
/// Declares a `fromJson` constructor in [clazz], if one does not exist
|
|
/// already.
|
|
Future<void> _declareFromJson(
|
|
ClassDeclaration clazz,
|
|
MemberDeclarationBuilder builder,
|
|
NamedTypeAnnotationCode mapStringObject) async {
|
|
if (!(await _checkNoFromJson(builder, clazz))) return;
|
|
|
|
builder.declareInType(DeclarationCode.fromParts([
|
|
// TODO(language#3580): Remove/replace 'external'?
|
|
' external ',
|
|
clazz.identifier.name,
|
|
'.fromJson(',
|
|
mapStringObject,
|
|
' json);',
|
|
]));
|
|
}
|
|
}
|
|
|
|
/// Shared logic for macros that want to generate a `toJson` method.
|
|
mixin _ToJson on _Shared {
|
|
/// Builds the actual `toJson` method.
|
|
Future<void> _buildToJson(
|
|
ClassDeclaration clazz,
|
|
TypeDefinitionBuilder typeBuilder,
|
|
_SharedIntrospectionData introspectionData) async {
|
|
final methods = await typeBuilder.methodsOf(clazz);
|
|
final toJson =
|
|
methods.firstWhereOrNull((c) => c.identifier.name == 'toJson');
|
|
if (toJson == null) return;
|
|
if (!(await _checkValidToJson(toJson, introspectionData, typeBuilder))) {
|
|
return;
|
|
}
|
|
|
|
final builder = await typeBuilder.buildMethod(toJson.identifier);
|
|
|
|
// If extending something other than `Object`, it must have a `toJson`
|
|
// method.
|
|
var superclassHasToJson = false;
|
|
final superclassDeclaration = introspectionData.superclass;
|
|
if (superclassDeclaration != null &&
|
|
!superclassDeclaration.isExactly('Object', _dartCore)) {
|
|
final superclassMethods = await builder.methodsOf(superclassDeclaration);
|
|
for (final superMethod in superclassMethods) {
|
|
if (superMethod.identifier.name == 'toJson') {
|
|
if (!(await _checkValidToJson(
|
|
superMethod, introspectionData, builder))) {
|
|
return;
|
|
}
|
|
superclassHasToJson = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!superclassHasToJson) {
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Serialization of classes that extend other classes is only '
|
|
'supported if those classes have a valid '
|
|
'`Map<String, Object?> toJson()` method.',
|
|
target: introspectionData.clazz.superclass?.asDiagnosticTarget),
|
|
Severity.error));
|
|
return;
|
|
}
|
|
}
|
|
|
|
final fields = introspectionData.fields;
|
|
final parts = <Object>[
|
|
'{\n final json = ',
|
|
if (superclassHasToJson)
|
|
'super.toJson()'
|
|
else ...[
|
|
'<',
|
|
introspectionData.stringCode,
|
|
', ',
|
|
introspectionData.objectCode.asNullable,
|
|
'>{}',
|
|
],
|
|
';\n ',
|
|
];
|
|
|
|
Future<Code> addEntryForField(FieldDeclaration field) async {
|
|
final parts = <Object>[];
|
|
final doNullCheck = field.type.isNullable;
|
|
if (doNullCheck) {
|
|
parts.addAll([
|
|
'if (',
|
|
field.identifier,
|
|
// `null` is a reserved word, we can just use it.
|
|
' != null) {\n ',
|
|
]);
|
|
}
|
|
parts.addAll([
|
|
"json[r'",
|
|
field.identifier.name,
|
|
"'] = ",
|
|
await _convertTypeToJson(
|
|
field.type,
|
|
RawCode.fromParts([
|
|
field.identifier,
|
|
if (doNullCheck) '!',
|
|
]),
|
|
builder,
|
|
introspectionData),
|
|
';\n ',
|
|
]);
|
|
if (doNullCheck) {
|
|
parts.add('}\n ');
|
|
}
|
|
return RawCode.fromParts(parts);
|
|
}
|
|
|
|
parts.addAll(await Future.wait(fields.map(addEntryForField)));
|
|
|
|
parts.add('return json;\n }');
|
|
|
|
builder.augment(FunctionBodyCode.fromParts(parts));
|
|
}
|
|
|
|
/// Emits an error [Diagnostic] if there is an existing `toJson` method on
|
|
/// [clazz].
|
|
///
|
|
/// Returns `true` if the check succeeded (there was no `toJson`) and false
|
|
/// if it didn't (a diagnostic was emitted).
|
|
Future<bool> _checkNoToJson(
|
|
DeclarationBuilder builder, ClassDeclaration clazz) async {
|
|
final methods = await builder.methodsOf(clazz);
|
|
final toJson =
|
|
methods.firstWhereOrNull((m) => m.identifier.name == 'toJson');
|
|
if (toJson != null) {
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Cannot generate a toJson method due to this existing one.',
|
|
target: toJson.asDiagnosticTarget),
|
|
Severity.error));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Checks that [method] is a valid `toJson` method, and throws a
|
|
/// [DiagnosticException] if not.
|
|
Future<bool> _checkValidToJson(
|
|
MethodDeclaration method,
|
|
_SharedIntrospectionData introspectionData,
|
|
DefinitionBuilder builder) async {
|
|
if (method.namedParameters.isNotEmpty ||
|
|
method.positionalParameters.isNotEmpty ||
|
|
!(await (await builder.resolve(method.returnType.code))
|
|
.isExactly(introspectionData.jsonMapType))) {
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Expected no parameters, and a return type of '
|
|
'Map<String, Object?>',
|
|
target: method.asDiagnosticTarget),
|
|
Severity.error));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Returns a [Code] object which is an expression that converts an instance
|
|
/// of type [type] (referenced by [valueReference]) into a JSON map.
|
|
Future<Code> _convertTypeToJson(
|
|
TypeAnnotation rawType,
|
|
Code valueReference,
|
|
DefinitionBuilder builder,
|
|
_SharedIntrospectionData introspectionData) async {
|
|
final type = _checkNamedType(rawType, builder);
|
|
if (type == null) {
|
|
return RawCode.fromString(
|
|
"throw 'Unable to serialize type ${rawType.code.debugString}'");
|
|
}
|
|
|
|
// Follow type aliases until we reach an actual named type.
|
|
var classDecl = await type.classDeclaration(builder);
|
|
if (classDecl == null) {
|
|
return RawCode.fromString(
|
|
"throw 'Unable to serialize type ${type.code.debugString}'");
|
|
}
|
|
|
|
var nullCheck = type.isNullable
|
|
? RawCode.fromParts([
|
|
valueReference,
|
|
// `null` is a reserved word, we can just use it.
|
|
' == null ? null : ',
|
|
])
|
|
: null;
|
|
|
|
// Check for the supported core types, and serialize them accordingly.
|
|
if (classDecl.library.uri == _dartCore) {
|
|
switch (classDecl.identifier.name) {
|
|
case 'List' || 'Set':
|
|
return RawCode.fromParts([
|
|
if (nullCheck != null) nullCheck,
|
|
'[ for (final item in ',
|
|
valueReference,
|
|
') ',
|
|
await _convertTypeToJson(type.typeArguments.single,
|
|
RawCode.fromString('item'), builder, introspectionData),
|
|
']',
|
|
]);
|
|
case 'Map':
|
|
return RawCode.fromParts([
|
|
if (nullCheck != null) nullCheck,
|
|
'{ for (final ',
|
|
introspectionData.mapEntry,
|
|
'(:key, :value) in ',
|
|
valueReference,
|
|
'.entries) key: ',
|
|
await _convertTypeToJson(type.typeArguments.last,
|
|
RawCode.fromString('value'), builder, introspectionData),
|
|
'}',
|
|
]);
|
|
case 'int' || 'double' || 'num' || 'String' || 'bool':
|
|
return valueReference;
|
|
}
|
|
}
|
|
|
|
// Next, check if it has a `toJson()` method and call that.
|
|
final methods = await builder.methodsOf(classDecl);
|
|
final toJson = methods
|
|
.firstWhereOrNull((c) => c.identifier.name == 'toJson')
|
|
?.identifier;
|
|
if (toJson != null) {
|
|
return RawCode.fromParts([
|
|
if (nullCheck != null) nullCheck,
|
|
valueReference,
|
|
'.toJson()',
|
|
]);
|
|
}
|
|
|
|
// Unsupported type, report an error and return valid code that throws.
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Unable to serialize type, it must be a native JSON type or a '
|
|
'type with a `Map<String, Object?> toJson()` method.',
|
|
target: type.asDiagnosticTarget),
|
|
Severity.error));
|
|
return RawCode.fromString(
|
|
"throw 'Unable to serialize type ${type.code.debugString}'");
|
|
}
|
|
|
|
/// Declares a `toJson` method in [clazz], if one does not exist already.
|
|
Future<void> _declareToJson(
|
|
ClassDeclaration clazz,
|
|
MemberDeclarationBuilder builder,
|
|
NamedTypeAnnotationCode mapStringObject) async {
|
|
if (!(await _checkNoToJson(builder, clazz))) return;
|
|
builder.declareInType(DeclarationCode.fromParts([
|
|
// TODO(language#3580): Remove/replace 'external'?
|
|
' external ',
|
|
mapStringObject,
|
|
' toJson();',
|
|
]));
|
|
}
|
|
}
|
|
|
|
/// This data is collected asynchronously, so we only want to do it once and
|
|
/// share that work across multiple locations.
|
|
final class _SharedIntrospectionData {
|
|
/// The declaration of the class we are generating for.
|
|
final ClassDeclaration clazz;
|
|
|
|
/// All the fields on the [clazz].
|
|
final List<FieldDeclaration> fields;
|
|
|
|
/// A [Code] representation of the type [List<Object?>].
|
|
final NamedTypeAnnotationCode jsonListCode;
|
|
|
|
/// A [Code] representation of the type [Map<String, Object?>].
|
|
final NamedTypeAnnotationCode jsonMapCode;
|
|
|
|
/// The resolved [StaticType] representing the [Map<String, Object?>] type.
|
|
final StaticType jsonMapType;
|
|
|
|
/// The resolved identifier for the [MapEntry] class.
|
|
final Identifier mapEntry;
|
|
|
|
/// A [Code] representation of the type [Object].
|
|
final NamedTypeAnnotationCode objectCode;
|
|
|
|
/// A [Code] representation of the type [String].
|
|
final NamedTypeAnnotationCode stringCode;
|
|
|
|
/// The declaration of the superclass of [clazz], if it is not [Object].
|
|
final ClassDeclaration? superclass;
|
|
|
|
_SharedIntrospectionData({
|
|
required this.clazz,
|
|
required this.fields,
|
|
required this.jsonListCode,
|
|
required this.jsonMapCode,
|
|
required this.jsonMapType,
|
|
required this.mapEntry,
|
|
required this.objectCode,
|
|
required this.stringCode,
|
|
required this.superclass,
|
|
});
|
|
|
|
static Future<_SharedIntrospectionData> build(
|
|
DeclarationPhaseIntrospector builder, ClassDeclaration clazz) async {
|
|
final (list, map, mapEntry, object, string) = await (
|
|
builder.resolveIdentifier(_dartCore, 'List'),
|
|
builder.resolveIdentifier(_dartCore, 'Map'),
|
|
builder.resolveIdentifier(_dartCore, 'MapEntry'),
|
|
builder.resolveIdentifier(_dartCore, 'Object'),
|
|
builder.resolveIdentifier(_dartCore, 'String'),
|
|
).wait;
|
|
final objectCode = NamedTypeAnnotationCode(name: object);
|
|
final nullableObjectCode = objectCode.asNullable;
|
|
final jsonListCode = NamedTypeAnnotationCode(name: list, typeArguments: [
|
|
nullableObjectCode,
|
|
]);
|
|
final jsonMapCode = NamedTypeAnnotationCode(name: map, typeArguments: [
|
|
NamedTypeAnnotationCode(name: string),
|
|
nullableObjectCode,
|
|
]);
|
|
final stringCode = NamedTypeAnnotationCode(name: string);
|
|
final superclass = clazz.superclass;
|
|
final (fields, jsonMapType, superclassDecl) = await (
|
|
builder.fieldsOf(clazz),
|
|
builder.resolve(jsonMapCode),
|
|
superclass == null
|
|
? Future.value(null)
|
|
: builder.typeDeclarationOf(superclass.identifier),
|
|
).wait;
|
|
|
|
return _SharedIntrospectionData(
|
|
clazz: clazz,
|
|
fields: fields,
|
|
jsonListCode: jsonListCode,
|
|
jsonMapCode: jsonMapCode,
|
|
jsonMapType: jsonMapType,
|
|
mapEntry: mapEntry,
|
|
objectCode: objectCode,
|
|
stringCode: stringCode,
|
|
superclass: superclassDecl as ClassDeclaration?,
|
|
);
|
|
}
|
|
}
|
|
|
|
final _dartCore = Uri.parse('dart:core');
|
|
|
|
extension _FirstWhereOrNull<T> on Iterable<T> {
|
|
T? firstWhereOrNull(bool Function(T) compare) {
|
|
for (final item in this) {
|
|
if (compare(item)) return item;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
extension _IsExactly on TypeDeclaration {
|
|
/// Cheaper than checking types using a [StaticType].
|
|
bool isExactly(String name, Uri library) =>
|
|
identifier.name == name && this.library.uri == library;
|
|
}
|
|
|
|
extension on Code {
|
|
/// Used for error messages.
|
|
String get debugString {
|
|
final buffer = StringBuffer();
|
|
_writeDebugString(buffer);
|
|
return buffer.toString();
|
|
}
|
|
|
|
void _writeDebugString(StringBuffer buffer) {
|
|
for (final part in parts) {
|
|
switch (part) {
|
|
case Code():
|
|
part._writeDebugString(buffer);
|
|
case Identifier():
|
|
buffer.write(part.name);
|
|
case OmittedTypeAnnotation():
|
|
buffer.write('<omitted>');
|
|
default:
|
|
buffer.write(part);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension on NamedTypeAnnotation {
|
|
/// Follows the declaration of this type through any type aliases, until it
|
|
/// reaches a [ClassDeclaration], or returns null if it does not bottom out on
|
|
/// a class.
|
|
Future<ClassDeclaration?> classDeclaration(DefinitionBuilder builder) async {
|
|
var typeDecl = await builder.typeDeclarationOf(identifier);
|
|
while (typeDecl is TypeAliasDeclaration) {
|
|
final aliasedType = typeDecl.aliasedType;
|
|
if (aliasedType is! NamedTypeAnnotation) {
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Only fields with named types are allowed on serializable '
|
|
'classes',
|
|
target: asDiagnosticTarget),
|
|
Severity.error));
|
|
return null;
|
|
}
|
|
typeDecl = await builder.typeDeclarationOf(aliasedType.identifier);
|
|
}
|
|
if (typeDecl is! ClassDeclaration) {
|
|
builder.report(Diagnostic(
|
|
DiagnosticMessage(
|
|
'Only classes are supported as field types for serializable '
|
|
'classes',
|
|
target: asDiagnosticTarget),
|
|
Severity.error));
|
|
return null;
|
|
}
|
|
return typeDecl;
|
|
}
|
|
}
|