blob: 9ec60950b5d4fb5556f800a7c0032d89b1cdb00d [file] [log] [blame]
// 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 'macro_cache.dart';
import '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();
final MacroResultsCache _macroResultsCache;
/// We only want to expose the public interface and not the private impl.
HostService get hostService => _hostService;
/// Helper for internal use only.
QueryService get _queryService => _hostService.queryService;
MacroHost._(this.macroPackageConfig, this.macroServer, this._hostService)
: _macroResultsCache = MacroResultsCache(_hostService.queryService);
/// Starts a macro host with introspection queries handled by [queryService].
static Future<MacroHost> serve({
// TODO(davidmorgan): support serving multiple protocols.
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 [annotation] is a macro annotation.
///
/// If so, returns the qualified name of the macro implementation.
///
/// If not, returns `null`.
QualifiedName? lookupMacroImplementation(QualifiedName annotation) =>
macroPackageConfig.lookupMacroImplementation(annotation);
/// Determines which phases the macro triggered by [annotation] runs in.
Future<Set<int>> queryMacroPhases(QualifiedName annotation) async {
await _ensureRunning(annotation);
return _hostService._macroState[annotation.asString]!.phases;
}
/// Sends [request] to the macro triggered by [annotation].
Future<AugmentResponse> augment(
QualifiedName annotation,
AugmentRequest request,
) async {
// TODO: Save the query results or pre-emptively send them to the macro?
final cached = await _macroResultsCache.cachedResult(annotation, request);
if (cached != null) return cached;
await _ensureRunning(annotation);
_queryService.startTrackingQueries();
final response =
(await macroServer.sendToMacro(
HostRequest.augmentRequest(
macroAnnotation: annotation,
request,
id: nextRequestId,
),
)).asAugmentResponse;
_macroResultsCache.cache(
annotation: annotation,
request: request,
queryResults:
_queryService.stopTrackingQueries()
// Add an initial query for the model itself also.
..add((
query: Query(target: request.target),
response: request.model,
)),
response: response,
);
return response;
}
/// If the macro triggered by [annotation] is not running, builds it and
/// launches it.
Future<void> _ensureRunning(QualifiedName annotation) async {
if (_hostService._macroState.containsKey(annotation.asString)) return;
await buildAndRunMacro(annotation);
}
/// Builds and runs the macro triggered by [annotation].
///
/// Throws if it's already running.
Future<void> buildAndRunMacro(QualifiedName annotation) async {
if (_hostService._macroState.containsKey(annotation.asString)) {
throw StateError('Macro is already running: ${annotation.asString}');
}
// TODO(davidmorgan): additional state is needed to track that a macro
// is still building; currently requests while the macro is building will
// time out after 5s.
_hostService._macroState[annotation.asString] = _MacroState();
final macroBundle = await macroBuilder.build(macroPackageConfig.uri, [
lookupMacroImplementation(annotation)!,
]);
macroRunner.start(macroBundle: macroBundle, endpoint: macroServer.endpoint);
}
}
class _HostService implements HostService {
final QueryService queryService;
/// Macro state by its annotation [QualifiedName] string representation.
final Map<String, _MacroState> _macroState = {};
_HostService(this.queryService);
/// Handle requests that are for the host.
@override
Future<Response> handle(MacroRequest request) async {
switch (request.type) {
case MacroRequestType.macroStartedRequest:
final macroStartedRequest = request.asMacroStartedRequest;
_macroState[macroStartedRequest.macroDescription.annotation.asString]!
._phasesCompleter
.complete(
macroStartedRequest.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 base class QueryService {
Future<QueryResponse> handle(QueryRequest request);
List<({Query query, Model response})>? _trackedQueries;
/// Starts collecting queries and responses.
///
/// Must call [stopTrackingQueries] before calling this again.
//
// TODO: Something that supports concurrent macros.
void startTrackingQueries() {
if (_trackedQueries != null) {
throw StateError(
'Already tracking queries, cannot handle concurrent macros',
);
}
_trackedQueries = [];
}
/// Stops collecting queries and responses, returns ones collected so far.
///
/// Must call [startTrackingQueries] before calling this.
//
// TODO: Something that supports concurrent macros.
List<({Query query, Model response})> stopTrackingQueries() {
if (_trackedQueries == null) {
throw StateError(
'Not tracking queries, must call `startTrackingQueries` first.',
);
}
final result = _trackedQueries!;
_trackedQueries = null;
return result;
}
}
class _MacroState {
// The first thing a macro does when it runs is sent a `MacroStartedRequest`
// with its phases, so this value is expected as soon as the macro runs.
final Completer<Set<int>> _phasesCompleter = Completer();
Future<Set<int>> get phases => _phasesCompleter.future;
}