diff --git a/pkgs/_analyzer_macros/lib/macro_implementation.dart b/pkgs/_analyzer_macros/lib/macro_implementation.dart index de18279a..1fe43c27 100644 --- a/pkgs/_analyzer_macros/lib/macro_implementation.dart +++ b/pkgs/_analyzer_macros/lib/macro_implementation.dart @@ -14,29 +14,21 @@ import 'package:macros/src/executor.dart' as injected; /// Injected macro implementation for the analyzer. class AnalyzerMacroImplementation implements injected.MacroImplementation { final Uri packageConfig; - final Map macroImplByName; final MacroHost _host; - AnalyzerMacroImplementation._( - this.packageConfig, this.macroImplByName, this._host); + AnalyzerMacroImplementation._(this.packageConfig, 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 => AnalyzerMacroImplementation._( packageConfig, - macroImplByName, await MacroHost.serve( - macroImplByName: macroImplByName, + packageConfig: packageConfig, queryService: AnalyzerQueryService())); @override @@ -61,7 +53,7 @@ class AnalyzerMacroPackageConfigs implements injected.MacroPackageConfigs { @override bool isMacro(Uri uri, String name) => - _impl._host.isMacro(_impl.packageConfig, QualifiedName('$uri#$name')); + _impl._host.isMacro(QualifiedName('$uri#$name')); } class AnalyzerMacroRunner implements injected.MacroRunner { @@ -73,8 +65,7 @@ class AnalyzerMacroRunner implements injected.MacroRunner { 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']!)); + _impl._host.lookupMacroImplementation(QualifiedName('$uri#$name'))!); } class AnalyzerRunningMacro implements injected.RunningMacro { diff --git a/pkgs/_analyzer_macros/test/analyzer_test.dart b/pkgs/_analyzer_macros/test/analyzer_test.dart index 8211d4d8..cacc0abd 100644 --- a/pkgs/_analyzer_macros/test/analyzer_test.dart +++ b/pkgs/_analyzer_macros/test/analyzer_test.dart @@ -24,12 +24,7 @@ void main() { AnalysisContextCollection(includedPaths: [directory.path]); analysisContext = contextCollection.contexts.first; injected.macroImplementation = await AnalyzerMacroImplementation.start( - packageConfig: Isolate.packageConfigSync!, - macroImplByName: { - 'package:_test_macros/declare_x_macro.dart#DeclareX': - 'package:_test_macros/declare_x_macro.dart' - '#DeclareXImplementation' - }); + packageConfig: Isolate.packageConfigSync!); }); test('discovers macros, runs them, applies augmentations', () async { diff --git a/pkgs/_cfe_macros/lib/macro_implementation.dart b/pkgs/_cfe_macros/lib/macro_implementation.dart index 01f07275..f2c5b699 100644 --- a/pkgs/_cfe_macros/lib/macro_implementation.dart +++ b/pkgs/_cfe_macros/lib/macro_implementation.dart @@ -14,30 +14,21 @@ import 'package:macros/src/executor.dart' as injected; /// Injected macro implementation for the analyzer. class CfeMacroImplementation implements injected.MacroImplementation { final Uri packageConfig; - final Map macroImplByName; final MacroHost _host; - CfeMacroImplementation._( - this.packageConfig, this.macroImplByName, this._host); + CfeMacroImplementation._(this.packageConfig, 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 => CfeMacroImplementation._( packageConfig, - macroImplByName, await MacroHost.serve( - macroImplByName: macroImplByName, - queryService: CfeQueryService())); + packageConfig: packageConfig, queryService: CfeQueryService())); @override injected.MacroPackageConfigs get packageConfigs => @@ -61,7 +52,7 @@ class CfeMacroPackageConfigs implements injected.MacroPackageConfigs { @override bool isMacro(Uri uri, String name) => - _impl._host.isMacro(_impl.packageConfig, QualifiedName('$uri#$name')); + _impl._host.isMacro(QualifiedName('$uri#$name')); } class CfeMacroRunner implements injected.MacroRunner { @@ -73,8 +64,7 @@ class CfeMacroRunner implements injected.MacroRunner { injected.RunningMacro run(Uri uri, String name) => CfeRunningMacro.run( _impl, QualifiedName('$uri#$name'), - // Look up from the macro name to its implementation. - QualifiedName(_impl.macroImplByName['$uri#$name']!)); + _impl._host.lookupMacroImplementation(QualifiedName('$uri#$name'))!); } class CfeRunningMacro implements injected.RunningMacro { diff --git a/pkgs/_cfe_macros/test/cfe_test.dart b/pkgs/_cfe_macros/test/cfe_test.dart index 305e5cfd..fc91d96f 100644 --- a/pkgs/_cfe_macros/test/cfe_test.dart +++ b/pkgs/_cfe_macros/test/cfe_test.dart @@ -29,20 +29,8 @@ void main() { tempDir = Directory.systemTemp.createTempSync('cfe_test'); // Inject test macro implementation. - // TODO(davidmorgan): the CFE passes us a mix of absolute file paths and - // package URIs, fix this properly. - final macroFileUri = Directory.current.uri - .resolve('../_test_macros/lib/declare_x_macro.dart'); injected.macroImplementation = await CfeMacroImplementation.start( - packageConfig: Isolate.packageConfigSync!, - macroImplByName: { - '$macroFileUri#DeclareX': - 'package:_test_macros/declare_x_macro.dart' - '#DeclareXImplementation', - 'package:_test_macros/declare_x_macro.dart#DeclareX': - 'package:_test_macros/declare_x_macro.dart' - '#DeclareXImplementation' - }); + packageConfig: Isolate.packageConfigSync!); }); test('discovers macros, runs them, applies augmentations', () async { diff --git a/pkgs/_macro_host/lib/macro_host.dart b/pkgs/_macro_host/lib/macro_host.dart index 45677e17..4ebfe1ea 100644 --- a/pkgs/_macro_host/lib/macro_host.dart +++ b/pkgs/_macro_host/lib/macro_host.dart @@ -10,39 +10,42 @@ import 'package:_macro_server/macro_server.dart'; import 'package:dart_model/dart_model.dart'; import 'package:macro_service/macro_service.dart'; +import 'src/package_config.dart'; + /// Hosts macros: builds them, runs them, serves the macro service. /// /// 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 MacroPackageConfig macroPackageConfig; final _HostService _hostService; final MacroServer macroServer; final MacroBuilder macroBuilder = MacroBuilder(); final MacroRunner macroRunner = MacroRunner(); - MacroHost._(this.macroImplByName, this.macroServer, this._hostService); + MacroHost._(this.macroPackageConfig, this.macroServer, this._hostService); /// Starts a macro host with introspection queries handled by [queryService]. - /// - /// [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 Uri packageConfig, required QueryService queryService, }) async { + final macroPackageConfig = MacroPackageConfig.readFromUri(packageConfig); final hostService = _HostService(queryService); final server = await MacroServer.serve(service: hostService); - return MacroHost._(macroImplByName, server, hostService); + return MacroHost._(macroPackageConfig, 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 macroImplByName.keys.contains(name.string); - } + bool isMacro(QualifiedName name) => lookupMacroImplementation(name) != null; + + /// Checks whether [name] is a macro annotation. + /// + /// If so, returns the qualified name of the macro implementation. + /// + /// If not, returns `null`. + QualifiedName? lookupMacroImplementation(QualifiedName name) => + macroPackageConfig.lookupMacroImplementation(name); /// Determines which phases the macro implemented at [name] runs in. Future> queryMacroPhases( diff --git a/pkgs/_macro_host/lib/src/package_config.dart b/pkgs/_macro_host/lib/src/package_config.dart new file mode 100644 index 00000000..404db514 --- /dev/null +++ b/pkgs/_macro_host/lib/src/package_config.dart @@ -0,0 +1,84 @@ +// 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/dart_model.dart'; +import 'package:package_config/package_config.dart'; + +/// Reads a package config to determine information about macros. +class MacroPackageConfig { + final PackageConfig packageConfig; + + MacroPackageConfig({required this.packageConfig}); + + factory MacroPackageConfig.readFromUri(Uri uri) => MacroPackageConfig( + packageConfig: + PackageConfig.parseBytes(File.fromUri(uri).readAsBytesSync(), uri)); + + /// Checks whether [name] is a macro annotation. + /// + /// If so, returns the qualified name of the macro implementation. + /// + /// If not, returns `null`. + /// + /// This is a placeholder implementation until `language/3728` is + /// implemented. It expects macros to be marked by a comment in the + /// annotation package `pubspec.yaml` that looks like this: + /// + /// ``` + /// # macro + /// ``` + /// + /// For example: + /// + /// ``` + /// # macro lib/declare_x_macro.dart#DeclareX package:_test_macros/declare_x_macro.dart#DeclareXImplementation + /// ``` + QualifiedName? lookupMacroImplementation(QualifiedName name) { + var packageName = name.uri; + if (packageName.startsWith('dart:') || + packageName.startsWith('org-dartlang-sdk:')) { + return null; + } + // TODO(davidmorgan): error handling when lookup fails. + if (packageName.startsWith('file:')) { + packageName = + packageConfig.toPackageUri(Uri.parse(packageName)).toString(); + } + final libraryPathAndName = + 'lib/${packageName.substring(packageName.indexOf('/') + 1)}#${name.name}'; + if (packageName.startsWith('package:') && packageName.contains('/')) { + packageName = packageName.substring('package:'.length); + packageName = packageName.substring(0, packageName.indexOf('/')); + } else { + // TODO(davidmorgan): support macros outside lib dirs. + throw ArgumentError('Name must start "package:" and have a path: $name'); + } + + final matchingPackage = packageConfig[packageName]; + if (matchingPackage == null) { + throw StateError('Package "$packageName" not found in package config.'); + } + + // TODO(language/3728): read macro annotation identifiers from package + // config. Until then, check the pubsec, to simulate what that feature will + // do. + final packageUri = matchingPackage.root; + final pubspecUri = packageUri.resolve('pubspec.yaml'); + final lines = File.fromUri(pubspecUri).readAsLinesSync(); + + final implsByLibraryQualifiedName = {}; + for (final line in lines) { + if (!line.startsWith('# macro ')) continue; + final items = line.split(' '); + // The rest of the line should be the library qualified name of the + // annotation then the fully qualified name of the implementation. + implsByLibraryQualifiedName[items[2]] = items[3]; + } + + final result = implsByLibraryQualifiedName[libraryPathAndName]; + return result == null ? null : QualifiedName(result); + } +} diff --git a/pkgs/_macro_host/pubspec.yaml b/pkgs/_macro_host/pubspec.yaml index 17e7d30d..d2790157 100644 --- a/pkgs/_macro_host/pubspec.yaml +++ b/pkgs/_macro_host/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: _macro_server: any dart_model: any macro_service: any + package_config: ^2.1.0 dev_dependencies: _test_macros: any diff --git a/pkgs/_macro_host/test/macro_host_test.dart b/pkgs/_macro_host/test/macro_host_test.dart index b455949e..6874d659 100644 --- a/pkgs/_macro_host/test/macro_host_test.dart +++ b/pkgs/_macro_host/test/macro_host_test.dart @@ -19,12 +19,12 @@ void main() { final queryService = TestQueryService(); final host = await MacroHost.serve( - macroImplByName: {macroName.string: macroImplementation.string}, + packageConfig: Isolate.packageConfigSync!, queryService: queryService); final packageConfig = Isolate.packageConfigSync!; - expect(host.isMacro(packageConfig, macroName), true); + expect(host.isMacro(macroName), true); expect( await host.queryMacroPhases(packageConfig, macroImplementation), {2}); @@ -42,12 +42,12 @@ void main() { final queryService = TestQueryService(); final host = await MacroHost.serve( - macroImplByName: {macroName.string: macroImplementation.string}, + packageConfig: Isolate.packageConfigSync!, queryService: queryService); final packageConfig = Isolate.packageConfigSync!; - expect(host.isMacro(packageConfig, macroName), true); + expect(host.isMacro(macroName), true); expect( await host.queryMacroPhases(packageConfig, macroImplementation), {3}); diff --git a/pkgs/_macro_host/test/package_config_test.dart b/pkgs/_macro_host/test/package_config_test.dart new file mode 100644 index 00000000..8ad301dd --- /dev/null +++ b/pkgs/_macro_host/test/package_config_test.dart @@ -0,0 +1,41 @@ +// 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 'dart:isolate'; + +import 'package:_macro_host/src/package_config.dart'; +import 'package:dart_model/dart_model.dart'; +import 'package:test/test.dart'; + +void main() { + group(MacroPackageConfig, () { + test('can look up macro implementations from package URIs', () async { + final packageConfig = + MacroPackageConfig.readFromUri(Isolate.packageConfigSync!); + + expect( + packageConfig + .lookupMacroImplementation(QualifiedName( + 'package:_test_macros/declare_x_macro.dart#DeclareX'))! + .string, + 'package:_test_macros/declare_x_macro.dart#DeclareXImplementation'); + }); + + test('can look up macro implementations from file URIs', () async { + final packageConfig = + MacroPackageConfig.readFromUri(Isolate.packageConfigSync!); + + final sourceFileUri = Directory.current.uri + .resolve('../_test_macros/lib/declare_x_macro.dart'); + + expect( + packageConfig + .lookupMacroImplementation( + QualifiedName('$sourceFileUri#DeclareX'))! + .string, + 'package:_test_macros/declare_x_macro.dart#DeclareXImplementation'); + }); + }); +} diff --git a/pkgs/_test_macros/pubspec.yaml b/pkgs/_test_macros/pubspec.yaml index 6029fcf1..18a63e81 100644 --- a/pkgs/_test_macros/pubspec.yaml +++ b/pkgs/_test_macros/pubspec.yaml @@ -16,3 +16,7 @@ dependencies: dev_dependencies: dart_flutter_team_lints: ^3.0.0 test: ^1.25.0 + +# TODO(language/3728): use the real feature when there is one. +# macro lib/declare_x_macro.dart#DeclareX package:_test_macros/declare_x_macro.dart#DeclareXImplementation +# macro lib/query_class.dart#QueryClass package:_test_macros/query_class.dart#QueryClassImplementation