Skip to content
This repository has been archived by the owner on Feb 4, 2025. It is now read-only.

Commit

Permalink
@JsonCodable() supports super toJson and fromJson.
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmorgan committed Oct 8, 2024
1 parent a3a5ec0 commit dbc871e
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 5 deletions.
28 changes: 28 additions & 0 deletions goldens/foo/lib/foo.analyzer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"bar": {
"properties": {
"isAbstract": false,
"isConstructor": false,
"isGetter": false,
"isField": true,
"isMethod": false,
Expand All @@ -22,6 +23,19 @@
"instantiation": []
}
}
},
"": {
"properties": {
"isAbstract": false,
"isConstructor": true,
"isGetter": false,
"isField": false,
"isMethod": false,
"isStatic": false
},
"requiredPositionalParameters": [],
"optionalPositionalParameters": [],
"namedParameters": []
}
}
},
Expand All @@ -30,6 +44,7 @@
"bar": {
"properties": {
"isAbstract": false,
"isConstructor": false,
"isGetter": false,
"isField": true,
"isMethod": false,
Expand All @@ -45,6 +60,19 @@
"instantiation": []
}
}
},
"": {
"properties": {
"isAbstract": false,
"isConstructor": true,
"isGetter": false,
"isField": false,
"isMethod": false,
"isStatic": false
},
"requiredPositionalParameters": [],
"optionalPositionalParameters": [],
"namedParameters": []
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions goldens/foo/lib/json_codable_test.analyzer.augmentations
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ json[r'x'] = x;
return json;
}

}
augment class D {
// TODO(davidmorgan): see https://github.com/dart-lang/macros/issues/80.
// external D.fromJson(prefix0.Map<prefix0.String, prefix0.Object?> json);
// external prefix0.Map<prefix0.String, prefix0.Object?> toJson();

augment D.fromJson(prefix0.Map<prefix0.String, prefix0.Object?> json) :
y = json[r'y'] as prefix0.String,
super.fromJson(json);

augment prefix0.Map<prefix0.String, prefix0.Object?> toJson() {
final json = super.toJson();
json[r'y'] = y;

return json;
}

}
augment class E {
// TODO(davidmorgan): see https://github.com/dart-lang/macros/issues/80.
Expand Down
18 changes: 14 additions & 4 deletions goldens/foo/lib/json_codable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ void main() {
expect(b.toJson(), isEmpty);
});

/* TODO(davidmorgan): make this test work.
test('class hierarchies', () {
var json = {
'x': 1,
Expand All @@ -118,7 +117,6 @@ void main() {

expect(d.toJson(), equals(json));
});
*/

