blob: ee744ce2c998f977590dd07eed7da4b153dfee66 [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 'package:_macro_host/macro_host.dart';
import 'package:analyzer/dart/element/element.dart';
// ignore: implementation_imports
import 'package:analyzer/src/summary2/macro_declarations.dart' as analyzer;
// 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' as macros_api_v1;
// ignore: implementation_imports
import 'package:macros/src/executor.dart' as macros_api_v1;
import 'query_service.dart';
/// Injected macro implementation for the analyzer.
class AnalyzerMacroImplementation implements injected.MacroImplementation {
final Uri packageConfig;
final MacroHost _host;
AnalyzerMacroImplementation._(this.packageConfig, this._host);
/// Starts the macro host.
///
/// [packageConfig] is the package config of the workspace of the code being
/// edited.
static Future<AnalyzerMacroImplementation> start({
required Protocol protocol,
required Uri packageConfig,
}) async =>
AnalyzerMacroImplementation._(
packageConfig,
await MacroHost.serve(
// TODO(davidmorgan): support serving multiple protocols.
protocol: protocol,
packageConfig: packageConfig,
queryService: AnalyzerQueryService()));
@override
injected.MacroPackageConfigs get packageConfigs =>
AnalyzerMacroPackageConfigs(this);
@override
injected.MacroRunner get runner => AnalyzerMacroRunner(this);
}
class AnalyzerMacroPackageConfigs implements injected.MacroPackageConfigs {
final AnalyzerMacroImplementation _impl;
AnalyzerMacroPackageConfigs(this._impl);
@override
bool isMacro(Uri uri, String name) =>
_impl._host.isMacro(QualifiedName(uri: '$uri', name: name));
}
class AnalyzerMacroRunner implements injected.MacroRunner {
final AnalyzerMacroImplementation _impl;
AnalyzerMacroRunner(this._impl);
@override
injected.RunningMacro run(Uri uri, String name) => AnalyzerRunningMacro.run(
_impl,
QualifiedName(uri: '$uri', name: name),
_impl._host
.lookupMacroImplementation(QualifiedName(uri: '$uri', name: name))!);
}
class AnalyzerRunningMacro implements injected.RunningMacro {
final AnalyzerMacroImplementation _impl;
final QualifiedName name;
final QualifiedName implementation;
AnalyzerRunningMacro._(this._impl, this.name, this.implementation);
static AnalyzerRunningMacro run(AnalyzerMacroImplementation impl,
QualifiedName name, QualifiedName implementation) {
return AnalyzerRunningMacro._(impl, name, implementation);
}
/// Queries for [target] and returns the [Model] representing the result.
///
/// TODO: Make this a more limited query which doesn't fetch members.
Future<Model> _queryTarget(QualifiedName target) =>
Scope.query.run(() async => (await _impl._host.hostService.handle(
MacroRequest.queryRequest(
QueryRequest(query: Query(target: target)),
id: nextRequestId)))
.asQueryResponse
.model);
/// Cached macro results
static final _declPhaseCachedResults = <
// TODO: This is just the name of the class, yikes
String,
(
List<(Query, Model)> queryResults,
AugmentResponse macroResponse,
)>{};
@override
Future<AnalyzerMacroExecutionResult> executeDeclarationsPhase(
macros_api_v1.MacroTarget target,
macros_api_v1.DeclarationPhaseIntrospector
declarationsPhaseIntrospector) async {
var watch = Stopwatch()..start();
try {
// TODO(davidmorgan): this is a hack to access analyzer internals; remove.
introspector = declarationsPhaseIntrospector;
final cached = _declPhaseCachedResults[
// TODO: Obviously flawed
(target as macros_api_v1.ClassDeclaration).identifier.name];
AugmentResponse? response;
if (cached != null) {
var allWasCached = true;
for (final (query, result) in cached.$1) {
final newResult = await Scope.query.run(() async => (await _impl
._host.hostService
.handle(MacroRequest.queryRequest(QueryRequest(query: query),
id: nextRequestId)))
.asQueryResponse
.model);
if (result.identityHash != newResult.identityHash) {
allWasCached = false;
break;
}
}
if (allWasCached) {
response = cached.$2;
}
}
if (response == null) {
print('invalidated, re-running');
_impl._host.hostService.startTracking();
response = await _impl._host.augment(
name,
AugmentRequest(
phase: 2,
target: target.qualifiedName,
model: await _queryTarget(target.qualifiedName)));
_declPhaseCachedResults[target.identifier.name] =
(_impl._host.hostService.stopTracking(), response);
}
return await AnalyzerMacroExecutionResult.dartModelToInjected(
target, response);
} finally {
print('executeDeclarationsPhase: ${watch.elapsedMilliseconds}ms');
}
}
/// Cached macro results from the definitions phase.
static final _definitionPhaseCachedResults = <
// TODO: This is just the name of the class, yikes
String,
(
List<(Query, Model)> queryResults,
AugmentResponse macroResponse,
)>{};
@override
Future<AnalyzerMacroExecutionResult> executeDefinitionsPhase(
macros_api_v1.MacroTarget target,
macros_api_v1.DefinitionPhaseIntrospector
definitionPhaseIntrospector) async {
var watch = Stopwatch()..start();
try {
// TODO(davidmorgan): this is a hack to access analyzer internals; remove.
introspector = definitionPhaseIntrospector;
final cached = _definitionPhaseCachedResults[
// TODO: Obviously flawed
(target as macros_api_v1.ClassDeclaration).identifier.name];
AugmentResponse? response;
if (cached != null) {
var allWasCached = true;
for (final (query, result) in cached.$1) {
final newResult = await Scope.query.run(() async => (await _impl
._host.hostService
.handle(MacroRequest.queryRequest(QueryRequest(query: query),
id: nextRequestId)))
.asQueryResponse
.model);
if (result.identityHash != newResult.identityHash) {
allWasCached = false;
break;
}
}
if (allWasCached) {
response = cached.$2;
}
}
if (response == null) {
print('invalidated, re-running');
_impl._host.hostService.startTracking();
response = await _impl._host.augment(
name,
AugmentRequest(
phase: 3,
target: target.qualifiedName,
model: await _queryTarget(target.qualifiedName)));
_definitionPhaseCachedResults[target.identifier.name] =
(_impl._host.hostService.stopTracking(), response);
}
return await AnalyzerMacroExecutionResult.dartModelToInjected(
target, response);
} finally {
print('executeDefinitionsPhase: ${watch.elapsedMilliseconds}ms');
}
}
/// Cached macro results from the definitions phase.
static final _typesPhaseCachedResults = <
// TODO: This is just the name of the class, yikes
String,
(
List<(Query, Model)> queryResults,
AugmentResponse macroResponse,
)>{};
@override
Future<AnalyzerMacroExecutionResult> executeTypesPhase(
macros_api_v1.MacroTarget target,
macros_api_v1.TypePhaseIntrospector typePhaseIntrospector) async {
var watch = Stopwatch()..start();
try {
// TODO(davidmorgan): this is a hack to access analyzer internals; remove.
introspector = typePhaseIntrospector;
final cached = _typesPhaseCachedResults[
// TODO: Obviously flawed
(target as macros_api_v1.ClassDeclaration).identifier.name];
AugmentResponse? response;
if (cached != null) {
var allWasCached = true;
for (final (query, result) in cached.$1) {
final newResult = await Scope.query.run(() async => (await _impl
._host.hostService
.handle(MacroRequest.queryRequest(QueryRequest(query: query),
id: nextRequestId)))
.asQueryResponse
.model);
if (result.identityHash != newResult.identityHash) {
allWasCached = false;
break;
}
}
if (allWasCached) {
response = cached.$2;
}
}
if (response == null) {
print('invalidated, re-running');
_impl._host.hostService.startTracking();
response = await _impl._host.augment(
name,
AugmentRequest(
phase: 1,
target: target.qualifiedName,
model: await _queryTarget(target.qualifiedName)));
_typesPhaseCachedResults[target.identifier.name] =
(_impl._host.hostService.stopTracking(), response);
}
return await AnalyzerMacroExecutionResult.dartModelToInjected(
target, response);
} finally {
print('executeTypesPhase: ${watch.elapsedMilliseconds}ms');
}
}
}
/// Converts [AugmentResponse] to [macros_api_v1.MacroExecutionResult].
///
/// TODO(davidmorgan): add to `AugmentationResponse` to cover all the
/// functionality of `MacroExecutionResult`.
class AnalyzerMacroExecutionResult
implements macros_api_v1.MacroExecutionResult {
final macros_api_v1.MacroTarget target;
@override
final Map<macros_api_v1.Identifier, Iterable<macros_api_v1.DeclarationCode>>
typeAugmentations;
AnalyzerMacroExecutionResult(
this.target, Iterable<macros_api_v1.DeclarationCode> declarations)
// TODO(davidmorgan): this assumes augmentations are for the macro
// application target. Instead, it should be explicit in
// `AugmentResponse`.
: typeAugmentations = {
(target as macros_api_v1.Declaration).identifier: declarations
};
static Future<AnalyzerMacroExecutionResult> dartModelToInjected(
macros_api_v1.MacroTarget target, AugmentResponse augmentResponse) async {
final declarations = <macros_api_v1.DeclarationCode>[];
if (augmentResponse.typeAugmentations?.isNotEmpty == true) {
// TODO: Handle targets that are not interfaces, or test that this works
// already.
for (final entry in augmentResponse.typeAugmentations!.entries) {
if (entry.key != target.qualifiedName.name) {
throw UnimplementedError(
'Type augmentations are only implemented when the type is the '
'target of the augmentation, expected '
'${target.qualifiedName.name} but got ${entry.key}');
}
for (final augmentation in entry.value) {
declarations.add(macros_api_v1.DeclarationCode.fromParts(
await _resolveNames(augmentation.code)));
}
}
}
for (final augmentation
in augmentResponse.libraryAugmentations ?? const <Augmentation>[]) {
declarations.add(macros_api_v1.DeclarationCode.fromParts(
await _resolveNames(augmentation.code)));
}
if (augmentResponse.enumValueAugmentations?.isNotEmpty == true) {
throw UnimplementedError('Enum value augmentations are not implemented');
}
if (augmentResponse.extendsTypeAugmentations?.isNotEmpty == true ||
augmentResponse.interfaceAugmentations?.isNotEmpty == true ||
augmentResponse.mixinAugmentations?.isNotEmpty == true) {
throw UnimplementedError('Type augmentations are not implemented');
}
return AnalyzerMacroExecutionResult(target, declarations);
}
@override
List<macros_api_v1.Diagnostic> get diagnostics => [];
@override
Map<macros_api_v1.Identifier, Iterable<macros_api_v1.DeclarationCode>>
get enumValueAugmentations => {};
@override
macros_api_v1.MacroException? get exception => null;
@override
Map<macros_api_v1.Identifier, macros_api_v1.NamedTypeAnnotationCode>
get extendsTypeAugmentations => {};
@override
Map<macros_api_v1.Identifier, Iterable<macros_api_v1.TypeAnnotationCode>>
get interfaceAugmentations => {};
@override
Iterable<macros_api_v1.DeclarationCode> get libraryAugmentations => {};
@override
Map<macros_api_v1.Identifier, Iterable<macros_api_v1.TypeAnnotationCode>>
get mixinAugmentations => {};
@override
Iterable<String> get newTypeNames => [];
@override
void serialize(Object serializer) => throw UnimplementedError();
}
extension MacroTargetExtension on macros_api_v1.MacroTarget {
QualifiedName get qualifiedName {
final element = ((this as macros_api_v1.Declaration).identifier
as analyzer.IdentifierImpl)
.element!;
return element.qualifiedName;
}
}
extension QualifiedNameForElement on Element {
QualifiedName get qualifiedName {
final uri = '${library!.definingCompilationUnit.source.uri}';
final enclosingElement = enclosingElement3;
if (enclosingElement == null) {
throw UnsupportedError('Library macro targets are not yet supported');
}
final scope = (enclosingElement is LibraryElement ||
enclosingElement is CompilationUnitElement)
? null
: enclosingElement.displayName;
final isStatic = scope == null
? null
: switch (this) {
ClassMemberElement self => self.isStatic,
_ => throw UnimplementedError(
'Cannot create a QualifiedName for $runtimeType'),
};
return QualifiedName(
uri: uri, name: displayName, scope: scope, isStatic: isStatic);
}
}
/// Converts [codes] to a list of `String` and `Identifier`.
Future<List<Object>> _resolveNames(List<Code> codes) async {
// Find the set of unique [QualifiedName]s used.
final qualifiedNameStrings = <String>{};
for (final code in codes) {
if (code.type == CodeType.qualifiedName) {
qualifiedNameStrings.add(code.asQualifiedName.asString);
}
}
// Create futures looking up their [Identifier]s, then `await` in parallel.
final qualifiedNamesList =
qualifiedNameStrings.map(QualifiedName.parse).toList();
final identifierFutures = <Future<macros_api_v1.Identifier>>[];
for (final qualifiedName in qualifiedNamesList) {
identifierFutures.add((introspector as macros_api_v1.TypePhaseIntrospector)
// ignore: deprecated_member_use
.resolveIdentifier(Uri.parse(qualifiedName.uri), qualifiedName.name));
}
final identifiers = await Future.wait(identifierFutures);
// Build the result using the looked up [Identifier]s.
final identifiersByQualifiedNameStrings =
Map.fromIterables(qualifiedNameStrings, identifiers);
final result = <Object>[];
for (final code in codes) {
if (code.type == CodeType.string) {
result.add(code.asString);
} else if (code.type == CodeType.qualifiedName) {
final qualifiedName = code.asQualifiedName;
result.add(identifiersByQualifiedNameStrings[qualifiedName.asString]!);
}
}
return result;
}