From 79fa863d0c5a712e90d9ee2909d99548fed8dfee Mon Sep 17 00:00:00 2001 From: David Morgan Date: Tue, 10 Sep 2024 14:29:12 +0200 Subject: [PATCH 1/3] Add a first JSON macro based on package:json/json.dart. --- goldens/foo/lib/foo.analyzer.json | 58 ++++-- .../lib/json_codable.analyzer.augmentations | 59 ++++++ goldens/foo/lib/json_codable.dart | 45 +++++ .../lib/macro_implementation.dart | 70 ++++++-- pkgs/_analyzer_macros/lib/query_service.dart | 105 ++++++----- pkgs/_analyzer_macros/test/golden_test.dart | 9 +- pkgs/_cfe_macros/test/golden_test.dart | 9 +- pkgs/_test_macros/lib/json_codable.dart | 170 ++++++++++++++++++ pkgs/_test_macros/pubspec.yaml | 1 + pkgs/dart_model/lib/src/dart_model.g.dart | 6 + schemas/dart_model.schema.json | 4 + .../dart_model_generator/lib/definitions.dart | 5 + 12 files changed, 453 insertions(+), 88 deletions(-) create mode 100644 goldens/foo/lib/json_codable.analyzer.augmentations create mode 100644 goldens/foo/lib/json_codable.dart create mode 100644 pkgs/_test_macros/lib/json_codable.dart diff --git a/goldens/foo/lib/foo.analyzer.json b/goldens/foo/lib/foo.analyzer.json index 922c4fcc..4f908f41 100644 --- a/goldens/foo/lib/foo.analyzer.json +++ b/goldens/foo/lib/foo.analyzer.json @@ -11,6 +11,16 @@ "isField": true, "isMethod": false, "isStatic": false + }, + "returnType": { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "int" + }, + "instantiation": [] + } } } } @@ -24,6 +34,16 @@ "isField": true, "isMethod": false, "isStatic": false + }, + "returnType": { + "type": "NamedTypeDesc", + "value": { + "name": { + "uri": "dart:core", + "name": "int" + }, + "instantiation": [] + } } } } @@ -33,25 +53,6 @@ }, "types": { "named": { - "package:foo/foo.dart#Foo": { - "typeParameters": [], - "self": { - "name": { - "uri": "package:foo/foo.dart", - "name": "Foo" - }, - "instantiation": [] - }, - "supertypes": [ - { - "name": { - "uri": "dart:core", - "name": "Object" - }, - "instantiation": [] - } - ] - }, "dart:core#Object": { "typeParameters": [], "self": { @@ -112,6 +113,25 @@ } ] }, + "package:foo/foo.dart#Foo": { + "typeParameters": [], + "self": { + "name": { + "uri": "package:foo/foo.dart", + "name": "Foo" + }, + "instantiation": [] + }, + "supertypes": [ + { + "name": { + "uri": "dart:core", + "name": "Object" + }, + "instantiation": [] + } + ] + }, "package:foo/foo.dart#Bar": { "typeParameters": [], "self": { diff --git a/goldens/foo/lib/json_codable.analyzer.augmentations b/goldens/foo/lib/json_codable.analyzer.augmentations new file mode 100644 index 00000000..046e98d7 --- /dev/null +++ b/goldens/foo/lib/json_codable.analyzer.augmentations @@ -0,0 +1,59 @@ +part of 'package:foo/json_codable.dart'; + +import 'package:foo/json_codable.dart' as prefix0; +import 'dart:core' as prefix1; + +augment class A { +external prefix0.A.fromJson(prefix1.Map json); +external prefix1.Map toJson(); + +augment prefix0.A.fromJson(prefix1.Map json) : +boolField = json[r'boolField'] as prefix1.bool, +nullableBoolField = json[r'nullableBoolField'] as prefix1.bool?, +stringField = json[r'stringField'] as prefix1.String, +nullableStringField = json[r'nullableStringField'] as prefix1.String?, +intField = json[r'intField'] as prefix1.int, +nullableIntField = json[r'nullableIntField'] as prefix1.int?, +doubleField = json[r'doubleField'] as prefix1.double, +nullableDoubleField = json[r'nullableDoubleField'] as prefix1.double?, +numField = json[r'numField'] as prefix1.num, +nullableNumField = json[r'nullableNumField'] as prefix1.num?, +listOfSerializableField = json[r'listOfSerializableField'] == null ? null : [for (final item in json[r'listOfSerializableField'] as prefix1.List) item == null ? null : prefix0.C.fromJson(item as prefix1.Map], +nullableListOfSerializableField = [for (final item in json[r'nullableListOfSerializableField'] as prefix1.List) item == null ? null : prefix0.C.fromJson(item as prefix1.Map], +setOfSerializableField = json[r'setOfSerializableField'] == null ? null : {for (final item in json[r'setOfSerializableField'] as prefix1.Set) item == null ? null : prefix0.C.fromJson(item as prefix1.Map}, +nullableSetOfSerializableField = {for (final item in json[r'nullableSetOfSerializableField'] as prefix1.Set) item == null ? null : prefix0.C.fromJson(item as prefix1.Map}, +mapOfSerializableField = json[r'mapOfSerializableField'] == null ? null : {for (final (:key, :value) in json[r'mapOfSerializableField'] as prefix1.Map) key: value == null ? null : prefix0.C.fromJson(value as prefix1.Map}, +nullableMapOfSerializableField = {for (final (:key, :value) in json[r'nullableMapOfSerializableField'] as prefix1.Map) key: value == null ? null : prefix0.C.fromJson(value as prefix1.Map}; + +prefix1.Map toJson() { +json[r'boolField'] = boolField; +json[r'nullableBoolField'] = nullableBoolField; +json[r'stringField'] = stringField; +json[r'nullableStringField'] = nullableStringField; +json[r'intField'] = intField; +json[r'nullableIntField'] = nullableIntField; +json[r'doubleField'] = doubleField; +json[r'nullableDoubleField'] = nullableDoubleField; +json[r'numField'] = numField; +json[r'nullableNumField'] = nullableNumField; +json[r'listOfSerializableField'] = listOfSerializableField == null ? null : {for (final item in listOfSerializableField) item == null ? null : item.toJson()}; +json[r'nullableListOfSerializableField'] = {for (final item in nullableListOfSerializableField) item == null ? null : item.toJson()}; +json[r'setOfSerializableField'] = setOfSerializableField == null ? null : {for (final item in setOfSerializableField) item == null ? null : item.toJson()}; +json[r'nullableSetOfSerializableField'] = {for (final item in nullableSetOfSerializableField) item == null ? null : item.toJson()}; +json[r'mapOfSerializableField'] = mapOfSerializableField == null ? null : {for (final (:key, :value) in mapOfSerializableField.entries) key: value == null ? null : value.toJson()}; +json[r'nullableMapOfSerializableField'] = {for (final (:key, :value) in nullableMapOfSerializableField.entries) key: value == null ? null : value.toJson()} +}; + +} +augment class C { +external prefix0.C.fromJson(prefix1.Map json); +external prefix1.Map toJson(); + +augment prefix0.C.fromJson(prefix1.Map json) : +boolField = json[r'boolField'] as prefix1.bool; + +prefix1.Map toJson() { +json[r'boolField'] = boolField +}; + +} diff --git a/goldens/foo/lib/json_codable.dart b/goldens/foo/lib/json_codable.dart new file mode 100644 index 00000000..6ea32858 --- /dev/null +++ b/goldens/foo/lib/json_codable.dart @@ -0,0 +1,45 @@ +// 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:_test_macros/json_codable.dart'; + +@JsonCodable() +class A { + final bool boolField; + + final bool? nullableBoolField; + + final String stringField; + + final String? nullableStringField; + + final int intField; + + final int? nullableIntField; + + final double doubleField; + + final double? nullableDoubleField; + + final num numField; + + final num? nullableNumField; + + final List listOfSerializableField; + + final List? nullableListOfSerializableField; + + final Set setOfSerializableField; + + final Set? nullableSetOfSerializableField; + + final Map mapOfSerializableField; + + final Map? nullableMapOfSerializableField; +} + +@JsonCodable() +class C { + final bool boolField; +} diff --git a/pkgs/_analyzer_macros/lib/macro_implementation.dart b/pkgs/_analyzer_macros/lib/macro_implementation.dart index 4397385b..74a5ee7a 100644 --- a/pkgs/_analyzer_macros/lib/macro_implementation.dart +++ b/pkgs/_analyzer_macros/lib/macro_implementation.dart @@ -88,7 +88,7 @@ class AnalyzerRunningMacro implements injected.RunningMacro { DeclarationPhaseIntrospector declarationsPhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access analyzer internals; remove. introspector = declarationsPhaseIntrospector; - return AnalyzerMacroExecutionResult( + return await AnalyzerMacroExecutionResult.expandTemplates( target, await _impl._host.augment( name, AugmentRequest(phase: 2, target: target.qualifiedName))); @@ -100,7 +100,7 @@ class AnalyzerRunningMacro implements injected.RunningMacro { DefinitionPhaseIntrospector definitionPhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access analyzer internals; remove. introspector = definitionPhaseIntrospector; - return AnalyzerMacroExecutionResult( + return await AnalyzerMacroExecutionResult.expandTemplates( target, await _impl._host.augment( name, AugmentRequest(phase: 3, target: target.qualifiedName))); @@ -111,7 +111,7 @@ class AnalyzerRunningMacro implements injected.RunningMacro { MacroTarget target, TypePhaseIntrospector typePhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access analyzer internals; remove. introspector = typePhaseIntrospector; - return AnalyzerMacroExecutionResult( + return await AnalyzerMacroExecutionResult.expandTemplates( target, await _impl._host.augment( name, AugmentRequest(phase: 1, target: target.qualifiedName))); @@ -124,9 +124,27 @@ class AnalyzerRunningMacro implements injected.RunningMacro { /// functionality of `MacroExecutionResult`. class AnalyzerMacroExecutionResult implements injected.MacroExecutionResult { final MacroTarget target; - final AugmentResponse augmentResponse; + @override + final Map> typeAugmentations; + + AnalyzerMacroExecutionResult( + this.target, Iterable declarations) + // TODO(davidmorgan): this assumes augmentations are for the macro + // application target. Instead, it should be explicit in + // `AugmentResponse`. + : typeAugmentations = {(target as Declaration).identifier: declarations}; + + static Future expandTemplates( + MacroTarget target, AugmentResponse augmentResponse) async { + final declarations = []; + for (final augmentation in augmentResponse.augmentations) { + declarations.add( + DeclarationCode.fromParts(await _expandTemplates(augmentation.code))); + } + return AnalyzerMacroExecutionResult(target, declarations); + } - AnalyzerMacroExecutionResult(this.target, this.augmentResponse); + Future resolveTypes() async {} @override List get diagnostics => []; @@ -155,16 +173,6 @@ class AnalyzerMacroExecutionResult implements injected.MacroExecutionResult { @override void serialize(Object serializer) => throw UnimplementedError(); - - @override - Map> get typeAugmentations => { - // TODO(davidmorgan): this assumes augmentations are for the macro - // application target. Instead, it should be explicit in - // `AugmentResponse`. - (target as Declaration).identifier: augmentResponse.augmentations - .map((a) => DeclarationCode.fromParts([a.code])) - .toList(), - }; } extension MacroTargetExtension on MacroTarget { @@ -176,3 +184,35 @@ extension MacroTargetExtension on MacroTarget { name: element.displayName); } } + +/// Converts [code] to a mix of `Identifier` and `String`. +/// +/// Looks up references of the form `{{uri#name}}` using `resolveIdentifier`. +Future> _expandTemplates(String code) async { + final result = []; + var index = 0; + while (index < code.length) { + final start = code.indexOf('{{', index); + if (start == -1) { + result.add(code.substring(index)); + break; + } + result.add(code.substring(index, start)); + final end = code.indexOf('}}', start); + if (end == -1) { + throw ArgumentError('Unmatched opening brace: $code'); + } + final name = code.substring(start + 2, end); + final parts = name.split('#'); + if (parts.length != 2) { + throw ArgumentError('Expected "uri#name" in: $name'); + } + final uri = Uri.parse(parts[0]); + final identifier = await (introspector as TypePhaseIntrospector) + // ignore: deprecated_member_use + .resolveIdentifier(uri, parts[1]); + result.add(identifier); + index = end + 2; + } + return result; +} diff --git a/pkgs/_analyzer_macros/lib/query_service.dart b/pkgs/_analyzer_macros/lib/query_service.dart index ad0bcca2..47fa28f7 100644 --- a/pkgs/_analyzer_macros/lib/query_service.dart +++ b/pkgs/_analyzer_macros/lib/query_service.dart @@ -5,6 +5,7 @@ import 'package:_macro_host/macro_host.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/dart/element/type_provider.dart'; // ignore: implementation_imports import 'package:analyzer/src/summary2/linked_element_factory.dart' as analyzer; import 'package:dart_model/dart_model.dart' hide InterfaceType; @@ -31,64 +32,76 @@ class AnalyzerQueryService implements QueryService { final uri = target.uri; final library = _elementFactory.libraryOfUri2(Uri.parse(uri)); final clazz = library.getClass(target.name)!; + final types = AnalyzerTypeHierarchy(library.typeProvider) + ..addInterfaceElement(clazz); + final interface = Interface(); for (final field in clazz.fields) { interface.members[field.name] = Member( properties: Properties( - isAbstract: field.isAbstract, - isGetter: false, - isField: true, - isMethod: false, - isStatic: field.isStatic, - )); + isAbstract: field.isAbstract, + isGetter: false, + isField: true, + isMethod: false, + isStatic: field.isStatic, + ), + returnType: types.addDartType(field.type)); } - return Model(types: _buildTypeHierarchy(library, {clazz})) + return Model(types: types.typeHierarchy) ..uris[uri] = (Library()..scopes[clazz.name] = interface); } +} - TypeHierarchy _buildTypeHierarchy( - LibraryElement library, Set classes) { - // These classes need to be present in every type hierarchy due to their - // special role. Other types are only relevant if reachable from [classes]. - final typeProvider = library.typeProvider; - classes.add(typeProvider.objectElement); - classes.add(typeProvider.nullElement); - classes.add(typeProvider.futureElement); +/// Converts between analyzer types and `dart_model` types. +class AnalyzerTypeHierarchy { + final TypeProvider typeProvider; + final AnalyzerTypesToMacros translator = const AnalyzerTypesToMacros(); + final TypeHierarchy typeHierarchy = TypeHierarchy(); + final TypeTranslationContext context = TypeTranslationContext(); - // Make sure we include all supertypes of involved classes to close the - // type hierarchy. - for (final included in classes.toList()) { - for (final superType in included.allSupertypes) { - classes.add(superType.element); - } - } + AnalyzerTypeHierarchy(this.typeProvider) { + // Types that are always needed. + addInterfaceElement(typeProvider.objectElement); + addInterfaceElement(typeProvider.nullElement); + addInterfaceElement(typeProvider.futureElement); + } - final context = TypeTranslationContext(); - const translator = AnalyzerTypesToMacros(); - final result = TypeHierarchy(); - for (final element in classes) { - final asNamedType = element.thisType - .acceptWithArgument(translator, context) - .asNamedTypeDesc; + /// Adds [type] to the hierarchy and returns its [StaticTypeDesc]. + StaticTypeDesc addDartType(DartType type) => + type.acceptWithArgument(translator, context); - final superTypes = [ - if (element.supertype case final supertype?) supertype, - ...element.interfaces, - ...element.mixins, - ]; + /// Adds [element] and any supertypes to the hierarchy, if not already + /// present. + void addInterfaceElement(InterfaceElement element) { + final asNamedType = element.thisType + .acceptWithArgument(translator, context) + .asNamedTypeDesc; - result.named[asNamedType.name.asString] = TypeHierarchyEntry( - self: asNamedType, - typeParameters: [ - for (final typeParameter in element.typeParameters) - translator.translateTypeParameter(typeParameter, context), - ], - supertypes: [ - for (final superType in superTypes) - superType.acceptWithArgument(translator, context).asNamedTypeDesc - ], - ); + final maybeEntry = typeHierarchy.named[asNamedType.name.asString]; + if (maybeEntry != null) { + return; } - return result; + + final superTypes = [ + if (element.supertype case final supertype?) supertype, + ...element.interfaces, + ...element.mixins, + ]; + + for (final type in superTypes) { + addInterfaceElement(type.element); + } + + typeHierarchy.named[asNamedType.name.asString] = TypeHierarchyEntry( + self: asNamedType, + typeParameters: [ + for (final typeParameter in element.typeParameters) + translator.translateTypeParameter(typeParameter, context), + ], + supertypes: [ + for (final superType in superTypes) + superType.acceptWithArgument(translator, context).asNamedTypeDesc + ], + ); } } diff --git a/pkgs/_analyzer_macros/test/golden_test.dart b/pkgs/_analyzer_macros/test/golden_test.dart index f1ae0cef..39285d3f 100644 --- a/pkgs/_analyzer_macros/test/golden_test.dart +++ b/pkgs/_analyzer_macros/test/golden_test.dart @@ -67,10 +67,6 @@ void main() { } else { applicationGoldenFile = null; } - - if (introspectionGoldenFile == null && applicationGoldenFile == null) { - throw StateError('Setup failed, no goldens for $path'); - } }); if (updateGoldens) { @@ -97,6 +93,11 @@ void main() { } test(relativePath, () async { + if (introspectionGoldenFile == null && applicationGoldenFile == null) { + // Nothing to check. + return; + } + final resolvedLibrary = (await analysisContext.currentSession .getResolvedLibrary(path)) as ResolvedLibraryResult; final augmentationUnit = diff --git a/pkgs/_cfe_macros/test/golden_test.dart b/pkgs/_cfe_macros/test/golden_test.dart index 2bf26d89..83d13800 100644 --- a/pkgs/_cfe_macros/test/golden_test.dart +++ b/pkgs/_cfe_macros/test/golden_test.dart @@ -76,10 +76,6 @@ void main() { } else { applicationGoldenFile = null; } - - if (introspectionGoldenFile == null && applicationGoldenFile == null) { - throw StateError('Setup failed, no goldens for $path'); - } }); if (updateGoldens) { tearDown(() { @@ -105,6 +101,11 @@ void main() { } test(relativePath, () async { + if (introspectionGoldenFile == null && applicationGoldenFile == null) { + // Nothing to check. + return; + } + final packagesUri = Isolate.packageConfigSync; final outputFile = File('${tempDir.path}/$relativePath.dill'); diff --git a/pkgs/_test_macros/lib/json_codable.dart b/pkgs/_test_macros/lib/json_codable.dart new file mode 100644 index 00000000..c492623f --- /dev/null +++ b/pkgs/_test_macros/lib/json_codable.dart @@ -0,0 +1,170 @@ +// 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:dart_model/dart_model.dart'; +import 'package:macro/macro.dart'; +import 'package:macro_service/macro_service.dart'; + +/// A macro which adds a `fromJson(Map json)` JSON decoding +/// constructor to a class. +class JsonCodable { + const JsonCodable(); +} + +final _jsonMapType = + '{{dart:core#Map}}<{{dart:core#String}}, {{dart:core#Object}}?>'; + +class JsonCodableImplementation implements Macro { + @override + MacroDescription get description => MacroDescription( + annotation: QualifiedName( + uri: 'package:_test_macros/json_codable.dart', name: 'JsonCodable'), + runsInPhases: [2, 3]); + + @override + Future augment(Host host, AugmentRequest request) async { + return switch (request.phase) { + 1 => phase1(host, request), + 2 => phase2(host, request), + _ => AugmentResponse(augmentations: []), + }; + } + + Future phase1(Host host, AugmentRequest request) async { + final target = request.target; + return AugmentResponse(augmentations: [ + Augmentation(code: ''' +external ${target.code}.fromJson($_jsonMapType json); +external $_jsonMapType toJson(); + '''), + ]); + } + + Future phase2(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]!; + final initializers = []; + for (final field + in clazz.members.entries.where((m) => m.value.properties.isField)) { + final name = field.key; + final type = field.value.returnType; + initializers + .add('$name = ${_convertTypeFromJson("json[r'$name']", type)}'); + } + + // TODO(davidmorgan): helper for augmenting initializers. + result.add(Augmentation(code: ''' +augment ${target.code}.fromJson($_jsonMapType json) : +${initializers.join(',\n')}; +''')); + + final serializers = []; + for (final field + in clazz.members.entries.where((m) => m.value.properties.isField)) { + final name = field.key; + final type = field.value.returnType; + serializers.add("json[r'$name'] = ${_convertTypeToJson(name, type)}"); + } + + // TODO(davidmorgan): helper for augmenting methods. + result.add(Augmentation(code: ''' +$_jsonMapType toJson() { +${serializers.join(';\n')} +}; +''')); + + return AugmentResponse(augmentations: result); + } + + String _convertTypeFromJson(String reference, StaticTypeDesc type) { + // TODO(davidmorgan): _checkNamedType equivalent. + final nullable = type.type == StaticTypeDescType.nullableTypeDesc; + final orNull = nullable ? '?' : ''; + final nullCheck = nullable ? '' : '$reference == null ? null : '; + final underlyingType = type.type == StaticTypeDescType.nullableTypeDesc + ? type.asNullableTypeDesc.inner + : type; + + if (underlyingType.type == StaticTypeDescType.namedTypeDesc) { + final namedType = underlyingType.asNamedTypeDesc; + if (namedType.name.uri == 'dart:core') { + switch (namedType.name.name) { + case 'bool': + case 'String': + case 'int': + case 'double': + case 'num': + return '$reference as ${namedType.name.code}$orNull'; + case 'List': + return '$nullCheck [for (final item in $reference ' + 'as {{dart:core#List}}<{{dart:core#Object}}?>) ' + '${_convertTypeFromJson('item', namedType.instantiation.first)}' + ']'; + case 'Set': + return '$nullCheck {for (final item in $reference ' + 'as {{dart:core#Set}}<{{dart:core#Object}}?>) ' + '${_convertTypeFromJson('item', namedType.instantiation.first)}' + '}'; + case 'Map': + return '$nullCheck {for (final (:key, :value) in $reference ' + 'as $_jsonMapType) key: ' + '${_convertTypeFromJson('value', namedType.instantiation.last)}' + '}'; + } + } + // TODO(davidmorgan): check for fromJson constructor. + return '$nullCheck ${namedType.name.code}.fromJson($reference as ' + '$_jsonMapType'; + } + + // TODO(davidmorgan): error reporting. + throw UnsupportedError('$type'); + } + + String _convertTypeToJson(String reference, StaticTypeDesc type) { + // TODO(davidmorgan): add _checkNamedType equivalent. + final nullable = type.type == StaticTypeDescType.nullableTypeDesc; + final nullCheck = nullable ? '' : '$reference == null ? null : '; + final underlyingType = type.type == StaticTypeDescType.nullableTypeDesc + ? type.asNullableTypeDesc.inner + : type; + + if (underlyingType.type == StaticTypeDescType.namedTypeDesc) { + final namedType = underlyingType.asNamedTypeDesc; + if (namedType.name.uri == 'dart:core') { + switch (namedType.name.name) { + case 'bool': + case 'String': + case 'int': + case 'double': + case 'num': + return reference; + case 'List': + case 'Set': + return '$nullCheck {for (final item in $reference) ' + '${_convertTypeToJson('item', namedType.instantiation.first)}' + '}'; + case 'Map': + return '$nullCheck {for (final (:key, :value) in ' + '$reference.entries) key: ' + '${_convertTypeToJson('value', namedType.instantiation.last)}' + '}'; + } + } + // TODO(davidmorgan): check for toJson method. + return '$nullCheck $reference.toJson()'; + } + + // TODO(davidmorgan): error reporting. + throw UnsupportedError('$type'); + } +} + +// TODO(davidmorgan): figure out where this should go. +extension TemplatingExtension on QualifiedName { + String get code => '{{$uri#$name}}'; +} diff --git a/pkgs/_test_macros/pubspec.yaml b/pkgs/_test_macros/pubspec.yaml index 3067cdcf..e96e0f3d 100644 --- a/pkgs/_test_macros/pubspec.yaml +++ b/pkgs/_test_macros/pubspec.yaml @@ -19,4 +19,5 @@ dev_dependencies: # TODO(language/3728): use the real feature when there is one. # macro lib/declare_x_macro.dart#DeclareX package:_test_macros/declare_x_macro.dart#DeclareXImplementation +# macro lib/json_codable.dart#JsonCodable package:_test_macros/json_codable.dart#JsonCodableImplementation # macro lib/query_class.dart#QueryClass package:_test_macros/query_class.dart#QueryClassImplementation diff --git a/pkgs/dart_model/lib/src/dart_model.g.dart b/pkgs/dart_model/lib/src/dart_model.g.dart index bc20f541..8a7b4f30 100644 --- a/pkgs/dart_model/lib/src/dart_model.g.dart +++ b/pkgs/dart_model/lib/src/dart_model.g.dart @@ -137,16 +137,22 @@ extension type Library.fromJson(Map node) implements Object { extension type Member.fromJson(Map node) implements Object { static final TypedMapSchema _schema = TypedMapSchema({ 'properties': Type.typedMapPointer, + 'returnType': Type.typedMapPointer, }); Member({ Properties? properties, + StaticTypeDesc? returnType, }) : this.fromJson(Scope.createMap( _schema, properties, + returnType, )); /// 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; } /// Partial model of a corpus of Dart source code. diff --git a/schemas/dart_model.schema.json b/schemas/dart_model.schema.json index 7cac2ebe..18e1e6fb 100644 --- a/schemas/dart_model.schema.json +++ b/schemas/dart_model.schema.json @@ -113,6 +113,10 @@ "properties": { "$comment": "The properties of this member.", "$ref": "#/$defs/Properties" + }, + "returnType": { + "$comment": "The return type of this member, if it has one.", + "$ref": "#/$defs/StaticTypeDesc" } } }, diff --git a/tool/dart_model_generator/lib/definitions.dart b/tool/dart_model_generator/lib/definitions.dart index f5793c14..93d826d5 100644 --- a/tool/dart_model_generator/lib/definitions.dart +++ b/tool/dart_model_generator/lib/definitions.dart @@ -87,6 +87,11 @@ final schemas = Schemas([ Property('properties', type: 'Properties', description: 'The properties of this member.'), + Property( + 'returnType', + type: 'StaticTypeDesc', + description: 'The return type of this member, if it has one.', + ), ]), Definition.clazz('Model', description: 'Partial model of a corpus of Dart source code.', From d407dc60b1866442e3476e684be59dafba904dc9 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Thu, 26 Sep 2024 18:17:58 +0200 Subject: [PATCH 2/3] Address review comments. --- .../lib/json_codable.analyzer.augmentations | 18 ++++++++++----- .../lib/macro_implementation.dart | 4 ++-- pkgs/_test_macros/lib/json_codable.dart | 23 +++++++++++++++---- .../dart_model_generator/lib/definitions.dart | 1 + 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/goldens/foo/lib/json_codable.analyzer.augmentations b/goldens/foo/lib/json_codable.analyzer.augmentations index 046e98d7..8ce63b9c 100644 --- a/goldens/foo/lib/json_codable.analyzer.augmentations +++ b/goldens/foo/lib/json_codable.analyzer.augmentations @@ -26,6 +26,7 @@ mapOfSerializableField = json[r'mapOfSerializableField'] == null ? null : {for nullableMapOfSerializableField = {for (final (:key, :value) in json[r'nullableMapOfSerializableField'] as prefix1.Map) key: value == null ? null : prefix0.C.fromJson(value as prefix1.Map}; prefix1.Map toJson() { + final json = prefix1.Map{}; json[r'boolField'] = boolField; json[r'nullableBoolField'] = nullableBoolField; json[r'stringField'] = stringField; @@ -36,12 +37,14 @@ json[r'doubleField'] = doubleField; json[r'nullableDoubleField'] = nullableDoubleField; json[r'numField'] = numField; json[r'nullableNumField'] = nullableNumField; -json[r'listOfSerializableField'] = listOfSerializableField == null ? null : {for (final item in listOfSerializableField) item == null ? null : item.toJson()}; -json[r'nullableListOfSerializableField'] = {for (final item in nullableListOfSerializableField) item == null ? null : item.toJson()}; -json[r'setOfSerializableField'] = setOfSerializableField == null ? null : {for (final item in setOfSerializableField) item == null ? null : item.toJson()}; -json[r'nullableSetOfSerializableField'] = {for (final item in nullableSetOfSerializableField) item == null ? null : item.toJson()}; +json[r'listOfSerializableField'] = listOfSerializableField == null ? null : [for (final item in listOfSerializableField) item == null ? null : item.toJson()]; +json[r'nullableListOfSerializableField'] = [for (final item in nullableListOfSerializableField) item == null ? null : item.toJson()]; +json[r'setOfSerializableField'] = setOfSerializableField == null ? null : [for (final item in setOfSerializableField) item == null ? null : item.toJson()]; +json[r'nullableSetOfSerializableField'] = [for (final item in nullableSetOfSerializableField) item == null ? null : item.toJson()]; json[r'mapOfSerializableField'] = mapOfSerializableField == null ? null : {for (final (:key, :value) in mapOfSerializableField.entries) key: value == null ? null : value.toJson()}; -json[r'nullableMapOfSerializableField'] = {for (final (:key, :value) in nullableMapOfSerializableField.entries) key: value == null ? null : value.toJson()} +json[r'nullableMapOfSerializableField'] = {for (final (:key, :value) in nullableMapOfSerializableField.entries) key: value == null ? null : value.toJson()}; + + return json; }; } @@ -53,7 +56,10 @@ augment prefix0.C.fromJson(prefix1.Map json) : boolField = json[r'boolField'] as prefix1.bool; prefix1.Map toJson() { -json[r'boolField'] = boolField + final json = prefix1.Map{}; +json[r'boolField'] = boolField; + + return json; }; } diff --git a/pkgs/_analyzer_macros/lib/macro_implementation.dart b/pkgs/_analyzer_macros/lib/macro_implementation.dart index 74a5ee7a..f4ac7652 100644 --- a/pkgs/_analyzer_macros/lib/macro_implementation.dart +++ b/pkgs/_analyzer_macros/lib/macro_implementation.dart @@ -144,8 +144,6 @@ class AnalyzerMacroExecutionResult implements injected.MacroExecutionResult { return AnalyzerMacroExecutionResult(target, declarations); } - Future resolveTypes() async {} - @override List get diagnostics => []; @@ -188,6 +186,8 @@ extension MacroTargetExtension on MacroTarget { /// Converts [code] to a mix of `Identifier` and `String`. /// /// Looks up references of the form `{{uri#name}}` using `resolveIdentifier`. +/// +/// TODO(davidmorgan): move to the client side. Future> _expandTemplates(String code) async { final result = []; var index = 0; diff --git a/pkgs/_test_macros/lib/json_codable.dart b/pkgs/_test_macros/lib/json_codable.dart index c492623f..1ba9f6cb 100644 --- a/pkgs/_test_macros/lib/json_codable.dart +++ b/pkgs/_test_macros/lib/json_codable.dart @@ -25,6 +25,8 @@ class JsonCodableImplementation implements Macro { @override Future augment(Host host, AugmentRequest request) async { return switch (request.phase) { + // TODO(davidmorgan): these should be phases 2 and 3, but that doesn't + // work right now, it gives no output for phase 3. Investigate and fix. 1 => phase1(host, request), 2 => phase2(host, request), _ => AugmentResponse(augmentations: []), @@ -47,6 +49,7 @@ external $_jsonMapType toJson(); 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`. final initializers = []; for (final field in clazz.members.entries.where((m) => m.value.properties.isField)) { @@ -57,6 +60,7 @@ external $_jsonMapType toJson(); } // 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.code}.fromJson($_jsonMapType json) : ${initializers.join(',\n')}; @@ -71,9 +75,12 @@ ${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: ''' $_jsonMapType toJson() { -${serializers.join(';\n')} + final json = $_jsonMapType{}; +${serializers.map((s) =>'$s;\n').join('')} + return json; }; ''')); @@ -82,6 +89,9 @@ ${serializers.join(';\n')} String _convertTypeFromJson(String reference, StaticTypeDesc type) { // TODO(davidmorgan): _checkNamedType equivalent. + // TODO(davidmorgan): should this code use `StaticType` and related classes + // instead of using the extension types `StaticTypeDesc` directly? + // TODO(davidmorgan): check for and handle missing type argument(s). final nullable = type.type == StaticTypeDescType.nullableTypeDesc; final orNull = nullable ? '?' : ''; final nullCheck = nullable ? '' : '$reference == null ? null : '; @@ -102,14 +112,17 @@ ${serializers.join(';\n')} case 'List': return '$nullCheck [for (final item in $reference ' 'as {{dart:core#List}}<{{dart:core#Object}}?>) ' - '${_convertTypeFromJson('item', namedType.instantiation.first)}' + '${_convertTypeFromJson( + 'item', namedType.instantiation.single)}' ']'; case 'Set': return '$nullCheck {for (final item in $reference ' 'as {{dart:core#Set}}<{{dart:core#Object}}?>) ' - '${_convertTypeFromJson('item', namedType.instantiation.first)}' + '${_convertTypeFromJson( + 'item', namedType.instantiation.single)}' '}'; case 'Map': + // TODO(davidmorgan): check for and handle wrong key type. return '$nullCheck {for (final (:key, :value) in $reference ' 'as $_jsonMapType) key: ' '${_convertTypeFromJson('value', namedType.instantiation.last)}' @@ -145,9 +158,9 @@ ${serializers.join(';\n')} return reference; case 'List': case 'Set': - return '$nullCheck {for (final item in $reference) ' + return '$nullCheck [for (final item in $reference) ' '${_convertTypeToJson('item', namedType.instantiation.first)}' - '}'; + ']'; case 'Map': return '$nullCheck {for (final (:key, :value) in ' '$reference.entries) key: ' diff --git a/tool/dart_model_generator/lib/definitions.dart b/tool/dart_model_generator/lib/definitions.dart index 93d826d5..1821fdac 100644 --- a/tool/dart_model_generator/lib/definitions.dart +++ b/tool/dart_model_generator/lib/definitions.dart @@ -80,6 +80,7 @@ final schemas = Schemas([ Property('scopes', type: 'Map', description: 'Scopes by name.'), ]), + // TODO(davidmorgan): make `Member` a union. Definition.clazz('Member', description: 'Member of a scope.', createInBuffer: true, From e80adbff72be3878ed92f9cdf7e8bfb3452c76a5 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Thu, 26 Sep 2024 18:24:51 +0200 Subject: [PATCH 3/3] Dartfmt. --- pkgs/_test_macros/lib/json_codable.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/_test_macros/lib/json_codable.dart b/pkgs/_test_macros/lib/json_codable.dart index 1ba9f6cb..24d1ec07 100644 --- a/pkgs/_test_macros/lib/json_codable.dart +++ b/pkgs/_test_macros/lib/json_codable.dart @@ -79,7 +79,7 @@ ${initializers.join(',\n')}; result.add(Augmentation(code: ''' $_jsonMapType toJson() { final json = $_jsonMapType{}; -${serializers.map((s) =>'$s;\n').join('')} +${serializers.map((s) => '$s;\n').join('')} return json; }; ''')); @@ -110,16 +110,16 @@ ${serializers.map((s) =>'$s;\n').join('')} case 'num': return '$reference as ${namedType.name.code}$orNull'; case 'List': + final type = namedType.instantiation.single; return '$nullCheck [for (final item in $reference ' 'as {{dart:core#List}}<{{dart:core#Object}}?>) ' - '${_convertTypeFromJson( - 'item', namedType.instantiation.single)}' + '${_convertTypeFromJson('item', type)}' ']'; case 'Set': + final type = namedType.instantiation.single; return '$nullCheck {for (final item in $reference ' 'as {{dart:core#Set}}<{{dart:core#Object}}?>) ' - '${_convertTypeFromJson( - 'item', namedType.instantiation.single)}' + '${_convertTypeFromJson('item', type)}' '}'; case 'Map': // TODO(davidmorgan): check for and handle wrong key type.