blob: dfd606b472bbb10da5caa3d40ee4fcb6df24205d [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:io';
import 'macro_runner.dart';
import 'source_file.dart';
/// Runs macros.
///
/// Various functionality related to running macros.
class MacroTool {
final MacroRunner macroRunner;
final String packageConfigPath;
final String workspacePath;
final int benchmarkIterations;
final String? scriptPath;
/// The most recent result from running macros.
WorkspaceResult? _applyResult;
MacroTool({
required this.macroRunner,
required this.packageConfigPath,
required this.workspacePath,
required this.benchmarkIterations,
this.scriptPath,
});
/// Runs macros.
///
/// Writes macro augmentations next to each source file with the extension
/// `.macro_tool_output`.
///
/// Then throws `StateError` if there were any errors.
Future<void> apply() async {
_applyResult = await macroRunner.run();
for (final result in _applyResult!.fileResults) {
if (result.output != null) {
result.sourceFile.writeOutput(macroRunner, result.output!);
}
}
if (_applyResult!.allErrors.isNotEmpty) {
throw StateError('Errors: ${_applyResult!.allErrors}');
}
}
/// Patches source so the analyzer can analyze it without running macros.
///
/// Adds a `part` statement where needed to include augmentation output by
/// `apply`.
void patchForAnalyzer() {
if (_applyResult == null) {
throw UnsupportedError(
'"patch_for_analyzer" command requires "apply" first.',
);
}
for (final result in _applyResult!.fileResults) {
if (result.output != null) {
result.sourceFile.patchForAnalyzer(macroRunner);
}
}
}
/// Patches source and augmentations so the CFE can run then without
/// running macros.
///
/// This means changing augmentations from using parts to library
/// augmentations and adding `import augment` as needed.
void patchForCfe() {
if (_applyResult == null) {
throw UnsupportedError('"patch_for_cfe" command requires "apply" first.');
}
for (final result in _applyResult!.fileResults) {
if (result.output != null) {
result.sourceFile.patchForCfe(macroRunner);
}
}
}
/// Runs the script.
///
/// The process exit code becomes the tool exit code.
Future<int> run() async {
if (scriptPath == null) {
throw UnsupportedError('"run" command requires "--script".');
}
final result = Process.runSync(Platform.resolvedExecutable, [
'run',
'--enable-experiment=macros',
'--enable-experiment=enhanced-parts',
'--packages=$packageConfigPath',
scriptPath!,
], workingDirectory: workspacePath);
stdout.write(result.stdout);
stderr.write(result.stderr);
return result.exitCode;
}
/// Reverts changes to source from any of [patchForAnalyzer], [patchForCfe]
/// and/or [bustCaches].
void revert() {
for (final sourceFile in macroRunner.sourceFiles) {
sourceFile.revert(macroRunner);
}
}
/// Benchmarks [apply].
///
/// Each [apply] returns two timings: the time to first result and the time
/// to last result. For the analyzer, this corresponds to analysis complete
/// for one file in the workspace and analysis complete for all files in the
/// workspace.
///
/// Output is three sets of values separated by double commas:
///
/// 1. Initial apply, first file time then last files time
/// 2. All non-initial applies first file times
/// 3. All non-initial applies last file times
Future<void> benchmarkApply({bool injectImplementation = true}) async {
// Busts caches, applies, throws if error, returns result.
Future<WorkspaceResult> measure() async {
bustCaches();
_applyResult = await macroRunner.run(
injectImplementation: injectImplementation,
);
if (_applyResult!.allErrors.isNotEmpty) {
throw StateError('Errors: ${_applyResult!.allErrors}');
}
return _applyResult!;
}
final initialResult = await measure();
stdout.write('${initialResult.firstResultAfter.inMilliseconds},');
stdout.write('${initialResult.lastResultAfter.inMilliseconds},');
stdout.write(',');
final subsequentResults = <WorkspaceResult>[];
for (var i = 0; i != benchmarkIterations; ++i) {
subsequentResults.add(await measure());
stdout.write('${subsequentResults[i].firstResultAfter.inMilliseconds},');
}
for (var i = 0; i != benchmarkIterations; ++i) {
stdout.write(',${subsequentResults[i].lastResultAfter.inMilliseconds}');
}
print('');
}
/// As [benchmarkApply] but without injecting the `data_model` macro
/// implementation.
Future<void> benchmarkAnalyze() async {
await benchmarkApply(injectImplementation: false);
}
/// Modifies source to avoid cached results, for benchmarking.
///
/// Modifies files with `CACHEBUSTER`. Throws if not found in any source.
void bustCaches() {
var cacheBusterFound = false;
for (final sourceFile in macroRunner.sourceFiles) {
if (sourceFile.bustCaches(macroRunner)) {
cacheBusterFound = true;
}
}
if (!cacheBusterFound) {
throw StateError(
'Did not find CACHEBUSTER in any source, no changes were made.',
);
}
}
/// Loops watching for changes to [scriptPath] and applying after every change.
Future<void> watch() async {
if (scriptPath == null) {
throw UnsupportedError('"watch" command requires "--script".');
}
// `asBroadcastStream` so repeated use of `first` below waits for the next
// change.
var events = File(scriptPath!).watch().asBroadcastStream();
print(
'Caution: timings can be misleading due to JIT warmup, host '
'caching, and random variation. Check with benchmarks :)',
);
print('Watching for changes to: $scriptPath');
while (true) {
_applyResult = await macroRunner.run();
for (final result in _applyResult!.fileResults) {
if (result.output != null) {
result.sourceFile.writeOutput(macroRunner, result.output!);
}
}
if (_applyResult!.allErrors.isNotEmpty) {
print('Errors: ${_applyResult!.allErrors}');
}
stdout.write(
'Macros ran in ${_applyResult!.firstResultAfter.inMilliseconds}ms '
'(${_applyResult!.lastResultAfter.inMilliseconds}ms total),'
' watching...',
);
await events.first;
print('changed, rerunning.');
macroRunner.notifyChange(SourceFile(scriptPath!));
}
}
}