diff --git a/goldens/foo/lib/foo.analyzer.json b/goldens/foo/lib/foo.analyzer.json index 4f908f41..5171d9db 100644 --- a/goldens/foo/lib/foo.analyzer.json +++ b/goldens/foo/lib/foo.analyzer.json @@ -7,6 +7,7 @@ "bar": { "properties": { "isAbstract": false, + "isConstructor": false, "isGetter": false, "isField": true, "isMethod": false, @@ -22,6 +23,19 @@ "instantiation": [] } } + }, + "": { + "properties": { + "isAbstract": false, + "isConstructor": true, + "isGetter": false, + "isField": false, + "isMethod": false, + "isStatic": false + }, + "requiredPositionalParameters": [], + "optionalPositionalParameters": [], + "namedParameters": [] } } }, @@ -30,6 +44,7 @@ "bar": { "properties": { "isAbstract": false, + "isConstructor": false, "isGetter": false, "isField": true, "isMethod": false, @@ -45,6 +60,19 @@ "instantiation": [] } } + }, + "": { + "properties": { + "isAbstract": false, + "isConstructor": true, + "isGetter": false, + "isField": false, + "isMethod": false, + "isStatic": false + }, + "requiredPositionalParameters": [], + "optionalPositionalParameters": [], + "namedParameters": [] } } } diff --git a/goldens/foo/lib/json_codable_test.analyzer.augmentations b/goldens/foo/lib/json_codable_test.analyzer.augmentations index bd183bc3..d85bbd3c 100644 --- a/goldens/foo/lib/json_codable_test.analyzer.augmentations +++ b/goldens/foo/lib/json_codable_test.analyzer.augmentations @@ -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 json); +// external prefix0.Map toJson(); + +augment D.fromJson(prefix0.Map json) : +y = json[r'y'] as prefix0.String, +super.fromJson(json); + +augment prefix0.Map 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. diff --git a/goldens/foo/lib/json_codable_test.dart b/goldens/foo/lib/json_codable_test.dart index a09a662b..4d4eb8b0 100644 --- a/goldens/foo/lib/json_codable_test.dart +++ b/goldens/foo/lib/json_codable_test.dart @@ -106,7 +106,6 @@ void main() { expect(b.toJson(), isEmpty); }); - /* TODO(davidmorgan): make this test work. test('class hierarchies', () { var json = { 'x': 1, @@ -118,7 +117,6 @@ void main() { expect(d.toJson(), equals(json)); }); - */ test('collections of nullable objects', () { var json = { @@ -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. @@ -275,7 +272,6 @@ class D extends C { final String y; } -*/ @JsonCodable() class E { @@ -310,3 +306,17 @@ class F { final int fieldWithDollarSign$; } + + + + + + + + + + + + + + diff --git a/pkgs/_analyzer_macros/lib/query_service.dart b/pkgs/_analyzer_macros/lib/query_service.dart index 47fa28f7..5c3bfa37 100644 --- a/pkgs/_analyzer_macros/lib/query_service.dart +++ b/pkgs/_analyzer_macros/lib/query_service.dart @@ -40,6 +40,7 @@ class AnalyzerQueryService implements QueryService { interface.members[field.name] = Member( properties: Properties( isAbstract: field.isAbstract, + isConstructor: false, isGetter: false, isField: true, isMethod: false, @@ -47,6 +48,41 @@ class AnalyzerQueryService implements QueryService { ), 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); } @@ -105,3 +141,30 @@ class AnalyzerTypeHierarchy { ); } } + +extension ExecutableElementExtension on ExecutableElement { + List requiredPositionalParameters( + AnalyzerTypesToMacros translator, TypeTranslationContext context) { + return parameters + .where((p) => p.isRequiredPositional) + .map((p) => p.type.acceptWithArgument(translator, context)) + .toList(); + } + + List optionalPositionalParameters( + AnalyzerTypesToMacros translator, TypeTranslationContext context) { + return parameters + .where((p) => p.isRequiredPositional) + .map((p) => p.type.acceptWithArgument(translator, context)) + .toList(); + } + + List namedParameters( + AnalyzerTypesToMacros translator, TypeTranslationContext context) { + return parameters + .where((p) => p.isNamed) + .map((p) => p.type.acceptWithArgument(translator, context)) + .cast() + .toList(); + } +} diff --git a/pkgs/_test_macros/lib/json_codable.dart b/pkgs/_test_macros/lib/json_codable.dart index 874f0bad..a6f63951 100644 --- a/pkgs/_test_macros/lib/json_codable.dart +++ b/pkgs/_test_macros/lib/json_codable.dart @@ -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 json)` constructor.'); + } + } + final initializers = []; for (final field in clazz.members.entries.where((m) => m.value.properties.isField)) { @@ -59,6 +101,10 @@ 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: ''' @@ -66,6 +112,31 @@ 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 json toJson()` method.'); + } + } + final serializers = []; for (final field in clazz.members.entries.where((m) => m.value.properties.isField)) { @@ -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; } diff --git a/pkgs/dart_model/lib/src/dart_model.g.dart b/pkgs/dart_model/lib/src/dart_model.g.dart index 8a7b4f30..948b628e 100644 --- a/pkgs/dart_model/lib/src/dart_model.g.dart +++ b/pkgs/dart_model/lib/src/dart_model.g.dart @@ -138,14 +138,26 @@ extension type Member.fromJson(Map 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? requiredPositionalParameters, + List? optionalPositionalParameters, + List? namedParameters, + Properties? parameters, }) : this.fromJson(Scope.createMap( _schema, properties, returnType, + requiredPositionalParameters, + optionalPositionalParameters, + namedParameters, + parameters, )); /// The properties of this member. @@ -153,6 +165,21 @@ extension type Member.fromJson(Map node) implements Object { /// 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 get requiredPositionalParameters => + (node['requiredPositionalParameters'] as List).cast(); + + /// The optional positional parameters of this member, if it has them. + List get optionalPositionalParameters => + (node['optionalPositionalParameters'] as List).cast(); + + /// The named positional parameters of this member, if it has them. + List 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. @@ -266,6 +293,7 @@ extension type Properties.fromJson(Map node) static final TypedMapSchema _schema = TypedMapSchema({ 'isAbstract': Type.boolean, 'isClass': Type.boolean, + 'isConstructor': Type.boolean, 'isGetter': Type.boolean, 'isField': Type.boolean, 'isMethod': Type.boolean, @@ -274,6 +302,7 @@ extension type Properties.fromJson(Map node) Properties({ bool? isAbstract, bool? isClass, + bool? isConstructor, bool? isGetter, bool? isField, bool? isMethod, @@ -282,6 +311,7 @@ extension type Properties.fromJson(Map node) _schema, isAbstract, isClass, + isConstructor, isGetter, isField, isMethod, @@ -294,6 +324,9 @@ extension type Properties.fromJson(Map 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; diff --git a/pkgs/dart_model/lib/src/type_system.dart b/pkgs/dart_model/lib/src/type_system.dart index 9bd094f5..932edd78 100644 --- a/pkgs/dart_model/lib/src/type_system.dart +++ b/pkgs/dart_model/lib/src/type_system.dart @@ -61,6 +61,10 @@ final class StaticTypeSystem { return Scope.none.run(() => _isSubtype(a, b)); } + QualifiedName lookupSupertype(QualifiedName name) { + return _constructSuperTypes(_lookupNamed(name.asString), []).first.name; + } + bool _isSubtype(StaticType a, StaticType b) { // This is using `T0` and `T1` names to make the implementation easier to // compare with the specification at diff --git a/schemas/dart_model.schema.json b/schemas/dart_model.schema.json index 18e1e6fb..787e4c90 100644 --- a/schemas/dart_model.schema.json +++ b/schemas/dart_model.schema.json @@ -117,6 +117,31 @@ "returnType": { "$comment": "The return type of this member, if it has one.", "$ref": "#/$defs/StaticTypeDesc" + }, + "requiredPositionalParameters": { + "type": "array", + "description": "The required positional parameters of this member, if it has them.", + "items": { + "$ref": "#/$defs/StaticTypeDesc" + } + }, + "optionalPositionalParameters": { + "type": "array", + "description": "The optional positional parameters of this member, if it has them.", + "items": { + "$ref": "#/$defs/StaticTypeDesc" + } + }, + "namedParameters": { + "type": "array", + "description": "The named positional parameters of this member, if it has them.", + "items": { + "$ref": "#/$defs/NamedFunctionTypeParameter" + } + }, + "parameters": { + "$comment": "The properties of this member.", + "$ref": "#/$defs/Properties" } } }, @@ -205,6 +230,10 @@ "type": "boolean", "description": "Whether the entity is a class." }, + "isConstructor": { + "type": "boolean", + "description": "Whether the entity is a constructor." + }, "isGetter": { "type": "boolean", "description": "Whether the entity is a getter." diff --git a/tool/dart_model_generator/lib/definitions.dart b/tool/dart_model_generator/lib/definitions.dart index 90055fcd..9cbdb08b 100644 --- a/tool/dart_model_generator/lib/definitions.dart +++ b/tool/dart_model_generator/lib/definitions.dart @@ -143,6 +143,24 @@ static Protocol handshakeProtocol = Protocol( type: 'StaticTypeDesc', description: 'The return type of this member, if it has one.', ), + Property('requiredPositionalParameters', + type: 'List', + description: + 'The required positional parameters of this member, ' + 'if it has them.'), + Property('optionalPositionalParameters', + type: 'List', + description: + 'The optional positional parameters of this member, ' + 'if it has them.'), + Property('namedParameters', + type: 'List', + description: + 'The named positional parameters of this member, ' + 'if it has them.'), + Property('parameters', + type: 'Properties', + description: 'The properties of this member.'), ]), Definition.clazz('Model', description: 'Partial model of a corpus of Dart source code.', @@ -201,6 +219,9 @@ static Protocol handshakeProtocol = Protocol( 'definition.'), Property('isClass', type: 'bool', description: 'Whether the entity is a class.'), + Property('isConstructor', + type: 'bool', + description: 'Whether the entity is a constructor.'), Property('isGetter', type: 'bool', description: 'Whether the entity is a getter.'), Property('isField',