From 2de33b9cf349bb4976c41de217bf9b8c35b35932 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Thu, 30 May 2024 09:35:54 +0200 Subject: [PATCH] [macros] Add `dart_model` exploration code. --- working/macros/dart_model/README.md | 22 ++ .../macros/dart_model/analysis_options.yaml | 91 +++++++ .../dart_model/dart_model/lib/delta.dart | 106 ++++++++ .../dart_model/dart_model/lib/model.dart | 256 ++++++++++++++++++ .../dart_model/dart_model/lib/query.dart | 95 +++++++ .../macros/dart_model/dart_model/pubspec.yaml | 10 + .../dart_model/test/delta_test.dart | 137 ++++++++++ .../dart_model/test/model_test.dart | 53 ++++ .../dart_model/test/query_test.dart | 68 +++++ .../lib/dart_model_analyzer_service.dart | 205 ++++++++++++++ .../dart_model_analyzer_service/pubspec.yaml | 15 + .../dart_model/dart_model_repl/bin/main.dart | 9 + .../dart_model_repl/lib/dart_model_repl.dart | 102 +++++++ .../dart_model/dart_model_repl/pubspec.yaml | 19 ++ .../dart_model/macro_client/lib/macro.dart | 9 + .../macro_client/lib/macro_client.dart | 23 ++ .../macro_client/lib/socket_service.dart | 53 ++++ .../dart_model/macro_client/pubspec.yaml | 15 + .../dart_model/macro_host/bin/main.dart | 18 ++ .../dart_model/macro_host/lib/macro_host.dart | 29 ++ .../macro_host/lib/socket_client.dart | 38 +++ .../macros/dart_model/macro_host/pubspec.yaml | 19 ++ .../macro_protocol/lib/message.dart | 66 +++++ .../dart_model/macro_protocol/pubspec.yaml | 12 + working/macros/dart_model/testing/presubmit | 18 ++ .../testing/scratch/lib/scratch.dart | 16 ++ .../dart_model/testing/scratch/pubspec.yaml | 12 + .../lib/annotations.dart | 7 + .../test_macro_annotations/pubspec.yaml | 5 + .../testing/test_macros/bin/main.dart | 10 + .../testing/test_macros/lib/first_macro.dart | 18 ++ .../testing/test_macros/pubspec.yaml | 17 ++ 32 files changed, 1573 insertions(+) create mode 100644 working/macros/dart_model/README.md create mode 100644 working/macros/dart_model/analysis_options.yaml create mode 100644 working/macros/dart_model/dart_model/lib/delta.dart create mode 100644 working/macros/dart_model/dart_model/lib/model.dart create mode 100644 working/macros/dart_model/dart_model/lib/query.dart create mode 100644 working/macros/dart_model/dart_model/pubspec.yaml create mode 100644 working/macros/dart_model/dart_model/test/delta_test.dart create mode 100644 working/macros/dart_model/dart_model/test/model_test.dart create mode 100644 working/macros/dart_model/dart_model/test/query_test.dart create mode 100644 working/macros/dart_model/dart_model_analyzer_service/lib/dart_model_analyzer_service.dart create mode 100644 working/macros/dart_model/dart_model_analyzer_service/pubspec.yaml create mode 100644 working/macros/dart_model/dart_model_repl/bin/main.dart create mode 100644 working/macros/dart_model/dart_model_repl/lib/dart_model_repl.dart create mode 100644 working/macros/dart_model/dart_model_repl/pubspec.yaml create mode 100644 working/macros/dart_model/macro_client/lib/macro.dart create mode 100644 working/macros/dart_model/macro_client/lib/macro_client.dart create mode 100644 working/macros/dart_model/macro_client/lib/socket_service.dart create mode 100644 working/macros/dart_model/macro_client/pubspec.yaml create mode 100644 working/macros/dart_model/macro_host/bin/main.dart create mode 100644 working/macros/dart_model/macro_host/lib/macro_host.dart create mode 100644 working/macros/dart_model/macro_host/lib/socket_client.dart create mode 100644 working/macros/dart_model/macro_host/pubspec.yaml create mode 100644 working/macros/dart_model/macro_protocol/lib/message.dart create mode 100644 working/macros/dart_model/macro_protocol/pubspec.yaml create mode 100755 working/macros/dart_model/testing/presubmit create mode 100644 working/macros/dart_model/testing/scratch/lib/scratch.dart create mode 100644 working/macros/dart_model/testing/scratch/pubspec.yaml create mode 100644 working/macros/dart_model/testing/test_macro_annotations/lib/annotations.dart create mode 100644 working/macros/dart_model/testing/test_macro_annotations/pubspec.yaml create mode 100644 working/macros/dart_model/testing/test_macros/bin/main.dart create mode 100644 working/macros/dart_model/testing/test_macros/lib/first_macro.dart create mode 100644 working/macros/dart_model/testing/test_macros/pubspec.yaml diff --git a/working/macros/dart_model/README.md b/working/macros/dart_model/README.md new file mode 100644 index 0000000000..7a50015608 --- /dev/null +++ b/working/macros/dart_model/README.md @@ -0,0 +1,22 @@ +# `dart_model` exploration + +Code exploring +[a query-like API](https://github.com/dart-lang/language/issues/3706) for +macros, in particular with regard to incremental build performance and +convenience for macro authors. + +_This code will be deleted/archived, do not use it for anything!_ + +Packages: + +`dart_model` is a standalone data model, the input to a macro;\ +`dart_model_analyzer_service` serves `dart_model` queries using the analyzer +as a library;\ +`dart_model_repl` is a REPL that can issue queries and watch code for changes\ +`macro_client` is for writing "macros";\ +`macro_host` hosts a set of "macros" running against a codebase;\ +`macro_protocol` is how "macros" communicate with their host;\ +`testing` is for test "macros" and experiments with them. + +The "macros" referred to in this exploration are independent of the in-progress +macro implementation, hence the "scare quotes". diff --git a/working/macros/dart_model/analysis_options.yaml b/working/macros/dart_model/analysis_options.yaml new file mode 100644 index 0000000000..ea3c5182a6 --- /dev/null +++ b/working/macros/dart_model/analysis_options.yaml @@ -0,0 +1,91 @@ +linter: + rules: + # From package:lints/core.yaml + - avoid_empty_else + - avoid_relative_lib_imports + - avoid_shadowing_type_parameters + - avoid_types_as_parameter_names + - await_only_futures + - camel_case_extensions + - camel_case_types + - collection_methods_unrelated_type + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - empty_catches + - file_names + - hash_and_equals + - implicit_call_tearoffs + - library_annotations + - no_duplicate_case_values + - no_wildcard_variable_uses + - non_constant_identifier_names + - null_check_on_nullable_type_parameter + - prefer_generic_function_type_aliases + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + - prefer_typing_uninitialized_variables + - provide_deprecation_message + - secure_pubspec_urls + - type_literal_in_constant_pattern + - unnecessary_overrides + - unrelated_type_equality_checks + - use_string_in_part_of_directives + - valid_regexps + - void_checks + # From package:lints/recommended.yaml + - annotate_overrides + - avoid_function_literals_in_foreach_calls + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_single_cascade_in_expression_statements + - constant_identifier_names + - control_flow_in_finally + - empty_constructor_bodies + - empty_statements + - exhaustive_cases + - implementation_imports + - library_prefixes + - library_private_types_in_public_api + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - null_closures + - overridden_fields + - package_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_contains + - prefer_final_fields + - prefer_for_elements_to_map_fromIterable + - prefer_function_declarations_over_variables + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_interpolation_to_compose_strings + - prefer_is_not_operator + - prefer_null_aware_operators + - prefer_spread_collections + - recursive_getters + - slash_for_doc_comments + - type_init_formals + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_constructor_name + - unnecessary_getters_setters + - unnecessary_late + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - use_super_parameters diff --git a/working/macros/dart_model/dart_model/lib/delta.dart b/working/macros/dart_model/dart_model/lib/delta.dart new file mode 100644 index 0000000000..9f3f0111da --- /dev/null +++ b/working/macros/dart_model/dart_model/lib/delta.dart @@ -0,0 +1,106 @@ +// 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 'dart:convert'; + +import 'package:collection/collection.dart'; + +import 'model.dart'; + +extension type Update.fromJson(List node) { + Update({ + required Path path, + required Object? value, + }) : this.fromJson([path, value]); + + Path get path => node[0] as Path; + Object? get value => node[1]; +} + +extension type Removal.fromJson(List node) { + Removal({ + required Path path, + }) : this.fromJson(path.path); + + Path get path => node as Path; +} + +extension type Delta.fromJson(Map node) { + Delta({ + List? updates, + List? removals, + }) : this.fromJson({ + 'updates': updates ?? [], + 'removals': removals ?? [], + }); + + static Delta compute(Model previous, Model current) { + final updates = []; + final removals = []; + _compute(previous, current, Path([]), updates, removals); + return Delta(updates: updates, removals: removals); + } + + static void _compute(Model previous, Model current, Path path, + List updates, List removals) { + for (final key + in previous.node.keys.followedBy(current.node.keys).toSet()) { + final keyIsInPrevious = previous.node.containsKey(key); + final keyIsInCurrent = current.node.containsKey(key); + + if (keyIsInPrevious && !keyIsInCurrent) { + removals.add(Removal(path: path.followedByOne(key))); + } else if (keyIsInPrevious && keyIsInCurrent) { + // It's either the same or a change. + final previousValue = previous.node[key]!; + final currentValue = current.node[key]!; + + if (currentValue is Map) { + if (previousValue is Map) { + _compute(previousValue as Model, currentValue as Model, + path.followedByOne(key), updates, removals); + } else { + updates.add( + Update(path: path.followedByOne(key), value: currentValue)); + } + } else if (currentValue is String) { + if (previousValue is! String || previousValue != currentValue) { + updates.add( + Update(path: path.followedByOne(key), value: currentValue)); + } + } else if (currentValue is List) { + if (previousValue is! List || + !const DeepCollectionEquality() + .equals(previousValue, currentValue)) { + updates.add( + Update(path: path.followedByOne(key), value: currentValue)); + } + } else { + throw 'Not sure what to do: $previousValue $currentValue'; + } + } else { + // It's new. + updates.add( + Update(path: path.followedByOne(key), value: current.node[key])); + } + } + } + + bool get isEmpty => updates.isEmpty && removals.isEmpty; + + List get updates => (node['updates'] as List).cast(); + + List get removals => (node['removals'] as List).cast(); + + String prettyPrint() => const JsonEncoder.withIndent(' ').convert(node); + + void update(Model previous) { + for (final update in updates) { + previous.updateAtPath(update.path, update.value); + } + for (final removal in removals) { + previous.removeAtPath(removal.path); + } + } +} diff --git a/working/macros/dart_model/dart_model/lib/model.dart b/working/macros/dart_model/dart_model/lib/model.dart new file mode 100644 index 0000000000..4e87bd8dbf --- /dev/null +++ b/working/macros/dart_model/dart_model/lib/model.dart @@ -0,0 +1,256 @@ +// 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 'dart:convert'; + +import 'package:collection/collection.dart'; + +Map _roots = Map.identity(); +Map _names = Map.identity(); + +final class QualifiedName { + final String uri; + final String name; + + QualifiedName({required this.uri, required this.name}); + + static QualifiedName? tryParse(String qualifiedName) { + final index = qualifiedName.indexOf('#'); + if (index == -1) return null; + return QualifiedName( + uri: qualifiedName.substring(0, index), + name: qualifiedName.substring(index + 1)); + } + + @override + bool operator ==(Object other) => + other is QualifiedName && other.toString() == toString(); + + @override + int get hashCode => toString().hashCode; + + @override + String toString() => '$uri#$name'; +} + +extension type Model.fromJson(Map node) { + Model() : this.fromJson({}); + + Iterable get uris => node.keys; + + Library? library(String uri) => node[uri] as Library?; + Scope? scope(QualifiedName qualifiedName) => + library(qualifiedName.uri)?.scope(qualifiedName.name); + + void ensure(String name) { + if (node.containsKey(name)) return; + add(name, Library()); + } + + void add(String name, Library library) { + if (node.containsKey(name)) throw ArgumentError('Already present: $name'); + _names[library] = name; + _roots[library] = this; + node[name] = library; + } + + bool hasPath(Path path) { + if (path.path.length == 1) { + return node.containsKey(path.path.first); + } else { + final next = node[path.path.first]; + if (next is Map) { + return (next as Model).hasPath(path.skipOne()); + } else { + return false; + } + } + } + + Object? getAtPath(Path path) { + if (path.path.length == 1) { + return node[path.path.first]; + } else { + final next = node[path.path.first]; + if (next is Map) { + return (next as Model).getAtPath(path.skipOne()); + } else { + throw ArgumentError('Model does not have path: $path'); + } + } + } + + void updateAtPath(Path path, Object? value) { + if (path.path.length == 1) { + node[path.path.single] = value; + } else { + if (!node.containsKey(path.path.first)) { + node[path.path.first] = {}; + } + (node[path.path.first] as Model).updateAtPath(path.skipOne(), value); + } + } + + void removeAtPath(Path path) { + if (path.path.length == 1) { + node.remove(path.path.single); + } else { + final first = path.path.first; + final rest = path.skipOne(); + if (first == '*') { + for (final key in node.keys) { + final next = node[key]; + if (next is Map) { + (next as Model).removeAtPath(rest); + } + } + } else { + (node[path.path.first] as Model).removeAtPath(rest); + } + } + } + + bool equals(Model other) => + const DeepCollectionEquality().equals(node, other.node); + + String prettyPrint() => const JsonEncoder.withIndent(' ').convert(node); +} + +extension type Library.fromJson(Map node) implements Object { + Library() : this.fromJson({}); + + Iterable get names => node.keys; + + Scope? scope(String uri) => node[uri] as Scope?; + + void add(String name, Scope scope) { + _names[scope] = name; + _roots[scope] = _roots[this]!; + node[name] = scope; + } +} + +extension type Scope.fromJson(Map node) implements Object { + Scope() : this.fromJson({}); + + String get name => _names[this]!; + + Interface? get asInterface => Interface.fromJson(node); +} + +extension type Class.fromJson(Map node) implements Scope { + Class( + {bool? abstract, + List? annotations, + QualifiedName? supertype, + Iterable? interfaces, + Map? members}) + : this.fromJson({ + 'properties': ['class', if (abstract == true) 'abstract'], + if (annotations != null) 'annotations': annotations.toList(), + if (supertype != null) 'supertype': supertype.toString(), + if (interfaces != null) + 'interfaces': interfaces.map((i) => i.toString()).toList(), + if (members != null) 'members': members, + }); + + String get name => _names[this]!; +} + +extension type Interface.fromJson(Map node) implements Scope { + String get name => _names[this]!; + + bool get isClass => (node['properties'] as List).contains('class'); + bool get isAbstract => (node['properties'] as List).contains('abstract'); + + Interface? get supertype { + if (!node.containsKey('supertype')) return null; + final name = QualifiedName.tryParse(node['supertype'] as String); + if (name == null) return null; + return _roots[this]!.scope(name)?.asInterface; + } + + List get annotations => (node['annotations'] as List).cast(); + + Map get members => (node['members'] as Map).cast(); + + Iterable get interfaces { + final result = []; + for (final interface in (node['interfaces'] as List).cast()) { + final name = QualifiedName.tryParse(interface)!; + result.add(_roots[this]!.scope(name)!.asInterface!); + } + return result; + } + + Iterable get allSupertypes sync* { + if (supertype != null) { + yield supertype!; + yield* supertype!.allSupertypes; + } + yield* interfaces; + } +} + +extension type Member.fromJson(Map node) { + Member( + {required bool getter, + required bool abstract, + required bool method, + required bool field, + required bool static, + required bool synthetic}) + : this.fromJson({ + 'properties': [ + if (abstract) 'abstract', + if (getter) 'getter', + if (method) 'method', + if (field) 'field', + if (static) 'static', + if (synthetic) 'synthetic' + ] + }); + + String get name => _names[this]!; + + bool get isAbstract => (node['properties'] as List).contains('abstract'); + bool get isField => (node['properties'] as List).contains('field'); + bool get isGetter => (node['properties'] as List).contains('getter'); + bool get isMethod => (node['properties'] as List).contains('method'); + bool get isStatic => (node['properties'] as List).contains('static'); + bool get isSynthetic => (node['properties'] as List).contains('synthetic'); +} + +extension type Annotation.fromJson(Map node) { + Annotation({required QualifiedName type, Value? value}) + : this.fromJson({ + 'type': type.toString(), + if (value != null) 'value': value, + }); + + QualifiedName get type => QualifiedName.tryParse(node['type'] as String)!; + + Value get value => node['value'] as Value; +} + +extension type Value.fromJson(Object? value) { + // TODO(davidmorgan): check type. + Value.primitive(Object? value) : this.fromJson(value); + Value.object({ + required Map fields, + }) : this.fromJson(fields); + + Value? field(String name) => (value as Map)[name]; +} + +extension type Path.fromJson(List node) { + Path(List path) : this.fromJson(path); + + List get path => (node as List).cast(); + + Path followedByOne(String element) => + Path(path.followedBy([element]).toList()); + + Path skipOne() => Path(path.skip(1).toList()); +} diff --git a/working/macros/dart_model/dart_model/lib/query.dart b/working/macros/dart_model/dart_model/lib/query.dart new file mode 100644 index 0000000000..6c600e76f0 --- /dev/null +++ b/working/macros/dart_model/dart_model/lib/query.dart @@ -0,0 +1,95 @@ +// 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 'delta.dart'; +import 'model.dart'; + +extension type Query.fromJson(Map node) { + Query({List? operations}) + : this.fromJson({'operations': operations ?? []}); + + Query.uri(String uri) + : this(operations: [ + Operation.include([ + Path([uri]) + ]) + ]); + + Query.qualifiedName({required String uri, required String name}) + : this(operations: [ + Operation.include([ + Path([uri, name]) + ]) + ]); + + Query.annotation(QualifiedName qualifiedName) + : this(operations: [Operation.annotation(qualifiedName)]); + + List get operations => (node['operations'] as List).cast(); + + Model query(Model model) { + Model result = Model(); + + for (final operation in operations) { + if (operation.isInclude) { + for (final path in operation.paths) { + if (model.hasPath(path)) { + final node = model.getAtPath(path); + result.updateAtPath(path, node); + } + } + } else if (operation.isExclude) { + for (final path in operation.paths) { + model.removeAtPath(path); + } + } else if (operation.isFollow) { + // TODO(davidmorgan): implement. + } + } + + return result; + } + + // TODO(davidmorgan): implement properly. + String get firstUri => + operations.firstWhere((o) => o.isInclude).paths[0].path.first; + + String? get firstName { + final operation = operations.firstWhere((o) => o.isInclude); + final path = operation.paths[0]; + return path.path.length > 1 ? path.path[1] : null; + } +} + +extension type Operation.fromJson(Map node) { + // TODO(davidmorgan): this should be expessable as a general query, not + // special cased. + Operation.annotation(QualifiedName qualifiedName) + : this.fromJson( + {'type': 'annotation', 'annotationType': qualifiedName.toString()}); + + Operation.include(List include) + : this.fromJson({'type': 'include', 'paths': include}); + + Operation.exclude(List exclude) + : this.fromJson({'type': 'exclude', 'paths': exclude}); + + Operation.followTypes(int times) + : this.fromJson({'type': 'followTypes', 'times': times}); + + bool get isAnnotation => node['type'] == 'annotation'; + QualifiedName get annotationType => + QualifiedName.tryParse((node['annotationType'] as String))!; + + bool get isInclude => node['type'] == 'include'; + bool get isExclude => node['type'] == 'exclude'; + bool get isFollow => node['type'] == 'follow'; + + List get paths => (node['paths'] as List).cast(); +} + +abstract interface class Service { + Future query(Query query); + Future> watch(Query query); +} diff --git a/working/macros/dart_model/dart_model/pubspec.yaml b/working/macros/dart_model/dart_model/pubspec.yaml new file mode 100644 index 0000000000..e8dd26a4c3 --- /dev/null +++ b/working/macros/dart_model/dart_model/pubspec.yaml @@ -0,0 +1,10 @@ +name: dart_model +publish-to: none + +environment: + sdk: ^3.4.0 + +dev_dependencies: + test: ^1.25.0 +dependencies: + collection: ^1.18.0 diff --git a/working/macros/dart_model/dart_model/test/delta_test.dart b/working/macros/dart_model/dart_model/test/delta_test.dart new file mode 100644 index 0000000000..f6d4e527c6 --- /dev/null +++ b/working/macros/dart_model/dart_model/test/delta_test.dart @@ -0,0 +1,137 @@ +// 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/delta.dart'; +import 'package:dart_model/model.dart'; +import 'package:test/test.dart'; + +void main() { + group(Delta, () { + test('describes new data as updates', () { + final previous = Model.fromJson({'a': 'a', 'c': 'c'}); + final current = Model.fromJson({'a': 'a', 'b': 'b'}); + final delta = Delta.compute(previous, current); + + expect(delta.updates, [ + Update(path: Path(['b']), value: 'b') + ]); + }); + + test('describes deeply nested new data as updates', () { + final previous = Model.fromJson({ + 'a': { + 'b': {'c': 'd'} + }, + }); + final current = Model.fromJson({ + 'a': { + 'b': { + 'c': { + 'd': {'e': 'f'} + } + } + }, + }); + final delta = Delta.compute(previous, current); + + expect(delta.updates, [ + Update(path: Path(['a', 'b', 'c']), value: { + 'd': {'e': 'f'} + }) + ]); + }); + + test('describes changed data as updates', () { + final previous = Model.fromJson({'a': 'a', 'c': 'c'}); + final current = Model.fromJson({'a': 'a2', 'c': 'c'}); + final delta = Delta.compute(previous, current); + + expect(delta.updates, [ + Update(path: Path(['a']), value: 'a2') + ]); + }); + + test('describes deeply nested changed data as updates', () { + final previous = Model.fromJson({ + 'a': { + 'b': {'c': 'a'} + }, + 'c': 'c' + }); + final current = Model.fromJson({ + 'a': { + 'b': {'c': 'a2'} + }, + 'c': 'c' + }); + final delta = Delta.compute(previous, current); + + expect(delta.updates, [ + Update(path: Path(['a', 'b', 'c']), value: 'a2') + ]); + }); + + test('describes removed data', () { + final previous = Model.fromJson({'a': 'a', 'c': 'c'}); + final current = Model.fromJson({'a': 'a'}); + final delta = Delta.compute(previous, current); + + expect(delta.removals, [ + Removal(path: Path(['c'])) + ]); + }); + + test('describes deeply nested removed data', () { + final previous = Model.fromJson({ + 'a': 'a', + 'c': { + 'd': { + 'e': {'c': 'c'} + } + } + }); + final current = Model.fromJson({ + 'a': 'a', + 'c': { + 'd': {'e': {}} + } + }); + final delta = Delta.compute(previous, current); + + expect(delta.removals, [ + Removal(path: Path(['c', 'd', 'e', 'c'])) + ]); + }); + + test('can handle lists', () { + final previous = Model.fromJson({ + 'a': ['a'], + }); + final current = Model.fromJson({ + 'a': ['b'], + }); + final delta = Delta.compute(previous, current); + + expect(delta.updates, [ + Update(path: Path(['a']), value: ['b']) + ]); + }); + + test('can be applied to a model', () { + final previous = Model.fromJson({ + 'a': 'a', + 'b': 'b', + }); + final current = Model.fromJson({ + 'a': {'b': 'c'}, + 'b': {'c': 'a'}, + }); + final delta = Delta.compute(previous, current); + + expect(previous, isNot(current)); + delta.update(previous); + expect(previous, current); + }); + }); +} diff --git a/working/macros/dart_model/dart_model/test/model_test.dart b/working/macros/dart_model/dart_model/test/model_test.dart new file mode 100644 index 0000000000..ff3378d561 --- /dev/null +++ b/working/macros/dart_model/dart_model/test/model_test.dart @@ -0,0 +1,53 @@ +// 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/model.dart'; +import 'package:test/test.dart'; + +void main() { + group(Model, () { + final model = Model.fromJson({ + 'package:end_to_end_test/values.dart': { + 'SimpleValue': { + 'properties': ['abstract', 'class ', 'final'], + 'implements': [ + { + 'name': 'Built', + 'parameters': [ + 'package:end_to_end_test/values.dart#SimpleValue', + 'package:end_to_end_test/values.dart#SimpleValueBuilder', + ] + }, + ], + 'members': { + 'serializer': { + 'properties': ['static', 'getter'], + 'returnType': { + 'name': 'Serializer', + 'parameters': [ + 'package:end_to_end_test/values.dart#SimpleValue', + ] + } + }, + 'anInt': { + 'properties': ['abstract', 'getter'], + 'returnType': 'dart:core#int', + }, + 'aString': { + 'properties': ['abstract', 'getter'], + 'returnType': 'dart:core#String?', + }, + }, + } + } + }); + + test('works as described', () { + expect(model.uris, ['package:end_to_end_test/values.dart']); + + final library = model.library('package:end_to_end_test/values.dart')!; + expect(library.names, ['SimpleValue']); + }); + }); +} diff --git a/working/macros/dart_model/dart_model/test/query_test.dart b/working/macros/dart_model/dart_model/test/query_test.dart new file mode 100644 index 0000000000..1ad184ade3 --- /dev/null +++ b/working/macros/dart_model/dart_model/test/query_test.dart @@ -0,0 +1,68 @@ +// 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/model.dart'; +import 'package:dart_model/query.dart'; +import 'package:test/test.dart'; + +void main() { + group(Query, () { + test('can query by URI', () { + final model = Model.fromJson({ + 'package:dart_model/dart_model.dart': 'a', + 'package:dart_model/src/impl.dart': 'b' + }); + + final query = Query.uri('package:dart_model/dart_model.dart'); + final result = query.query(model); + + expect( + result, + Model.fromJson({ + 'package:dart_model/dart_model.dart': 'a', + })); + }); + + test('can query by URI and name', () { + final model = Model.fromJson({ + 'package:dart_model/dart_model.dart': {'a': 'a', 'b': 'b'}, + 'package:dart_model/src/impl.dart': {'b': 'b'}, + }); + + final query = Query.qualifiedName( + uri: 'package:dart_model/dart_model.dart', name: 'a'); + final result = query.query(model); + + expect( + result, + Model.fromJson({ + 'package:dart_model/dart_model.dart': {'a': 'a'}, + })); + }); + + test('can exclude by name', () { + final model = Model.fromJson({ + 'package:dart_model/dart_model.dart': {'a': 'a', 'b': 'b'}, + 'package:dart_model/src/impl.dart': {'b': 'b'}, + }); + + final query = Query(operations: [ + Operation.include([ + Path(['package:dart_model/dart_model.dart']) + ]), + Operation.exclude([ + Path(['*', 'b']), + ]) + ]); + + final result = query.query(model); + + expect( + result, + Model.fromJson({ + 'package:dart_model/dart_model.dart': {'a': 'a'}, + })); + }); + }); +} diff --git a/working/macros/dart_model/dart_model_analyzer_service/lib/dart_model_analyzer_service.dart b/working/macros/dart_model/dart_model_analyzer_service/lib/dart_model_analyzer_service.dart new file mode 100644 index 0000000000..047782788f --- /dev/null +++ b/working/macros/dart_model/dart_model_analyzer_service/lib/dart_model_analyzer_service.dart @@ -0,0 +1,205 @@ +// 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 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/analysis_context.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/session.dart'; +import 'package:analyzer/dart/element/element.dart'; +// ignore: implementation_imports +import 'package:analyzer/src/dart/constant/value.dart'; +import 'package:dart_model/delta.dart'; +import 'package:dart_model/model.dart'; +import 'package:dart_model/query.dart'; +import 'package:stream_transform/stream_transform.dart'; + +class DartModelAnalyzerService implements Service { + final AnalysisContext? context; + AnalysisSession? session; + + DartModelAnalyzerService({this.context, this.session}); + + @override + Future query(Query query) async { + if (context != null) { + session = context!.currentSession; + } + + if (query.operations.first.isAnnotation) { + final annotation = query.operations.first.annotationType; + final model = Model(); + for (final file in Directory(context!.contextRoot.root.path) + .listSync(recursive: true) + .whereType() + .where((f) => f.path.endsWith('.dart'))) { + final library = (await session!.getResolvedLibrary(file.path)) + as ResolvedLibraryResult; + for (final classElement + in library.element.topLevelElements.whereType()) { + final maybeModel = Model(); + _addInterfaceElement(maybeModel, classElement); + final maybeLibrary = maybeModel.library(maybeModel.uris.first)!; + final maybeName = maybeLibrary.names.first; + if (maybeLibrary + .scope(maybeName)! + .asInterface! + .annotations + .any((a) => a.type == annotation)) { + model.ensure(maybeModel.uris.first); + model + .library(maybeModel.uris.first)! + .add(maybeName, maybeLibrary.scope(maybeName)!); + } + } + } + return model; + } else { + final uri = query.firstUri; + await session!.getLibraryByUri(uri); + return queryLibrary( + (await session!.getLibraryByUri(uri) as LibraryElementResult).element, + query); + } + } + + @override + Future> watch(Query query) async { + if (context != null) { + session = context!.currentSession; + } + // TODO(davidmorgan): watch recursivly. + final changes = Directory('${context!.contextRoot.root.path}/lib').watch(); + Model previousModel = Model(); + return Stream.fromIterable([null]) + .followedBy(changes) + .asyncMap((change) async { + print('File change: $change'); + if (change != null) context!.changeFile(change.path); + final changed = await context!.applyPendingFileChanges(); + print('Changed: $changed'); + final model = await this.query(query); + var delta = Delta.compute(previousModel, model); + // Round trip to check serialization works. + // TODO(davidmorgan): add test coverage. + delta = Delta.fromJson(json.decode(json.encode(delta))); + previousModel = model; + return delta.isEmpty ? null : delta; + }) + .where((e) => e != null) + .map((e) => e!) + .asBroadcastStream(); + } + + Model queryLibrary(LibraryElement libraryElement, Query query) { + final result = Model(); + if (query.firstName == null) { + for (final classElement in libraryElement.topLevelElements + .whereType() + .toList()) { + _addInterfaceElement(result, classElement); + } + } else { + final classElement = libraryElement.topLevelElements + .whereType() + .where((e) => e.name == query.firstName) + .single; + _addInterfaceElement(result, classElement); + } + return result; + } + + QualifiedName _addInterfaceElement(Model result, InterfaceElement element) { + final uri = element.library.source.uri.toString(); + final name = element.displayName; + result.ensure(uri); + final supertype = element.supertype == null + ? null + : _addInterfaceElement(result, element.supertype!.element); + final interfaces = element.interfaces + .map((i) => _addInterfaceElement(result, i.element)) + .toList(); + final members = {}; + for (final field in element.fields) { + members[field.name] = Member( + abstract: field.isAbstract, + method: false, + field: true, + getter: false, + static: field.isStatic, + synthetic: field.isSynthetic); + } + for (final method in element.methods) { + members[method.name] = Member( + abstract: method.isAbstract, + method: true, + field: false, + getter: false, + static: method.isStatic, + synthetic: method.isSynthetic); + } + final annotations = []; + for (final metadata in element.metadata) { + annotations.add(_createAnnotation(metadata)); + } + for (final accessor in element.accessors) { + // TODO: maybe need a different namespace for these? + if (!accessor.isSynthetic) { + members[accessor.name] = Member( + abstract: accessor.isAbstract, + method: false, + field: false, + getter: accessor.isGetter, + // TODO: setter + static: accessor.isStatic, + synthetic: accessor.isSynthetic); + } + } + result.library(uri)!.add( + name, + Class( + annotations: annotations, + supertype: supertype, + interfaces: interfaces, + members: members, + abstract: element is ClassElement && element.isAbstract)); + return QualifiedName( + uri: element.source.uri.toString(), name: element.displayName); + } + + Annotation _createAnnotation(ElementAnnotation element) { + final value = element.computeConstantValue(); + if (value == null) { + return Annotation( + type: QualifiedName(uri: 'unresolved', name: 'unresolved'), + value: null); + } + final qualifiedName = QualifiedName( + uri: value.type!.element!.source!.uri.toString(), + name: value.type!.getDisplayString(withNullability: false)); + return Annotation( + type: qualifiedName, + value: _createValue(value as DartObjectImpl), + ); + } + + Value _createValue(DartObjectImpl object) { + if (object.isNull) return Value.primitive(null); + if (object.isBool) return Value.primitive(object.toBoolValue()); + if (object.isBoolNumStringOrNull) { + // TODO(davidmorgan): other types. + return Value.primitive(object.toStringValue()); + } + if (object.isUserDefinedObject) { + return Value.object(fields: { + for (final field in object.fields!.entries) + field.key: _createValue(field.value) + }); + } + return Value.fromJson( + 'package:code_model does not support value: ${object.toString()}'); + } +} diff --git a/working/macros/dart_model/dart_model_analyzer_service/pubspec.yaml b/working/macros/dart_model/dart_model_analyzer_service/pubspec.yaml new file mode 100644 index 0000000000..76c36efc54 --- /dev/null +++ b/working/macros/dart_model/dart_model_analyzer_service/pubspec.yaml @@ -0,0 +1,15 @@ +name: dart_model_analyzer_service +publish-to: none + +environment: + sdk: ^3.4.0 + +dependencies: + analyzer: '>=5.2.0 <7.0.0' + dart_model: any + stream_transform: any + +dependency_overrides: + dart_model: + path: + ../dart_model diff --git a/working/macros/dart_model/dart_model_repl/bin/main.dart b/working/macros/dart_model/dart_model_repl/bin/main.dart new file mode 100644 index 0000000000..27e2a04d89 --- /dev/null +++ b/working/macros/dart_model/dart_model_repl/bin/main.dart @@ -0,0 +1,9 @@ +// 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_repl/dart_model_repl.dart'; + +Future main() async { + await DartModelRepl().run(); +} diff --git a/working/macros/dart_model/dart_model_repl/lib/dart_model_repl.dart b/working/macros/dart_model/dart_model_repl/lib/dart_model_repl.dart new file mode 100644 index 0000000000..4cee0fc92a --- /dev/null +++ b/working/macros/dart_model/dart_model_repl/lib/dart_model_repl.dart @@ -0,0 +1,102 @@ +// 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 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/context_builder.dart'; +import 'package:analyzer/dart/analysis/context_locator.dart'; +import 'package:async/async.dart'; +import 'package:dart_model/model.dart'; +import 'package:dart_model/query.dart'; +import 'package:dart_model_analyzer_service/dart_model_analyzer_service.dart'; + +class DartModelRepl { + final _stdinLines = + StreamQueue(LineSplitter().bind(Utf8Decoder().bind(stdin))); + Service? service; + + Future run() async { + print('''' +Welcome to ${green}package:dart_model_repl$reset! +'''); + + while (await readInput()) {} + } + + Future readInput() async { + if (service == null) { + print('No service running; try "analyze ".'); + } + + stdout.write('> '); + final line = await _stdinLines.next; + if (line == 'exit') return false; + if (line == 'help') { + printHelp(); + return true; + } + + if (line.startsWith('analyze ')) { + createAnalyzer(line.substring('analyze '.length)); + return true; + } + + if (line.startsWith('query ')) { + final rest = line.substring('query '.length); + final maybeQualifiedName = QualifiedName.tryParse(rest); + final query = maybeQualifiedName == null + ? Query.uri(rest) + : Query.qualifiedName( + uri: maybeQualifiedName.uri, name: maybeQualifiedName.name); + final model = await service!.query(query); + print(model.prettyPrint()); + return true; + } + + if (line.startsWith('watch ')) { + final rest = line.substring('watch '.length); + final maybeQualifiedName = QualifiedName.tryParse(rest); + final query = maybeQualifiedName == null + ? Query.uri(rest) + : Query.qualifiedName( + uri: maybeQualifiedName.uri, name: maybeQualifiedName.name); + final model = Model(); + (await service!.watch(query)).listen((delta) { + delta.update(model); + print('=== current model for $query'); + print(model.prettyPrint()); + print('=== due to delta for $query'); + print(delta.prettyPrint()); + }); + return true; + } + + print('Unrecognized input. Try "help"?'); + return true; + } + + void printHelp() { + print(''' +analyze + Starts the analyzer query backend on . +query [#name] + Queries the library at the specified URI. If specified, query for + the scope called "name". +watch [#name] + Like "query", but watches and prints changes. +'''); + } + + void createAnalyzer(String workspace) { + final contextBuilder = ContextBuilder(); + final analysisContext = contextBuilder.createContext( + contextRoot: + ContextLocator().locateRoots(includedPaths: [workspace]).first); + service = DartModelAnalyzerService(context: analysisContext); + } +} + +final String reset = '\x1b[0m'; +final String green = '\x1b[92;1m'; diff --git a/working/macros/dart_model/dart_model_repl/pubspec.yaml b/working/macros/dart_model/dart_model_repl/pubspec.yaml new file mode 100644 index 0000000000..4858e8d948 --- /dev/null +++ b/working/macros/dart_model/dart_model_repl/pubspec.yaml @@ -0,0 +1,19 @@ +name: dart_model_repl +publish-to: none + +environment: + sdk: ^3.4.0 + +dependencies: + analyzer: any + async: ^2.11.0 + dart_model: any + dart_model_analyzer_service: any + +dependency_overrides: + dart_model: + path: + ../dart_model + dart_model_analyzer_service: + path: + ../dart_model_analyzer_service diff --git a/working/macros/dart_model/macro_client/lib/macro.dart b/working/macros/dart_model/macro_client/lib/macro.dart new file mode 100644 index 0000000000..cbbef97440 --- /dev/null +++ b/working/macros/dart_model/macro_client/lib/macro.dart @@ -0,0 +1,9 @@ +// 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/query.dart'; + +abstract interface class Macro { + void start(Service host) {} +} diff --git a/working/macros/dart_model/macro_client/lib/macro_client.dart b/working/macros/dart_model/macro_client/lib/macro_client.dart new file mode 100644 index 0000000000..bc515b6bf9 --- /dev/null +++ b/working/macros/dart_model/macro_client/lib/macro_client.dart @@ -0,0 +1,23 @@ +// 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 'dart:io'; + +import 'package:macro_client/macro.dart'; + +import 'socket_service.dart'; + +class MacroClient { + final List arguments; + + MacroClient(this.arguments); + + Future host(List macros) async { + final socket = await Socket.connect('localhost', 26199); + final host = SocketService(socket); + for (final macro in macros) { + macro.start(host); + } + } +} diff --git a/working/macros/dart_model/macro_client/lib/socket_service.dart b/working/macros/dart_model/macro_client/lib/socket_service.dart new file mode 100644 index 0000000000..f8e5728fe0 --- /dev/null +++ b/working/macros/dart_model/macro_client/lib/socket_service.dart @@ -0,0 +1,53 @@ +// 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 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dart_model/delta.dart'; +import 'package:dart_model/model.dart'; +import 'package:dart_model/query.dart'; +import 'package:macro_protocol/message.dart'; + +class SocketService implements Service { + final Socket socket; + final StreamController messagesController = + StreamController.broadcast(); + Stream get messages => messagesController.stream; + int _id = 0; + final Map> _deltaStreams = {}; + + SocketService(this.socket) { + socket + .cast>() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((m) => _handle(json.decode(m) as Message)); + } + + @override + Future query(Query query) async { + socket.writeln(json.encode(QueryRequest(query))); + return Model.fromJson((await messages.first) as Map); + } + + @override + Future> watch(Query query) async { + ++_id; + socket.writeln(json.encode(WatchRequest(query: query, id: _id))); + final result = StreamController(); + _deltaStreams[_id] = result; + return result.stream; + } + + void _handle(Message message) { + if (message.isWatchResponse) { + final response = message.asWatchResponse; + _deltaStreams[response.id]!.add(response.delta); + } else { + messagesController.add(message); + } + } +} diff --git a/working/macros/dart_model/macro_client/pubspec.yaml b/working/macros/dart_model/macro_client/pubspec.yaml new file mode 100644 index 0000000000..733972c8d9 --- /dev/null +++ b/working/macros/dart_model/macro_client/pubspec.yaml @@ -0,0 +1,15 @@ +name: macro_client +publish-to: none + +environment: + sdk: ^3.4.0 + +dependencies: + dart_model: any + macro_protocol: any + +dependency_overrides: + dart_model: + path: ../dart_model + macro_protocol: + path: ../macro_protocol diff --git a/working/macros/dart_model/macro_host/bin/main.dart b/working/macros/dart_model/macro_host/bin/main.dart new file mode 100644 index 0000000000..4a0b814411 --- /dev/null +++ b/working/macros/dart_model/macro_host/bin/main.dart @@ -0,0 +1,18 @@ +// 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:analyzer/dart/analysis/context_builder.dart'; +import 'package:analyzer/dart/analysis/context_locator.dart'; +import 'package:dart_model_analyzer_service/dart_model_analyzer_service.dart'; +import 'package:macro_host/macro_host.dart'; + +Future main(List arguments) async { + final workspace = arguments[0]; + final contextBuilder = ContextBuilder(); + final analysisContext = contextBuilder.createContext( + contextRoot: + ContextLocator().locateRoots(includedPaths: [workspace]).first); + final host = DartModelAnalyzerService(context: analysisContext); + MacroHost(host).run(); +} diff --git a/working/macros/dart_model/macro_host/lib/macro_host.dart b/working/macros/dart_model/macro_host/lib/macro_host.dart new file mode 100644 index 0000000000..c4c8a02112 --- /dev/null +++ b/working/macros/dart_model/macro_host/lib/macro_host.dart @@ -0,0 +1,29 @@ +// 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 'dart:io'; + +import 'package:dart_model/query.dart'; + +import 'socket_client.dart'; + +class MacroHost { + ServerSocket? serverSocket; + Service host; + + MacroHost(this.host); + + Future run() async { + serverSocket = await ServerSocket.bind('localhost', 26199); + print('macro_host listening on localhost:26199'); + + await for (final socket in serverSocket!) { + SocketClient(host, socket); + } + } + + void handle(Socket socket) { + print('Got $socket'); + } +} diff --git a/working/macros/dart_model/macro_host/lib/socket_client.dart b/working/macros/dart_model/macro_host/lib/socket_client.dart new file mode 100644 index 0000000000..4b01e807cb --- /dev/null +++ b/working/macros/dart_model/macro_host/lib/socket_client.dart @@ -0,0 +1,38 @@ +// 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 'dart:convert'; +import 'dart:io'; + +import 'package:dart_model/query.dart'; +import 'package:macro_protocol/message.dart'; + +class SocketClient { + final Service host; + final Socket socket; + + SocketClient(this.host, this.socket) { + socket + .cast>() + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(handle); + } + + void handle(String line) async { + final message = Message.fromJson(json.decode(line)); + + if (message.isQueryRequest) { + final response = await host.query(message.asQueryRequest.query); + socket.writeln(json.encode(QueryResponse(response))); + } else if (message.isWatchRequest) { + final request = message.asWatchRequest; + final response = await host.watch(request.query); + response.listen((delta) { + socket + .writeln(json.encode(WatchResponse(id: request.id, delta: delta))); + }); + } + } +} diff --git a/working/macros/dart_model/macro_host/pubspec.yaml b/working/macros/dart_model/macro_host/pubspec.yaml new file mode 100644 index 0000000000..aa90842404 --- /dev/null +++ b/working/macros/dart_model/macro_host/pubspec.yaml @@ -0,0 +1,19 @@ +name: macro_host +publish-to: none + +environment: + sdk: ^3.4.0 + +dependencies: + analyzer: any + dart_model: any + dart_model_analyzer_service: any + macro_protocol: any + +dependency_overrides: + dart_model: + path: ../dart_model + dart_model_analyzer_service: + path: ../dart_model_analyzer_service + macro_protocol: + path: ../macro_protocol diff --git a/working/macros/dart_model/macro_protocol/lib/message.dart b/working/macros/dart_model/macro_protocol/lib/message.dart new file mode 100644 index 0000000000..2ad6cdf176 --- /dev/null +++ b/working/macros/dart_model/macro_protocol/lib/message.dart @@ -0,0 +1,66 @@ +// 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/delta.dart'; +import 'package:dart_model/model.dart'; +import 'package:dart_model/query.dart'; + +extension type Message.fromJson(Map node) { + bool get isWatchRequest => node['type'] == 'watch'; + WatchRequest get asWatchRequest => this as WatchRequest; + bool get isWatchResponse => node['type'] == 'watchResponse'; + WatchResponse get asWatchResponse => this as WatchResponse; + + bool get isQueryRequest => node['type'] == 'query'; + QueryRequest get asQueryRequest => this as QueryRequest; + bool get isQueryResponse => node['type'] == 'queryResponse'; + QueryResponse get asQueryResponse => this as QueryResponse; +} + +extension type QueryRequest.fromJson(Map node) + implements Message { + QueryRequest(Query query) + : this.fromJson({ + 'type': 'query', + 'query': query.node, + }); + + Query get query => node['query'] as Query; +} + +extension type QueryResponse.fromJson(Map node) + implements Message { + QueryResponse(Model model) + : this.fromJson({ + 'type': 'queryResponse', + 'model': model.node, + }); + + Model get model => node['model'] as Model; +} + +extension type WatchRequest.fromJson(Map node) + implements Message { + WatchRequest({required Query query, required int id}) + : this.fromJson({ + 'type': 'watch', + 'query': query.node, + 'id': id, + }); + + Query get query => node['query'] as Query; + int get id => node['id'] as int; +} + +extension type WatchResponse.fromJson(Map node) { + WatchResponse({required Delta delta, required int id}) + : this.fromJson({ + 'type': 'watchResponse', + 'delta': delta.node, + 'id': id, + }); + + int get id => node['id'] as int; + Delta get delta => node['delta'] as Delta; +} diff --git a/working/macros/dart_model/macro_protocol/pubspec.yaml b/working/macros/dart_model/macro_protocol/pubspec.yaml new file mode 100644 index 0000000000..0b3ad51f81 --- /dev/null +++ b/working/macros/dart_model/macro_protocol/pubspec.yaml @@ -0,0 +1,12 @@ +name: macro_protocol +publish-to: none + +environment: + sdk: ^3.4.0 + +dependencies: + dart_model: any + +dependency_overrides: + dart_model: + path: ../dart_model diff --git a/working/macros/dart_model/testing/presubmit b/working/macros/dart_model/testing/presubmit new file mode 100755 index 0000000000..46d82272fd --- /dev/null +++ b/working/macros/dart_model/testing/presubmit @@ -0,0 +1,18 @@ +#!/bin/bash -- + +set -e + +# Analyze, format and test packages. +for package in dart_model dart_model_analyzer_service dart_model_repl \ + macro_client macro_host macro_protocol testing/scratch \ + testing/test_macro_annotations testing/test_macros; do + pushd "$package" + dart pub get + dart analyze --fatal-infos + dart format . + # Run tests if the test folder is present. + if test -d test; then + dart run test + fi + popd +done diff --git a/working/macros/dart_model/testing/scratch/lib/scratch.dart b/working/macros/dart_model/testing/scratch/lib/scratch.dart new file mode 100644 index 0000000000..b3ffdd2acc --- /dev/null +++ b/working/macros/dart_model/testing/scratch/lib/scratch.dart @@ -0,0 +1,16 @@ +// 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_macro_annotations/annotations.dart'; + +@FirstMacro() +class Foo {} + +@FirstMacro() +class Bar {} + +class Baz {} + +@FirstMacro() +class Hmm {} diff --git a/working/macros/dart_model/testing/scratch/pubspec.yaml b/working/macros/dart_model/testing/scratch/pubspec.yaml new file mode 100644 index 0000000000..7c455bfdea --- /dev/null +++ b/working/macros/dart_model/testing/scratch/pubspec.yaml @@ -0,0 +1,12 @@ +name: scratch +publish-to: none + +environment: + sdk: ^3.4.0 + +dependencies: + test_macro_annotations: any + +dependency_overrides: + test_macro_annotations: + path: ../test_macro_annotations diff --git a/working/macros/dart_model/testing/test_macro_annotations/lib/annotations.dart b/working/macros/dart_model/testing/test_macro_annotations/lib/annotations.dart new file mode 100644 index 0000000000..8bffa33a70 --- /dev/null +++ b/working/macros/dart_model/testing/test_macro_annotations/lib/annotations.dart @@ -0,0 +1,7 @@ +// 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. + +class FirstMacro { + const FirstMacro(); +} diff --git a/working/macros/dart_model/testing/test_macro_annotations/pubspec.yaml b/working/macros/dart_model/testing/test_macro_annotations/pubspec.yaml new file mode 100644 index 0000000000..a6347ac8ac --- /dev/null +++ b/working/macros/dart_model/testing/test_macro_annotations/pubspec.yaml @@ -0,0 +1,5 @@ +name: test_macro_annotations +publish-to: none + +environment: + sdk: ^3.4.0 diff --git a/working/macros/dart_model/testing/test_macros/bin/main.dart b/working/macros/dart_model/testing/test_macros/bin/main.dart new file mode 100644 index 0000000000..ff7b13dec9 --- /dev/null +++ b/working/macros/dart_model/testing/test_macros/bin/main.dart @@ -0,0 +1,10 @@ +// 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:macro_client/macro_client.dart'; +import 'package:test_macros/first_macro.dart'; + +Future main(List arguments) async { + MacroClient(arguments).host([FirstMacro()]); +} diff --git a/working/macros/dart_model/testing/test_macros/lib/first_macro.dart b/working/macros/dart_model/testing/test_macros/lib/first_macro.dart new file mode 100644 index 0000000000..3f00cb484c --- /dev/null +++ b/working/macros/dart_model/testing/test_macros/lib/first_macro.dart @@ -0,0 +1,18 @@ +// 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/model.dart'; +import 'package:dart_model/query.dart'; +import 'package:macro_client/macro.dart'; + +class FirstMacro implements Macro { + @override + Future start(Service host) async { + final stream = await host.watch(Query.annotation(QualifiedName( + uri: 'package:test_macro_annotations/annotations.dart', + name: 'FirstMacro', + ))); + stream.listen(print); + } +} diff --git a/working/macros/dart_model/testing/test_macros/pubspec.yaml b/working/macros/dart_model/testing/test_macros/pubspec.yaml new file mode 100644 index 0000000000..6155f9b5cb --- /dev/null +++ b/working/macros/dart_model/testing/test_macros/pubspec.yaml @@ -0,0 +1,17 @@ +name: test_macros +publish-to: none + +environment: + sdk: ^3.4.0 + +dependencies: + dart_model: any + macro_client: any + +dependency_overrides: + dart_model: + path: ../../dart_model + macro_client: + path: ../../macro_client + macro_protocol: + path: ../../macro_protocol