diff --git a/pkgs/_analyzer_macros/lib/macro_implementation.dart b/pkgs/_analyzer_macros/lib/macro_implementation.dart index 0594c47b..09b48b08 100644 --- a/pkgs/_analyzer_macros/lib/macro_implementation.dart +++ b/pkgs/_analyzer_macros/lib/macro_implementation.dart @@ -9,9 +9,9 @@ import 'package:analyzer/src/summary2/macro_declarations.dart' as analyzer; import 'package:analyzer/src/summary2/macro_injected_impl.dart' as injected; import 'package:dart_model/dart_model.dart'; import 'package:macro_service/macro_service.dart'; -import 'package:macros/macros.dart'; +import 'package:macros/macros.dart' as macros_api_v1; // ignore: implementation_imports -import 'package:macros/src/executor.dart' as injected; +import 'package:macros/src/executor.dart' as macros_api_v1; import 'query_service.dart'; @@ -83,11 +83,12 @@ class AnalyzerRunningMacro implements injected.RunningMacro { @override Future executeDeclarationsPhase( - MacroTarget target, - DeclarationPhaseIntrospector declarationsPhaseIntrospector) async { + macros_api_v1.MacroTarget target, + macros_api_v1.DeclarationPhaseIntrospector + declarationsPhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access analyzer internals; remove. introspector = declarationsPhaseIntrospector; - return await AnalyzerMacroExecutionResult.expandTemplates( + return await AnalyzerMacroExecutionResult.dartModelToInjected( target, await _impl._host.augment( name, AugmentRequest(phase: 2, target: target.qualifiedName))); @@ -95,11 +96,12 @@ class AnalyzerRunningMacro implements injected.RunningMacro { @override Future executeDefinitionsPhase( - MacroTarget target, - DefinitionPhaseIntrospector definitionPhaseIntrospector) async { + macros_api_v1.MacroTarget target, + macros_api_v1.DefinitionPhaseIntrospector + definitionPhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access analyzer internals; remove. introspector = definitionPhaseIntrospector; - return await AnalyzerMacroExecutionResult.expandTemplates( + return await AnalyzerMacroExecutionResult.dartModelToInjected( target, await _impl._host.augment( name, AugmentRequest(phase: 3, target: target.qualifiedName))); @@ -107,63 +109,71 @@ class AnalyzerRunningMacro implements injected.RunningMacro { @override Future executeTypesPhase( - MacroTarget target, TypePhaseIntrospector typePhaseIntrospector) async { + macros_api_v1.MacroTarget target, + macros_api_v1.TypePhaseIntrospector typePhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access analyzer internals; remove. introspector = typePhaseIntrospector; - return await AnalyzerMacroExecutionResult.expandTemplates( + return await AnalyzerMacroExecutionResult.dartModelToInjected( target, await _impl._host.augment( name, AugmentRequest(phase: 1, target: target.qualifiedName))); } } -/// Converts [AugmentResponse] to [injected.MacroExecutionResult]. +/// Converts [AugmentResponse] to [macros_api_v1.MacroExecutionResult]. /// /// TODO(davidmorgan): add to `AugmentationResponse` to cover all the /// functionality of `MacroExecutionResult`. -class AnalyzerMacroExecutionResult implements injected.MacroExecutionResult { - final MacroTarget target; +class AnalyzerMacroExecutionResult + implements macros_api_v1.MacroExecutionResult { + final macros_api_v1.MacroTarget target; @override - final Map> typeAugmentations; + final Map> + typeAugmentations; AnalyzerMacroExecutionResult( - this.target, Iterable declarations) + 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}; + : typeAugmentations = { + (target as macros_api_v1.Declaration).identifier: declarations + }; - static Future expandTemplates( - MacroTarget target, AugmentResponse augmentResponse) async { - final declarations = []; + static Future dartModelToInjected( + macros_api_v1.MacroTarget target, AugmentResponse augmentResponse) async { + final declarations = []; for (final augmentation in augmentResponse.augmentations) { - declarations.add( - DeclarationCode.fromParts(await _expandTemplates(augmentation.code))); + declarations.add(macros_api_v1.DeclarationCode.fromParts( + await _resolveNames(augmentation.code))); } return AnalyzerMacroExecutionResult(target, declarations); } @override - List get diagnostics => []; + List get diagnostics => []; @override - Map> get enumValueAugmentations => {}; + Map> + get enumValueAugmentations => {}; @override - MacroException? get exception => null; + macros_api_v1.MacroException? get exception => null; @override - Map get extendsTypeAugmentations => {}; + Map + get extendsTypeAugmentations => {}; @override - Map> get interfaceAugmentations => - {}; + Map> + get interfaceAugmentations => {}; @override - Iterable get libraryAugmentations => {}; + Iterable get libraryAugmentations => {}; @override - Map> get mixinAugmentations => {}; + Map> + get mixinAugmentations => {}; @override Iterable get newTypeNames => []; @@ -172,46 +182,49 @@ class AnalyzerMacroExecutionResult implements injected.MacroExecutionResult { void serialize(Object serializer) => throw UnimplementedError(); } -extension MacroTargetExtension on MacroTarget { +extension MacroTargetExtension on macros_api_v1.MacroTarget { QualifiedName get qualifiedName { - final element = - ((this as Declaration).identifier as analyzer.IdentifierImpl).element!; + final element = ((this as macros_api_v1.Declaration).identifier + as analyzer.IdentifierImpl) + .element!; return QualifiedName( uri: '${element.library!.definingCompilationUnit.source.uri}', name: element.displayName); } } -/// 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; - while (index < code.length) { - final start = code.indexOf('{{', index); - if (start == -1) { - result.add(code.substring(index)); - break; +/// Converts [codes] to a list of `String` and `Identifier`. +Future> _resolveNames(List codes) async { + // Find the set of unique [QualifiedName]s used. + final qualifiedNameStrings = {}; + for (final code in codes) { + if (code.type == CodeType.qualifiedName) { + qualifiedNameStrings.add(code.asQualifiedName.asString); } - 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) + } + + // Create futures looking up their [Identifier]s, then `await` in parallel. + final qualifiedNamesList = + qualifiedNameStrings.map(QualifiedName.parse).toList(); + final identifierFutures = >[]; + for (final qualifiedName in qualifiedNamesList) { + identifierFutures.add((introspector as macros_api_v1.TypePhaseIntrospector) // ignore: deprecated_member_use - .resolveIdentifier(uri, parts[1]); - result.add(identifier); - index = end + 2; + .resolveIdentifier(Uri.parse(qualifiedName.uri), qualifiedName.name)); + } + final identifiers = await Future.wait(identifierFutures); + + // Build the result using the looked up [Identifier]s. + final identifiersByQualifiedNameStrings = + Map.fromIterables(qualifiedNameStrings, identifiers); + final result = []; + for (final code in codes) { + if (code.type == CodeType.string) { + result.add(code.asString); + } else if (code.type == CodeType.qualifiedName) { + final qualifiedName = code.asQualifiedName; + result.add(identifiersByQualifiedNameStrings[qualifiedName.asString]!); + } } return result; } diff --git a/pkgs/_cfe_macros/lib/macro_implementation.dart b/pkgs/_cfe_macros/lib/macro_implementation.dart index f64a6dbc..746e494b 100644 --- a/pkgs/_cfe_macros/lib/macro_implementation.dart +++ b/pkgs/_cfe_macros/lib/macro_implementation.dart @@ -9,9 +9,9 @@ import 'package:front_end/src/kernel/macro/identifiers.dart' as cfe; // ignore: implementation_imports import 'package:front_end/src/macros/macro_injected_impl.dart' as injected; import 'package:macro_service/macro_service.dart'; -import 'package:macros/macros.dart'; +import 'package:macros/macros.dart' as macros_api_v1; // ignore: implementation_imports -import 'package:macros/src/executor.dart' as injected; +import 'package:macros/src/executor.dart' as macros_api_v1; import 'query_service.dart'; @@ -82,22 +82,26 @@ class CfeRunningMacro implements injected.RunningMacro { } @override - Future executeDeclarationsPhase(MacroTarget target, - DeclarationPhaseIntrospector declarationsPhaseIntrospector) async { + Future executeDeclarationsPhase( + macros_api_v1.MacroTarget target, + macros_api_v1.DeclarationPhaseIntrospector + declarationsPhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access CFE internals; remove. introspector = declarationsPhaseIntrospector; - return CfeMacroExecutionResult( + return await CfeMacroExecutionResult.dartModelToInjected( target, await _impl._host.augment( name, AugmentRequest(phase: 2, target: target.qualifiedName))); } @override - Future executeDefinitionsPhase(MacroTarget target, - DefinitionPhaseIntrospector definitionPhaseIntrospector) async { + Future executeDefinitionsPhase( + macros_api_v1.MacroTarget target, + macros_api_v1.DefinitionPhaseIntrospector + definitionPhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access CFE internals; remove. introspector = definitionPhaseIntrospector; - return CfeMacroExecutionResult( + return await CfeMacroExecutionResult.dartModelToInjected( target, await _impl._host.augment( name, AugmentRequest(phase: 3, target: target.qualifiedName))); @@ -105,72 +109,123 @@ class CfeRunningMacro implements injected.RunningMacro { @override Future executeTypesPhase( - MacroTarget target, TypePhaseIntrospector typePhaseIntrospector) async { + macros_api_v1.MacroTarget target, + macros_api_v1.TypePhaseIntrospector typePhaseIntrospector) async { // TODO(davidmorgan): this is a hack to access CFE internals; remove. introspector = typePhaseIntrospector; - return CfeMacroExecutionResult( + return await CfeMacroExecutionResult.dartModelToInjected( target, await _impl._host.augment( name, AugmentRequest(phase: 1, target: target.qualifiedName))); } } -/// Converts [AugmentResponse] to [injected.MacroExecutionResult]. +/// Converts [AugmentResponse] to [macros_api_v1.MacroExecutionResult]. /// /// TODO(davidmorgan): add to `AugmentationResponse` to cover all the /// functionality of `MacroExecutionResult`. -class CfeMacroExecutionResult implements injected.MacroExecutionResult { - final MacroTarget target; - final AugmentResponse augmentResponse; - - CfeMacroExecutionResult(this.target, this.augmentResponse); +class CfeMacroExecutionResult implements macros_api_v1.MacroExecutionResult { + final macros_api_v1.MacroTarget target; + @override + final Map> + typeAugmentations; + + CfeMacroExecutionResult( + this.target, Iterable declarations) + // TODO(davidmorgan): this assumes augmentations are for the macro + // application target. Instead, it should be explicit in + // `AugmentResponse`. + : typeAugmentations = { + // TODO(davidmorgan): empty augmentations response breaks the test, + // it's not clear why. + if (declarations.isNotEmpty) + (target as macros_api_v1.Declaration).identifier: declarations + }; + + static Future dartModelToInjected( + macros_api_v1.MacroTarget target, AugmentResponse augmentResponse) async { + final declarations = []; + for (final augmentation in augmentResponse.augmentations) { + declarations.add(macros_api_v1.DeclarationCode.fromParts( + await _resolveNames(augmentation.code))); + } + return CfeMacroExecutionResult(target, declarations); + } @override - List get diagnostics => []; + List get diagnostics => []; @override - Map> get enumValueAugmentations => {}; + Map> + get enumValueAugmentations => {}; @override - MacroException? get exception => null; + macros_api_v1.MacroException? get exception => null; @override - Map get extendsTypeAugmentations => {}; + Map + get extendsTypeAugmentations => {}; @override - Map> get interfaceAugmentations => - {}; + Map> + get interfaceAugmentations => {}; @override - Iterable get libraryAugmentations => {}; + Iterable get libraryAugmentations => {}; @override - Map> get mixinAugmentations => {}; + Map> + get mixinAugmentations => {}; @override Iterable get newTypeNames => []; @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`. - // TODO(davidmorgan): empty augmentations response breaks the test, - // it's not clear why. - if (augmentResponse.augmentations.isNotEmpty) - (target as Declaration).identifier: augmentResponse.augmentations - .map((a) => DeclarationCode.fromParts([a.code])) - .toList(), - }; } -extension MacroTargetExtension on MacroTarget { +extension MacroTargetExtension on macros_api_v1.MacroTarget { QualifiedName get qualifiedName { - final identifier = ((this as Declaration).identifier as cfe.IdentifierImpl) - .resolveIdentifier(); + final identifier = + ((this as macros_api_v1.Declaration).identifier as cfe.IdentifierImpl) + .resolveIdentifier(); return QualifiedName(uri: '${identifier.uri}', name: identifier.name); } } + +/// Converts [codes] to a list of `String` and `Identifier`. +// TODO(davidmorgan): share this code with `package:_analyzer_macros`. +Future> _resolveNames(List codes) async { + // Find the set of unique [QualifiedName]s used. + final qualifiedNameStrings = {}; + for (final code in codes) { + if (code.type == CodeType.qualifiedName) { + qualifiedNameStrings.add(code.asQualifiedName.asString); + } + } + + // Create futures looking up their [Identifier]s, then `await` in parallel. + final qualifiedNamesList = + qualifiedNameStrings.map(QualifiedName.parse).toList(); + final identifierFutures = >[]; + for (final qualifiedName in qualifiedNamesList) { + identifierFutures.add((introspector as macros_api_v1.TypePhaseIntrospector) + // ignore: deprecated_member_use + .resolveIdentifier(Uri.parse(qualifiedName.uri), qualifiedName.name)); + } + final identifiers = await Future.wait(identifierFutures); + + // Build the result using the looked up [Identifier]s. + final identifiersByQualifiedNameStrings = + Map.fromIterables(qualifiedNameStrings, identifiers); + final result = []; + for (final code in codes) { + if (code.type == CodeType.string) { + result.add(code.asString); + } else if (code.type == CodeType.qualifiedName) { + final qualifiedName = code.asQualifiedName; + result.add(identifiersByQualifiedNameStrings[qualifiedName.asString]!); + } + } + return result; +} diff --git a/pkgs/_macro_client/test/macro_client_test.dart b/pkgs/_macro_client/test/macro_client_test.dart index 2ad447b4..f27494e3 100644 --- a/pkgs/_macro_client/test/macro_client_test.dart +++ b/pkgs/_macro_client/test/macro_client_test.dart @@ -128,9 +128,16 @@ void main() { 'type': 'AugmentResponse', 'value': { 'augmentations': [ - {'code': 'int get x => 3;'} + { + 'code': [ + { + 'type': 'String', + 'value': 'int get x => 3;', + } + ] + } ] - } + }, }); }); @@ -205,10 +212,16 @@ void main() { 'value': { 'augmentations': [ { - 'code': '// {"uris":{"package:foo/foo.dart":{"scopes":{}}}}', + 'code': [ + { + 'type': 'String', + 'value': + '// {"uris":{"package:foo/foo.dart":{"scopes":{}}}}' + } + ] } ] - } + }, }, ); }); @@ -276,10 +289,16 @@ void main() { 'value': { 'augmentations': [ { - 'code': '// {"uris":{"package:foo/foo1.dart":{"scopes":{}}}}', + 'code': [ + { + 'type': 'String', + 'value': + '// {"uris":{"package:foo/foo1.dart":{"scopes":{}}}}' + } + ] } ] - } + }, }, ); @@ -291,10 +310,16 @@ void main() { 'value': { 'augmentations': [ { - 'code': '// {"uris":{"package:foo/foo2.dart":{"scopes":{}}}}', + 'code': [ + { + 'type': 'String', + 'value': + '// {"uris":{"package:foo/foo2.dart":{"scopes":{}}}}' + } + ] } ] - } + }, }, ); }); diff --git a/pkgs/_macro_host/test/macro_host_test.dart b/pkgs/_macro_host/test/macro_host_test.dart index df30bdb8..ef5424a4 100644 --- a/pkgs/_macro_host/test/macro_host_test.dart +++ b/pkgs/_macro_host/test/macro_host_test.dart @@ -34,8 +34,9 @@ void main() { expect( await host.augment(macroAnnotation, AugmentRequest(phase: 2)), - Scope.macro.run(() => AugmentResponse( - augmentations: [Augmentation(code: 'int get x => 3;')]))); + Scope.macro.run(() => AugmentResponse(augmentations: [ + Augmentation(code: [Code.string('int get x => 3;')]) + ]))); }); test('hosts a macro, responds to queries', () async { @@ -62,9 +63,10 @@ void main() { target: QualifiedName( uri: 'package:foo/foo.dart', name: 'Foo'))), Scope.macro.run(() => AugmentResponse(augmentations: [ - Augmentation( - code: - '// {"uris":{"package:foo/foo.dart":{"scopes":{}}}}') + Augmentation(code: [ + Code.string( + '// {"uris":{"package:foo/foo.dart":{"scopes":{}}}}') + ]) ]))); }); diff --git a/pkgs/_test_macros/lib/declare_x_macro.dart b/pkgs/_test_macros/lib/declare_x_macro.dart index 83e19b14..ece158b3 100644 --- a/pkgs/_test_macros/lib/declare_x_macro.dart +++ b/pkgs/_test_macros/lib/declare_x_macro.dart @@ -6,6 +6,8 @@ import 'package:dart_model/dart_model.dart'; import 'package:macro/macro.dart'; import 'package:macro_service/macro_service.dart'; +import 'templating.dart'; + /// Adds a getter `int get x` to the class. class DeclareX { const DeclareX(); @@ -27,6 +29,6 @@ class DeclareXImplementation implements Macro { // TODO(davidmorgan): still need to pass through the augment target. return AugmentResponse( - augmentations: [Augmentation(code: 'int get x => 3;')]); + augmentations: [Augmentation(code: expandTemplate('int get x => 3;'))]); } } diff --git a/pkgs/_test_macros/lib/json_codable.dart b/pkgs/_test_macros/lib/json_codable.dart index 874f0bad..919ec1df 100644 --- a/pkgs/_test_macros/lib/json_codable.dart +++ b/pkgs/_test_macros/lib/json_codable.dart @@ -6,6 +6,8 @@ import 'package:dart_model/dart_model.dart'; import 'package:macro/macro.dart'; import 'package:macro_service/macro_service.dart'; +import 'templating.dart'; + /// A macro which adds a `fromJson(Map json)` JSON decoding /// constructor to a class. class JsonCodable { @@ -35,11 +37,11 @@ class JsonCodableImplementation implements Macro { Future phase2(Host host, AugmentRequest request) async { final target = request.target; return AugmentResponse(augmentations: [ - Augmentation(code: ''' + Augmentation(code: expandTemplate(''' // TODO(davidmorgan): see https://github.com/dart-lang/macros/issues/80. // external ${target.name}.fromJson($_jsonMapType json); // external $_jsonMapType toJson(); - '''), + ''')) ]); } @@ -61,10 +63,10 @@ class JsonCodableImplementation implements Macro { // 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: ''' + result.add(Augmentation(code: expandTemplate(''' augment ${target.name}.fromJson($_jsonMapType json) : ${initializers.join(',\n')}; -''')); +'''))); final serializers = []; for (final field @@ -80,13 +82,13 @@ ${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: ''' + result.add(Augmentation(code: expandTemplate(''' augment $_jsonMapType toJson() { final json = $_jsonMapTypeForLiteral{}; ${serializers.join('')} return json; } -''')); +'''))); return AugmentResponse(augmentations: result); } @@ -182,8 +184,3 @@ ${serializers.join('')} 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/lib/query_class.dart b/pkgs/_test_macros/lib/query_class.dart index 246ef06f..6f652b2a 100644 --- a/pkgs/_test_macros/lib/query_class.dart +++ b/pkgs/_test_macros/lib/query_class.dart @@ -8,6 +8,8 @@ import 'package:dart_model/dart_model.dart'; import 'package:macro/macro.dart'; import 'package:macro_service/macro_service.dart'; +import 'templating.dart'; + /// Applies a macro which sends an empty query, and outputs an augmentation /// that is the query result as a comment. class QueryClass { @@ -31,7 +33,8 @@ class QueryClassImplementation implements Macro { final model = await host.query(Query( target: request.target, )); - return AugmentResponse( - augmentations: [Augmentation(code: '// ${json.encode(model)}')]); + return AugmentResponse(augmentations: [ + Augmentation(code: expandTemplate('// ${json.encode(model)}')) + ]); } } diff --git a/pkgs/_test_macros/lib/templating.dart b/pkgs/_test_macros/lib/templating.dart new file mode 100644 index 00000000..98f75608 --- /dev/null +++ b/pkgs/_test_macros/lib/templating.dart @@ -0,0 +1,42 @@ +// 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'; + +// TODO(davidmorgan): figure out where this should go. +extension TemplatingExtension on QualifiedName { + String get code => '{{$uri#$name}}'; +} + +/// Converts [template] to a mix of `Identifier` and `String`. +/// +/// References of the form `{{uri#name}}` become [QualifiedName] wrapped in +/// [Code.qualifiedName], everything else becomes `String`. +/// +/// TODO(davidmorgan): figure out where this should go. +List expandTemplate(String template) { + final result = []; + var index = 0; + while (index < template.length) { + final start = template.indexOf('{{', index); + if (start == -1) { + result.add(Code.string(template.substring(index))); + break; + } + result.add(Code.string(template.substring(index, start))); + final end = template.indexOf('}}', start); + if (end == -1) { + throw ArgumentError('Unmatched opening brace: $template'); + } + final name = template.substring(start + 2, end); + final parts = name.split('#'); + if (parts.length != 2) { + throw ArgumentError('Expected "uri#name" in: $name'); + } + result + .add(Code.qualifiedName(QualifiedName(uri: parts[0], name: parts[1]))); + index = end + 2; + } + return result; +} diff --git a/pkgs/dart_model/lib/src/dart_model.g.dart b/pkgs/dart_model/lib/src/dart_model.g.dart index 8a7b4f30..a23d2826 100644 --- a/pkgs/dart_model/lib/src/dart_model.g.dart +++ b/pkgs/dart_model/lib/src/dart_model.g.dart @@ -6,21 +6,72 @@ import 'package:dart_model/src/json_buffer/json_buffer_builder.dart'; // ignore: implementation_imports,unused_import,prefer_relative_imports import 'package:dart_model/src/scopes.dart'; -/// An augmentation to Dart code. TODO(davidmorgan): this is a placeholder. +/// An augmentation to Dart code. extension type Augmentation.fromJson(Map node) implements Object { static final TypedMapSchema _schema = TypedMapSchema({ - 'code': Type.stringPointer, + 'code': Type.closedListPointer, }); Augmentation({ - String? code, + List? code, }) : this.fromJson(Scope.createMap( _schema, code, )); /// Augmentation code. - String get code => node['code'] as String; + List get code => (node['code'] as List).cast(); +} + +enum CodeType { + // Private so switches must have a default. See `isKnown`. + _unknown, + qualifiedName, + string; + + bool get isKnown => this != _unknown; +} + +extension type Code.fromJson(Map node) implements Object { + static final TypedMapSchema _schema = TypedMapSchema({ + 'type': Type.stringPointer, + 'value': Type.anyPointer, + }); + static Code qualifiedName(QualifiedName qualifiedName) => + Code.fromJson(Scope.createMap( + _schema, + 'QualifiedName', + qualifiedName, + )); + static Code string(String string) => Code.fromJson(Scope.createMap( + _schema, + 'String', + string, + )); + CodeType get type { + switch (node['type'] as String) { + case 'QualifiedName': + return CodeType.qualifiedName; + case 'String': + return CodeType.string; + default: + return CodeType._unknown; + } + } + + QualifiedName get asQualifiedName { + if (node['type'] != 'QualifiedName') { + throw StateError('Not a QualifiedName.'); + } + return QualifiedName.fromJson(node['value'] as Map); + } + + String get asString { + if (node['type'] != 'String') { + throw StateError('Not a String.'); + } + return node['value'] as String; + } } /// The type-hierarchy representation of the type `dynamic`. diff --git a/schemas/dart_model.schema.json b/schemas/dart_model.schema.json index 18e1e6fb..c941b56d 100644 --- a/schemas/dart_model.schema.json +++ b/schemas/dart_model.schema.json @@ -8,14 +8,40 @@ "$defs": { "Augmentation": { "type": "object", - "description": "An augmentation to Dart code. TODO(davidmorgan): this is a placeholder.", + "description": "An augmentation to Dart code.", "properties": { "code": { - "type": "string", - "description": "Augmentation code." + "type": "array", + "description": "Augmentation code.", + "items": { + "$ref": "#/$defs/Code" + } } } }, + "Code": { + "type": "object", + "description": "Code that is part of augmentations to Dart code.", + "properties": { + "type": { + "type": "string" + }, + "value": { + "oneOf": [ + { + "$ref": "#/$defs/QualifiedName" + }, + { + "type": "string" + } + ] + }, + "required": [ + "type", + "value" + ] + } + }, "DynamicTypeDesc": { "type": "null", "description": "The type-hierarchy representation of the type `dynamic`." diff --git a/tool/dart_model_generator/lib/definitions.dart b/tool/dart_model_generator/lib/definitions.dart index 90055fcd..b0ddb9f3 100644 --- a/tool/dart_model_generator/lib/definitions.dart +++ b/tool/dart_model_generator/lib/definitions.dart @@ -66,12 +66,19 @@ static Protocol handshakeProtocol = Protocol( Definition.clazz( 'Augmentation', createInBuffer: true, - description: 'An augmentation to Dart code. ' - 'TODO(davidmorgan): this is a placeholder.', + description: 'An augmentation to Dart code.', properties: [ - Property('code', type: 'String', description: 'Augmentation code.'), + Property('code', + type: 'List', description: 'Augmentation code.'), ], ), + Definition.union( + 'Code', + createInBuffer: true, + description: 'Code that is part of augmentations to Dart code.', + types: ['QualifiedName', 'String'], + properties: [], + ), Definition.nullTypedef('DynamicTypeDesc', description: 'The type-hierarchy representation of the type `dynamic`.'), diff --git a/tool/dart_model_generator/lib/generate_dart_model.dart b/tool/dart_model_generator/lib/generate_dart_model.dart index d497ba39..1a7e0f32 100644 --- a/tool/dart_model_generator/lib/generate_dart_model.dart +++ b/tool/dart_model_generator/lib/generate_dart_model.dart @@ -647,11 +647,17 @@ class UnionTypeDefinition implements Definition { result ..writeln('$type get as$type {') ..writeln("if (node['type'] != '$type') " - "{ throw StateError('Not a $type.'); }") - ..writeln('return $type.fromJson' + "{ throw StateError('Not a $type.'); }"); + // TODO(davidmorgan): this special case allows `String` to be in a union + // type, see if there is a nice way to generalize to other primitives. + if (type == 'String') { + result.writeln("return node['value'] as String;"); + } else { + result.writeln('return $type.fromJson' "(node['value'] as " - '${context.lookupDefinition(type).representationTypeName});') - ..writeln('}'); + '${context.lookupDefinition(type).representationTypeName});'); + } + result.writeln('}'); } for (final property in properties) {