blob: 67b397295be26ef62d172cf3a1e0fb6264b128b0 [file] [log] [blame] [edit]
// 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 'package:_macro_builder/macro_builder.dart';
import 'package:_macro_runner/macro_runner.dart';
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 MacroPackageConfig macroPackageConfig;
final _HostService _hostService;
final MacroServer macroServer;
final MacroBuilder macroBuilder = MacroBuilder();
final MacroRunner macroRunner = MacroRunner();
MacroHost._(this.macroPackageConfig, this.macroServer, this._hostService);
/// Starts a macro host with introspection queries handled by [queryService].
static Future<MacroHost> serve({
// TODO(davidmorgan): this should be negotiated per client, not set here.
required Protocol protocol,
required Uri packageConfig,
required QueryService queryService,
}) async {
final macroPackageConfig = MacroPackageConfig.readFromUri(packageConfig);
final hostService = _HostService(queryService);
final server =
await MacroServer.serve(protocol: protocol, service: hostService);
return MacroHost._(macroPackageConfig, server, hostService);
}
/// Whether [name] is a macro according to that package's `pubspec.yaml`.
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<Set<int>> queryMacroPhases(
Uri packageConfig, QualifiedName name) async {
// TODO(davidmorgan): track macro lifecycle, correctly run once per macro
// code change including if queried multiple times before response returns.
if (_hostService._macroPhases != null) {
return _hostService._macroPhases!.future;
}
_hostService._macroPhases = Completer();
final macroBundle = await macroBuilder.build(packageConfig, [name]);
macroRunner.start(
macroBundle: macroBundle,
protocol: macroServer.protocol,
endpoint: macroServer.endpoint);
return _hostService._macroPhases!.future;
}
/// Sends [request] to the macro with [name].
Future<AugmentResponse> augment(
QualifiedName name, AugmentRequest request) async {
// TODO(davidmorgan): this just assumes the macro is running, actually
// track macro lifecycle.
final response = await macroServer.sendToMacro(
name, HostRequest.augmentRequest(request, id: nextRequestId));
return response.asAugmentResponse;
}
}
class _HostService implements HostService {
final QueryService queryService;
// TODO(davidmorgan): this should be per macro, as part of tracking per-macro
// lifecycle state.
Completer<Set<int>>? _macroPhases;
_HostService(this.queryService);
/// Handle requests that are for the host.
@override
Future<Response> handle(MacroRequest request) async {
switch (request.type) {
case MacroRequestType.macroStartedRequest:
_macroPhases!.complete(request
.asMacroStartedRequest.macroDescription.runsInPhases
.toSet());
return Response.macroStartedResponse(MacroStartedResponse(),
requestId: request.id);
case MacroRequestType.queryRequest:
return Response.queryResponse(
await queryService.handle(request.asQueryRequest),
requestId: request.id);
default:
return Response.errorResponse(ErrorResponse(error: 'unsupported'),
requestId: request.id);
}
}
}
/// Service provided by the frontend the host integrates with.
abstract interface class QueryService {
Future<QueryResponse> handle(QueryRequest request);
}