From db556adcfc78f9af3e442e00cee0930ab66facfe Mon Sep 17 00:00:00 2001 From: David Morgan Date: Thu, 8 Aug 2024 10:48:11 +0200 Subject: [PATCH] Use package:_macro_host in _analyzer_macros. (#28) --- .../lib/macro_implementation.dart | 173 ++++++++++++++++++ pkgs/_analyzer_macros/pubspec.yaml | 9 +- pkgs/_analyzer_macros/test/analyzer_test.dart | 111 +---------- pkgs/_macro_host/lib/macro_host.dart | 16 +- pkgs/_macro_host/test/macro_host_test.dart | 32 +++- pkgs/_test_macros/lib/declare_x_macro.dart | 4 + 6 files changed, 228 insertions(+), 117 deletions(-) create mode 100644 pkgs/_analyzer_macros/lib/macro_implementation.dart diff --git a/pkgs/_analyzer_macros/lib/macro_implementation.dart b/pkgs/_analyzer_macros/lib/macro_implementation.dart new file mode 100644 index 00000000..2c999d0f --- /dev/null +++ b/pkgs/_analyzer_macros/lib/macro_implementation.dart @@ -0,0 +1,173 @@ +// 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_host/macro_host.dart'; +// ignore: implementation_imports +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'; +// ignore: implementation_imports +import 'package:macros/src/executor.dart' as injected; + +/// Injected macro implementation for the analyzer. +class MacroImplementation implements injected.MacroImplementation { + final Uri packageConfig; + final Map macroImplByName; + final MacroHost _host; + + MacroImplementation._(this.packageConfig, this.macroImplByName, this._host); + + /// Starts the macro host. + /// + /// [packageConfig] is the package config of the workspace of the code being + /// edited. + /// + /// [macroImplByName] identifies macros, it's a placeholder until we identify + /// macros using package config. Keys are macro annotation qualified names + /// (`uri#name`) and values are macro implementation qualified names. + static Future start({ + required Uri packageConfig, + required Map macroImplByName, + }) async => + MacroImplementation._( + packageConfig, + macroImplByName, + await MacroHost.serve( + macroImplByName: macroImplByName, + queryService: AnalyzerQueryService())); + + @override + injected.MacroPackageConfigs get packageConfigs => + AnalyzerMacroPackageConfigs(this); + + @override + injected.MacroRunner get runner => AnalyzerMacroRunner(this); +} + +class AnalyzerQueryService implements QueryService { + @override + Future handle(QueryRequest request) async { + return QueryResponse(); + } +} + +class AnalyzerMacroPackageConfigs implements injected.MacroPackageConfigs { + final MacroImplementation _impl; + + AnalyzerMacroPackageConfigs(this._impl); + + @override + bool isMacro(Uri uri, String name) => + _impl._host.isMacro(_impl.packageConfig, QualifiedName('$uri#$name')); +} + +class AnalyzerMacroRunner implements injected.MacroRunner { + final MacroImplementation _impl; + + AnalyzerMacroRunner(this._impl); + + @override + injected.RunningMacro run(Uri uri, String name) => AnalyzerRunningMacro.run( + _impl, + QualifiedName('$uri#$name'), + // Look up from the macro name to its implementation. + QualifiedName(_impl.macroImplByName['$uri#$name']!)); +} + +class AnalyzerRunningMacro implements injected.RunningMacro { + final MacroImplementation _impl; + final QualifiedName name; + final QualifiedName implementation; + late final Future _started; + + AnalyzerRunningMacro._(this._impl, this.name, this.implementation); + + // TODO(davidmorgan): should this be async, removing the need for `_started`? + // If so the API injected into analyzer+CFE needs to change to be async. + static AnalyzerRunningMacro run(MacroImplementation impl, QualifiedName name, + QualifiedName implementation) { + final result = AnalyzerRunningMacro._(impl, name, implementation); + // TODO(davidmorgan): this is currently what starts the macro running, + // make it explicit. + result._started = + impl._host.queryMacroPhases(impl.packageConfig, implementation); + return result; + } + + @override + Future executeDeclarationsPhase( + MacroTarget target, + DeclarationPhaseIntrospector declarationsPhaseIntrospector) async { + await _started; + return AnalyzerMacroExecutionResult( + target, await _impl._host.augment(name, AugmentRequest(phase: 2))); + } + + @override + Future executeDefinitionsPhase( + MacroTarget target, + DefinitionPhaseIntrospector definitionPhaseIntrospector) async { + await _started; + return AnalyzerMacroExecutionResult( + target, await _impl._host.augment(name, AugmentRequest(phase: 3))); + } + + @override + Future executeTypesPhase( + MacroTarget target, TypePhaseIntrospector typePhaseIntrospector) async { + await _started; + return AnalyzerMacroExecutionResult( + target, await _impl._host.augment(name, AugmentRequest(phase: 1))); + } +} + +/// Converts [AugmentResponse] to [injected.MacroExecutionResult]. +/// +/// TODO(davidmorgan): add to `AugmentationResponse` to cover all the +/// functionality of `MacroExecutionResult`. +class AnalyzerMacroExecutionResult implements injected.MacroExecutionResult { + final MacroTarget target; + final AugmentResponse augmentResponse; + + AnalyzerMacroExecutionResult(this.target, this.augmentResponse); + + @override + List get diagnostics => []; + + @override + Map> get enumValueAugmentations => {}; + + @override + MacroException? get exception => null; + + @override + Map get extendsTypeAugmentations => {}; + + @override + Map> get interfaceAugmentations => + {}; + + @override + Iterable get libraryAugmentations => {}; + + @override + 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`. + (target as Declaration).identifier: augmentResponse.augmentations + .map((a) => DeclarationCode.fromParts([a.code])) + .toList(), + }; +} diff --git a/pkgs/_analyzer_macros/pubspec.yaml b/pkgs/_analyzer_macros/pubspec.yaml index c996937e..c114009e 100644 --- a/pkgs/_analyzer_macros/pubspec.yaml +++ b/pkgs/_analyzer_macros/pubspec.yaml @@ -6,8 +6,13 @@ resolution: workspace environment: sdk: ^3.6.0-114.0.dev -dev_dependencies: +dependencies: + _macro_host: any analyzer: any - dart_flutter_team_lints: ^3.0.0 + dart_model: any + macro_service: any macros: any + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 test: ^1.25.0 diff --git a/pkgs/_analyzer_macros/test/analyzer_test.dart b/pkgs/_analyzer_macros/test/analyzer_test.dart index 2dff34ad..9394cc22 100644 --- a/pkgs/_analyzer_macros/test/analyzer_test.dart +++ b/pkgs/_analyzer_macros/test/analyzer_test.dart @@ -3,34 +3,33 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:io'; +import 'dart:isolate'; +import 'package:_analyzer_macros/macro_implementation.dart'; import 'package:analyzer/dart/analysis/analysis_context.dart'; import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/src/summary2/macro_injected_impl.dart' as injected; -import 'package:macros/macros.dart'; -import 'package:macros/src/executor.dart'; import 'package:test/test.dart'; void main() { late AnalysisContext analysisContext; - late TestMacroPackageConfigs packageConfigs; - late TestMacroRunner runner; group('analyzer with injected macro implementation', () { - setUp(() { + setUp(() async { // Set up analyzer. final directory = Directory.fromUri(Uri.parse('./test/package_under_test')).absolute; final contextCollection = AnalysisContextCollection(includedPaths: [directory.path]); analysisContext = contextCollection.contexts.first; - - // Inject test macro implementation. - packageConfigs = TestMacroPackageConfigs(); - runner = TestMacroRunner(); - injected.macroImplementation = injected.MacroImplementation( - packageConfigs: packageConfigs, runner: runner); + injected.macroImplementation = await MacroImplementation.start( + packageConfig: Isolate.packageConfigSync!, + macroImplByName: { + 'package:_test_macros/declare_x_macro.dart#DeclareX': + 'package:_test_macros/declare_x_macro.dart' + '#DeclareXImplementation' + }); }); test('discovers macros, runs them, applies augmentations', () async { @@ -44,10 +43,6 @@ void main() { await analysisContext.currentSession.getErrors(path) as ErrorsResult; expect(errors.errors, isEmpty); - // Macro was found by the analyzer and run. - expect(packageConfigs.macroWasFound, true); - expect(runner.macroWasRun, true); - // The expected new declaration augmentation was applied. final resolvedLibrary = (await analysisContext.currentSession .getResolvedLibrary(path)) as ResolvedLibraryResult; @@ -58,89 +53,3 @@ void main() { }); }); } - -class TestMacroPackageConfigs implements injected.MacroPackageConfigs { - bool macroWasFound = false; - - @override - bool isMacro(Uri uri, String name) { - final result = - uri.toString() == 'package:_test_macros/declare_x_macro.dart' && - name == 'DeclareX'; - if (result) { - macroWasFound = true; - } - return result; - } -} - -class TestMacroRunner implements injected.MacroRunner { - bool macroWasRun = false; - - @override - injected.RunningMacro run(Uri uri, String name) { - macroWasRun = true; - return TestRunningMacro(); - } -} - -class TestRunningMacro implements injected.RunningMacro { - @override - Future executeDeclarationsPhase(MacroTarget target, - DeclarationPhaseIntrospector declarationsPhaseIntrospector) async { - return TestMacroExecutionResult(typeAugmentations: { - (target as Declaration).identifier: [ - DeclarationCode.fromParts(['int get x => 3;']) - ], - }); - } - - @override - Future executeDefinitionsPhase(MacroTarget target, - DefinitionPhaseIntrospector definitionPhaseIntrospector) async { - return TestMacroExecutionResult(); - } - - @override - Future executeTypesPhase( - MacroTarget target, TypePhaseIntrospector typePhaseIntrospector) async { - return TestMacroExecutionResult(); - } -} - -class TestMacroExecutionResult implements MacroExecutionResult { - @override - List get diagnostics => []; - - @override - Map> get enumValueAugmentations => {}; - - @override - MacroException? get exception => null; - - @override - Map get extendsTypeAugmentations => {}; - - @override - Map> get interfaceAugmentations => - {}; - - @override - Iterable get libraryAugmentations => {}; - - @override - Map> get mixinAugmentations => {}; - - @override - Iterable get newTypeNames => []; - - @override - void serialize(Object serializer) => throw UnimplementedError(); - - @override - Map> typeAugmentations; - - TestMacroExecutionResult( - {Map>? typeAugmentations}) - : typeAugmentations = typeAugmentations ?? {}; -} diff --git a/pkgs/_macro_host/lib/macro_host.dart b/pkgs/_macro_host/lib/macro_host.dart index dcff2f5a..45677e17 100644 --- a/pkgs/_macro_host/lib/macro_host.dart +++ b/pkgs/_macro_host/lib/macro_host.dart @@ -15,25 +15,33 @@ import 'package:macro_service/macro_service.dart'; /// Tools that want to support macros, such as the Analyzer and the CFE, can /// do so by running a `MacroHost` and providing their own `HostService`. class MacroHost { + final Map macroImplByName; final _HostService _hostService; final MacroServer macroServer; final MacroBuilder macroBuilder = MacroBuilder(); final MacroRunner macroRunner = MacroRunner(); - MacroHost._(this.macroServer, this._hostService); + MacroHost._(this.macroImplByName, this.macroServer, this._hostService); /// Starts a macro host with introspection queries handled by [queryService]. - static Future serve({required QueryService queryService}) async { + /// + /// [macroImplByName] is a map from macro annotation qualified name to macro + /// implementation qualified name, it's needed until this information is + /// available in package configs. + static Future serve({ + required Map macroImplByName, + required QueryService queryService, + }) async { final hostService = _HostService(queryService); final server = await MacroServer.serve(service: hostService); - return MacroHost._(server, hostService); + return MacroHost._(macroImplByName, server, hostService); } /// Whether [name] is a macro according to that package's `pubspec.yaml`. bool isMacro(Uri packageConfig, QualifiedName name) { // TODO(language/3728): this is a placeholder, use package config when // available. - return true; + return macroImplByName.keys.contains(name.string); } /// Determines which phases the macro implemented at [name] runs in. diff --git a/pkgs/_macro_host/test/macro_host_test.dart b/pkgs/_macro_host/test/macro_host_test.dart index 0a3c463e..b455949e 100644 --- a/pkgs/_macro_host/test/macro_host_test.dart +++ b/pkgs/_macro_host/test/macro_host_test.dart @@ -12,15 +12,21 @@ import 'package:test/test.dart'; void main() { group(MacroHost, () { test('hosts a macro, receives augmentations', () async { - final service = TestQueryService(); - final host = await MacroHost.serve(queryService: service); - - final macroName = QualifiedName( + final macroName = + QualifiedName('package:_test_macros/declare_x_macro.dart#DeclareX'); + final macroImplementation = QualifiedName( 'package:_test_macros/declare_x_macro.dart#DeclareXImplementation'); + + final queryService = TestQueryService(); + final host = await MacroHost.serve( + macroImplByName: {macroName.string: macroImplementation.string}, + queryService: queryService); + final packageConfig = Isolate.packageConfigSync!; expect(host.isMacro(packageConfig, macroName), true); - expect(await host.queryMacroPhases(packageConfig, macroName), {2}); + expect( + await host.queryMacroPhases(packageConfig, macroImplementation), {2}); expect( await host.augment(macroName, AugmentRequest(phase: 2)), @@ -29,15 +35,21 @@ void main() { }); test('hosts a macro, responds to queries', () async { - final service = TestQueryService(); - final host = await MacroHost.serve(queryService: service); - - final macroName = QualifiedName( + final macroName = + QualifiedName('package:_test_macros/query_class.dart#QueryClass'); + final macroImplementation = QualifiedName( 'package:_test_macros/query_class.dart#QueryClassImplementation'); + + final queryService = TestQueryService(); + final host = await MacroHost.serve( + macroImplByName: {macroName.string: macroImplementation.string}, + queryService: queryService); + final packageConfig = Isolate.packageConfigSync!; expect(host.isMacro(packageConfig, macroName), true); - expect(await host.queryMacroPhases(packageConfig, macroName), {3}); + expect( + await host.queryMacroPhases(packageConfig, macroImplementation), {3}); expect( await host.augment(macroName, AugmentRequest(phase: 2)), diff --git a/pkgs/_test_macros/lib/declare_x_macro.dart b/pkgs/_test_macros/lib/declare_x_macro.dart index bdc06438..47276b02 100644 --- a/pkgs/_test_macros/lib/declare_x_macro.dart +++ b/pkgs/_test_macros/lib/declare_x_macro.dart @@ -17,6 +17,10 @@ class DeclareXImplementation implements Macro { @override Future augment(Host host, AugmentRequest request) async { + // TODO(davidmorgan): make the host only run in the phases requested so + // that this is not needed. + if (request.phase != 2) return AugmentResponse(augmentations: []); + // TODO(davidmorgan): still need to pass through the augment target. return AugmentResponse( augmentations: [Augmentation(code: 'int get x => 3;')]);