test('collections of nullable objects', () {
var json = {
Expand Down Expand Up @@ -266,7 +264,6 @@ class C {
final int x;
}

/*
@JsonCodable()
class D extends C {
// TODO(davidmorgan): see https://github.com/dart-lang/macros/issues/80.
Expand All @@ -275,7 +272,6 @@ class D extends C {

final String y;
}
*/

@JsonCodable()
class E {
Expand Down Expand Up @@ -310,3 +306,17 @@ class F {

final int fieldWithDollarSign$;
}














63 changes: 63 additions & 0 deletions pkgs/_analyzer_macros/lib/query_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,49 @@ class AnalyzerQueryService implements QueryService {
interface.members[field.name] = Member(
properties: Properties(
isAbstract: field.isAbstract,
isConstructor: false,
isGetter: false,
isField: true,
isMethod: false,
isStatic: field.isStatic,
),
returnType: types.addDartType(field.type));
}
for (final constructor in clazz.constructors) {
interface.members[constructor.name] = Member(
requiredPositionalParameters: constructor
.requiredPositionalParameters(types.translator, types.context),
optionalPositionalParameters: constructor
.optionalPositionalParameters(types.translator, types.context),
namedParameters:
constructor.namedParameters(types.translator, types.context),
properties: Properties(
isAbstract: constructor.isAbstract,
isConstructor: true,
isGetter: false,
isField: false,
isMethod: false,
isStatic: false,
));
}
for (final method in clazz.methods) {
interface.members[method.name] = Member(
requiredPositionalParameters: method.requiredPositionalParameters(
types.translator, types.context),
optionalPositionalParameters: method.optionalPositionalParameters(
types.translator, types.context),
namedParameters:
method.namedParameters(types.translator, types.context),
properties: Properties(
isAbstract: method.isAbstract,
isConstructor: false,
isGetter: false,
isField: false,
isMethod: true,
isStatic: method.isStatic,
),
returnType: types.addDartType(method.returnType));
}
return Model(types: types.typeHierarchy)
..uris[uri] = (Library()..scopes[clazz.name] = interface);
}
Expand Down Expand Up @@ -105,3 +141,30 @@ class AnalyzerTypeHierarchy {
);
}
}

extension ExecutableElementExtension on ExecutableElement {
List<StaticTypeDesc> requiredPositionalParameters(
AnalyzerTypesToMacros translator, TypeTranslationContext context) {
return parameters
.where((p) => p.isRequiredPositional)
.map((p) => p.type.acceptWithArgument(translator, context))
.toList();
}

List<StaticTypeDesc> optionalPositionalParameters(
AnalyzerTypesToMacros translator, TypeTranslationContext context) {
return parameters
.where((p) => p.isRequiredPositional)
.map((p) => p.type.acceptWithArgument(translator, context))
.toList();
}

List<NamedFunctionTypeParameter> namedParameters(
AnalyzerTypesToMacros translator, TypeTranslationContext context) {
return parameters
.where((p) => p.isNamed)
.map((p) => p.type.acceptWithArgument(translator, context))
.cast<NamedFunctionTypeParameter>()
.toList();
}
}
75 changes: 74 additions & 1 deletion pkgs/_test_macros/lib/json_codable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,48 @@ class JsonCodableImplementation implements Macro {
final model = await host.query(Query(target: target));
final clazz = model.uris[target.uri]!.scopes[target.name]!;
// TODO(davidmorgan): check for super `fromJson`.

MacroScope.current.addModel(model);
final superclassName =
MacroScope.current.typeSystem.lookupSupertype(target);
var superclassHasFromJson = false;
if (superclassName.asString != 'dart:core#Object') {
final supermodel = await host.query(Query(target: superclassName));
final superclass =
supermodel.uris[superclassName.uri]!.scopes[superclassName.name]!;
final constructor = superclass.members['fromJson'];
final memberIsValid = constructor != null &&
constructor.properties.isConstructor &&
constructor.requiredPositionalParameters.length == 1 &&
constructor.requiredPositionalParameters[0].type ==
StaticTypeDescType.namedTypeDesc &&
constructor.requiredPositionalParameters[0].asNamedTypeDesc.name
.asString ==
'dart:core#Map' &&
constructor.requiredPositionalParameters[0].asNamedTypeDesc
.instantiation[0].asNamedTypeDesc.name.asString ==
'dart:core#String' &&
constructor
.requiredPositionalParameters[0]
.asNamedTypeDesc
.instantiation[1]
.asNullableTypeDesc
.inner
.asNamedTypeDesc
.name
.asString ==
'dart:core#Object';
if (memberIsValid) {
superclassHasFromJson = true;
} else {
// TODO(davidmorgan): report as a diagnostic.
throw ArgumentError(
'Serialization of classes that extend other classes is only '
'supported if those classes have a valid '
'`fromJson(Map<String, Object?> json)` constructor.');
}
}

final initializers = <String>[];
for (final field
in clazz.members.entries.where((m) => m.value.properties.isField)) {
Expand All @@ -59,13 +101,42 @@ class JsonCodableImplementation implements Macro {
.add('$name = ${_convertTypeFromJson("json[r'$name']", type)}');
}

if (superclassHasFromJson) {
initializers.add('super.fromJson(json)');
}

// TODO(davidmorgan): helper for augmenting initializers.
// See: https://github.com/dart-lang/sdk/blob/main/pkg/_macros/lib/src/executor/builder_impls.dart#L500
result.add(Augmentation(code: '''
augment ${target.name}.fromJson($_jsonMapType json) :
${initializers.join(',\n')};
'''));

var superclassHasToJson = false;
if (superclassName.asString != 'dart:core#Object') {
final supermodel = await host.query(Query(target: superclassName));
final superclass =
supermodel.uris[superclassName.uri]!.scopes[superclassName.name]!;
final method = superclass.members['toJson'];
final memberIsValid = method != null &&
method.properties.isMethod &&
!method.properties.isStatic &&
method.requiredPositionalParameters.isEmpty &&
method.optionalPositionalParameters.isEmpty &&
method.namedParameters.isEmpty &&
// TODO(davidmorgan): remainder of type check.
method.returnType.asNamedTypeDesc.name.asString == 'dart:core#Map';
if (memberIsValid) {
superclassHasToJson = true;
} else {
// TODO(davidmorgan): report as a diagnostic.
throw ArgumentError(
'Serialization of classes that extend other classes is only '
'supported if those classes have a valid '
'`Map<String, Object?> json toJson()` method.');
}
}

final serializers = <String>[];
for (final field
in clazz.members.entries.where((m) => m.value.properties.isField)) {
Expand All @@ -80,9 +151,11 @@ ${initializers.join(',\n')};

// TODO(davidmorgan): helper for augmenting methods.
// See: https://github.com/dart-lang/sdk/blob/main/pkg/_macros/lib/src/executor/builder_impls.dart#L500
final jsonInitializer =
superclassHasToJson ? 'super.toJson()' : '$_jsonMapTypeForLiteral{}';
result.add(Augmentation(code: '''
augment $_jsonMapType toJson() {
final json = $_jsonMapTypeForLiteral{};
final json = $jsonInitializer;
${serializers.join('')}
return json;
}
Expand Down
33 changes: 33 additions & 0 deletions pkgs/dart_model/lib/src/dart_model.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,21 +138,48 @@ extension type Member.fromJson(Map<String, Object?> node) implements Object {
static final TypedMapSchema _schema = TypedMapSchema({
'properties': Type.typedMapPointer,
'returnType': Type.typedMapPointer,
'requiredPositionalParameters': Type.closedListPointer,
'optionalPositionalParameters': Type.closedListPointer,
'namedParameters': Type.closedListPointer,
'parameters': Type.typedMapPointer,
});
Member({
Properties? properties,
StaticTypeDesc? returnType,
List<StaticTypeDesc>? requiredPositionalParameters,
List<StaticTypeDesc>? optionalPositionalParameters,
List<NamedFunctionTypeParameter>? namedParameters,
Properties? parameters,
}) : this.fromJson(Scope.createMap(
_schema,
properties,
returnType,
requiredPositionalParameters,
optionalPositionalParameters,
namedParameters,
parameters,
));

/// The properties of this member.
Properties get properties => node['properties'] as Properties;

/// The return type of this member, if it has one.
StaticTypeDesc get returnType => node['returnType'] as StaticTypeDesc;

/// The required positional parameters of this member, if it has them.
List<StaticTypeDesc> get requiredPositionalParameters =>
(node['requiredPositionalParameters'] as List).cast();

/// The optional positional parameters of this member, if it has them.
List<StaticTypeDesc> get optionalPositionalParameters =>
(node['optionalPositionalParameters'] as List).cast();

/// The named positional parameters of this member, if it has them.
List<NamedFunctionTypeParameter> get namedParameters =>
(node['namedParameters'] as List).cast();

/// The properties of this member.
Properties get parameters => node['parameters'] as Properties;
}

/// Partial model of a corpus of Dart source code.
Expand Down Expand Up @@ -266,6 +293,7 @@ extension type Properties.fromJson(Map<String, Object?> node)
static final TypedMapSchema _schema = TypedMapSchema({
'isAbstract': Type.boolean,
'isClass': Type.boolean,
'isConstructor': Type.boolean,
'isGetter': Type.boolean,
'isField': Type.boolean,
'isMethod': Type.boolean,
Expand All @@ -274,6 +302,7 @@ extension type Properties.fromJson(Map<String, Object?> node)
Properties({
bool? isAbstract,
bool? isClass,
bool? isConstructor,
bool? isGetter,
bool? isField,
bool? isMethod,
Expand All @@ -282,6 +311,7 @@ extension type Properties.fromJson(Map<String, Object?> node)
_schema,
isAbstract,
isClass,
isConstructor,
isGetter,
isField,
isMethod,
Expand All @@ -294,6 +324,9 @@ extension type Properties.fromJson(Map<String, Object?> node)
/// Whether the entity is a class.
bool get isClass => node['isClass'] as bool;

/// Whether the entity is a constructor.
bool get isConstructor => node['isConstructor'] as bool;

/// Whether the entity is a getter.
bool get isGetter => node['isGetter'] as bool;

Expand Down
Loading

0 comments on commit dbc871e

Please sign in to comment.