diff --git a/goldens/foo/lib/foo.analyzer.json b/goldens/foo/lib/foo.analyzer.json index 4f908f41..3a26f5cb 100644 --- a/goldens/foo/lib/foo.analyzer.json +++ b/goldens/foo/lib/foo.analyzer.json @@ -4,9 +4,85 @@ "scopes": { "Foo": { "members": { + "construct": { + "properties": { + "isAbstract": false, + "isConstructor": true, + "isGetter": false, + "isField": false, + "isMethod": false, + "isStatic": false + }, + "requiredPositionalParameters": [ + { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "int" + }, + "instantiation": [] + } + } + ], + "optionalPositionalParameters": [], + "namedParameters": [ + { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "int" + }, + "instantiation": [] + } + } + ] + }, + "construct2": { + "properties": { + "isAbstract": false, + "isConstructor": true, + "isGetter": false, + "isField": false, + "isMethod": false, + "isStatic": false + }, + "requiredPositionalParameters": [ + { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "int" + }, + "instantiation": [] + } + } + ], + "optionalPositionalParameters": [ + { + "type": "NullableTypeDesc", + "value": { + "inner": { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "String" + }, + "instantiation": [] + } + } + } + } + ], + "namedParameters": [] + }, "bar": { "properties": { "isAbstract": false, + "isConstructor": false, "isGetter": false, "isField": true, "isMethod": false, @@ -22,14 +98,126 @@ "instantiation": [] } } + }, + "method": { + "properties": { + "isAbstract": false, + "isConstructor": false, + "isGetter": false, + "isField": false, + "isMethod": true, + "isStatic": false + }, + "returnType": { + "type": "VoidTypeDesc" + }, + "requiredPositionalParameters": [ + { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "int" + }, + "instantiation": [] + } + } + ], + "optionalPositionalParameters": [], + "namedParameters": [ + { + "type": "NullableTypeDesc", + "value": { + "inner": { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "int" + }, + "instantiation": [] + } + } + } + } + ] + }, + "method2": { + "properties": { + "isAbstract": false, + "isConstructor": false, + "isGetter": false, + "isField": false, + "isMethod": true, + "isStatic": false + }, + "returnType": { + "type": "NullableTypeDesc", + "value": { + "inner": { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "package:foo/foo.dart", + "name": "Bar" + }, + "instantiation": [] + } + } + } + }, + "requiredPositionalParameters": [ + { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "int" + }, + "instantiation": [] + } + } + ], + "optionalPositionalParameters": [ + { + "type": "NullableTypeDesc", + "value": { + "inner": { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "String" + }, + "instantiation": [] + } + } + } + } + ], + "namedParameters": [] } } }, "Bar": { "members": { + "": { + "properties": { + "isAbstract": false, + "isConstructor": true, + "isGetter": false, + "isField": false, + "isMethod": false, + "isStatic": false + }, + "requiredPositionalParameters": [], + "optionalPositionalParameters": [], + "namedParameters": [] + }, "bar": { "properties": { "isAbstract": false, + "isConstructor": false, "isGetter": false, "isField": true, "isMethod": false, diff --git a/goldens/foo/lib/foo.dart b/goldens/foo/lib/foo.dart index 066d73b3..537e8732 100644 --- a/goldens/foo/lib/foo.dart +++ b/goldens/foo/lib/foo.dart @@ -7,6 +7,14 @@ import 'package:_test_macros/query_class.dart'; @QueryClass() class Foo { final int bar = 3; + + Foo.construct(int x, {required int y}); + Foo.construct2(int x, [String? y]); + + void method(int x, {int? y}) {} + Bar? method2(int x, [String? y]) { + return null; + } } @QueryClass() 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..9675ccae 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 { diff --git a/pkgs/_analyzer_macros/lib/query_service.dart b/pkgs/_analyzer_macros/lib/query_service.dart index 47fa28f7..66af2894 100644 --- a/pkgs/_analyzer_macros/lib/query_service.dart +++ b/pkgs/_analyzer_macros/lib/query_service.dart @@ -36,10 +36,28 @@ class AnalyzerQueryService implements QueryService { ..addInterfaceElement(clazz); final interface = Interface(); + 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 field in clazz.fields) { interface.members[field.name] = Member( properties: Properties( isAbstract: field.isAbstract, + isConstructor: false, isGetter: false, isField: true, isMethod: false, @@ -47,6 +65,24 @@ class AnalyzerQueryService implements QueryService { ), returnType: types.addDartType(field.type)); } + 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) => + [ + for (final parameter in parameters) + if (parameter.isRequiredPositional) + parameter.type.acceptWithArgument(translator, context) + ]; + + List optionalPositionalParameters( + AnalyzerTypesToMacros translator, TypeTranslationContext context) => + [ + for (final parameter in parameters) + if (parameter.isOptionalPositional) + parameter.type.acceptWithArgument(translator, context) + ]; + + List namedParameters( + AnalyzerTypesToMacros translator, TypeTranslationContext context) => + [ + for (final parameter in parameters) + if (parameter.isNamed) + parameter.type.acceptWithArgument(translator, context) + as NamedFunctionTypeParameter + ]; +} diff --git a/pkgs/_macro_tool/lib/macro_tool.dart b/pkgs/_macro_tool/lib/macro_tool.dart index 2917877f..4da22dae 100644 --- a/pkgs/_macro_tool/lib/macro_tool.dart +++ b/pkgs/_macro_tool/lib/macro_tool.dart @@ -127,11 +127,8 @@ abstract class MacroTool { } /// Returns [source] with lines added by [_addImportAugment] removed. - String _removeToolAddedLinesFromSource(String source) => source - .split('\n') - .where((l) => !l.endsWith(_addedMarker)) - .map((l) => '$l\n') - .join(); + String _removeToolAddedLinesFromSource(String source) => + source.split('\n').where((l) => !l.endsWith(_addedMarker)).join('\n'); } final String _addedMarker = '// added by macro_tool'; diff --git a/pkgs/_test_macros/lib/json_codable.dart b/pkgs/_test_macros/lib/json_codable.dart index 919ec1df..3cffb313 100644 --- a/pkgs/_test_macros/lib/json_codable.dart +++ b/pkgs/_test_macros/lib/json_codable.dart @@ -46,12 +46,45 @@ class JsonCodableImplementation implements Macro { } Future phase3(Host host, AugmentRequest request) async { - final result = []; - final target = request.target; final model = await host.query(Query(target: target)); final clazz = model.uris[target.uri]!.scopes[target.name]!; - // TODO(davidmorgan): check for super `fromJson`. + + // TODO(davidmorgan): put `extends` information directly in `Interface`. + final superclassName = MacroScope.current.typeSystem.supertypeOf(target); + + return AugmentResponse(augmentations: [ + await _generateFromJson(host, model, target, superclassName, clazz), + await _generateToJson(host, model, target, superclassName, clazz) + ]); + } + + Future _generateFromJson( + Host host, + Model model, + QualifiedName target, + QualifiedName superclassName, + Interface clazz, + ) async { + var superclassHasFromJson = false; + // TODO(davidmorgan): add recommended way to check for core types. + if (superclassName.asString != 'dart:core#Object') { + // TODO(davidmorgan): first query could already fetch the super class. + final supermodel = await host.query(Query(target: superclassName)); + final superclass = + supermodel.uris[superclassName.uri]!.scopes[superclassName.name]!; + final constructor = superclass.members['fromJson']; + if (constructor != null && _isValidFromJsonConstructor(constructor)) { + 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)) { @@ -61,12 +94,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: expandTemplate(''' + return Augmentation(code: expandTemplate(''' augment ${target.name}.fromJson($_jsonMapType json) : ${initializers.join(',\n')}; -'''))); +''')); + } + + Future _generateToJson( + Host host, + Model model, + QualifiedName target, + QualifiedName superclassName, + Interface clazz, + ) async { + var superclassHasToJson = false; + if (superclassName.asString != 'dart:core#Object') { + // TODO(davidmorgan): first query could already fetch the super class. + final supermodel = await host.query(Query(target: superclassName)); + final superclass = + supermodel.uris[superclassName.uri]!.scopes[superclassName.name]!; + final method = superclass.members['toJson']; + if (method != null && _isValidToJsonMethod(method)) { + 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 @@ -82,17 +145,49 @@ ${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 - result.add(Augmentation(code: expandTemplate(''' + final jsonInitializer = + superclassHasToJson ? 'super.toJson()' : '$_jsonMapTypeForLiteral{}'; + return Augmentation(code: expandTemplate(''' augment $_jsonMapType toJson() { - final json = $_jsonMapTypeForLiteral{}; + final json = $jsonInitializer; ${serializers.join('')} return json; } -'''))); - - return AugmentResponse(augmentations: result); +''')); } + /// Returns whether [constructor] is a constructor + /// `fromJson(Map)`. + bool _isValidFromJsonConstructor(Member constructor) => + constructor.properties.isConstructor && + constructor.optionalPositionalParameters.isEmpty && + constructor.namedParameters.isEmpty && + constructor.requiredPositionalParameters.length == 1 && + constructor.requiredPositionalParameters[0].type == + StaticTypeDescType.namedTypeDesc && + _isJsonMapType( + constructor.requiredPositionalParameters[0].asNamedTypeDesc); + + /// Returns whether [method] is a method + /// `toJson(Map)`. + bool _isValidToJsonMethod(Member method) => + method.properties.isMethod && + !method.properties.isStatic && + method.requiredPositionalParameters.isEmpty && + method.optionalPositionalParameters.isEmpty && + method.namedParameters.isEmpty && + _isJsonMapType(method.returnType.asNamedTypeDesc); + + /// Returns whether [type] is a type `Map)`. + bool _isJsonMapType(NamedTypeDesc type) => + type.name.asString == 'dart:core#Map' && + type.instantiation[0].asNamedTypeDesc.name.asString == + 'dart:core#String' && + type.instantiation[1].type == StaticTypeDescType.nullableTypeDesc && + type.instantiation[1].asNullableTypeDesc.inner.asNamedTypeDesc.name + .asString == + 'dart:core#Object'; + String _convertTypeFromJson(String reference, StaticTypeDesc type) { // TODO(davidmorgan): _checkNamedType equivalent. // TODO(davidmorgan): should this code use `StaticType` and related classes diff --git a/pkgs/dart_model/lib/src/dart_model.g.dart b/pkgs/dart_model/lib/src/dart_model.g.dart index a23d2826..49d55610 100644 --- a/pkgs/dart_model/lib/src/dart_model.g.dart +++ b/pkgs/dart_model/lib/src/dart_model.g.dart @@ -189,14 +189,23 @@ 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, }); Member({ Properties? properties, StaticTypeDesc? returnType, + List? requiredPositionalParameters, + List? optionalPositionalParameters, + List? namedParameters, }) : this.fromJson(Scope.createMap( _schema, properties, returnType, + requiredPositionalParameters, + optionalPositionalParameters, + namedParameters, )); /// The properties of this member. @@ -204,6 +213,18 @@ 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 parameters of this member, if it has them. + List get namedParameters => + (node['namedParameters'] as List).cast(); } /// Partial model of a corpus of Dart source code. @@ -317,6 +338,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, @@ -325,6 +347,7 @@ extension type Properties.fromJson(Map node) Properties({ bool? isAbstract, bool? isClass, + bool? isConstructor, bool? isGetter, bool? isField, bool? isMethod, @@ -333,6 +356,7 @@ extension type Properties.fromJson(Map node) _schema, isAbstract, isClass, + isConstructor, isGetter, isField, isMethod, @@ -345,6 +369,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..9265d585 100644 --- a/pkgs/dart_model/lib/src/type_system.dart +++ b/pkgs/dart_model/lib/src/type_system.dart @@ -61,6 +61,15 @@ final class StaticTypeSystem { return Scope.none.run(() => _isSubtype(a, b)); } + /// Returns the super type of the type referred to by [name], which must be + /// part of the model. + /// + /// TODO(davidmorgan): this is a hack to check "extends" but is not correct, + /// see https://github.com/dart-lang/macros/pull/89#discussion_r1791855869 + QualifiedName supertypeOf(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 c941b56d..cd2128ce 100644 --- a/schemas/dart_model.schema.json +++ b/schemas/dart_model.schema.json @@ -143,6 +143,27 @@ "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 parameters of this member, if it has them.", + "items": { + "$ref": "#/$defs/NamedFunctionTypeParameter" + } } } }, @@ -231,6 +252,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 b0ddb9f3..fb0a4639 100644 --- a/tool/dart_model_generator/lib/definitions.dart +++ b/tool/dart_model_generator/lib/definitions.dart @@ -150,6 +150,22 @@ static Protocol handshakeProtocol = Protocol( type: 'StaticTypeDesc', description: 'The return type of this member, if it has one.', ), + // TODO(davidmorgan): base on + // https://github.com/dart-lang/sdk/blob/main/pkg/_macros/lib/src/api/introspection.dart#L269 + 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 parameters of this member, ' + 'if it has them.'), ]), Definition.clazz('Model', description: 'Partial model of a corpus of Dart source code.', @@ -208,6 +224,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',