blob: c9ed3f7a06709817c0536bd3a79ab77d1a4ac164 [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 'dart:math';
import 'package:path/path.dart' as p;
import 'macro_runner.dart';
final _random = Random.secure();
/// Dart source file manipulation for `_macro_tool`.
extension type SourceFile(String path) implements String {
static const _addedMarker = '// added by macro_tool';
static final _cacheBusterString = 'CACHEBUSTER';
static final _cacheBusterRegexp = RegExp('$_cacheBusterString[a-z0-9]*');
/// Returns all `*.dart` files under `workspacePath`, including in
/// subdirectories.s
static List<SourceFile> findDartInWorkspace(String workspacePath) =>
Directory(workspacePath)
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('.dart'))
.map((f) => SourceFile(f.path))
.toList()
..sort((a, b) => a.path.compareTo(b.path));
/// The source file path plus `.macro_tool_output`.
String get toolOutputPath => '$path.macro_tool_output';
/// Writes [output] to [toolOutputPath].
///
/// [macroRunner] is notified of the change.
void writeOutput(MacroRunner macroRunner, String output) {
File(toolOutputPath).writeAsStringSync(output);
macroRunner.notifyChange(toolOutputPath);
}
/// Patches [path] so the analyzer can analyze it without running macros.
///
/// Adds a `part` statement referencing [toolOutputPath].
///
/// [macroRunner] is notified of the change.
void patchForAnalyzer(MacroRunner macroRunner) {
final partName = p.basename(toolOutputPath);
final line = "part '$partName'; $_addedMarker\n";
final file = File(path);
file.writeAsStringSync(
_insertAfterLastImport(
line,
_removeToolAddedLinesFromSource(file.readAsStringSync()),
),
);
macroRunner.notifyChange(path);
}
String _insertAfterLastImport(String line, String source) {
final importRegexp = RegExp(r'^import .*;$', multiLine: true);
final index = source.lastIndexOf(importRegexp);
if (index == -1) return line + source;
final nextLineIndex = index + source.substring(index).indexOf('\n') + 1;
return source.substring(0, nextLineIndex) +
line +
source.substring(nextLineIndex);
}
/// Patches [path] and [toolOutputPath] so the CFE can run then without
/// running macros.
///
/// This means changing [toolOutputPath] from using parts to library
/// augmentations and adding `import augment` to [path].
///
/// [macroRunner] is notified of the change.
void patchForCfe(MacroRunner macroRunner) {
final partName = p.basename(toolOutputPath);
final line = "import augment '$partName'; $_addedMarker\n";
final file = File(path);
file.writeAsStringSync(
line + _removeToolAddedLinesFromSource(file.readAsStringSync()),
);
macroRunner.notifyChange(path);
final toolOutputFile = File(toolOutputPath);
toolOutputFile.writeAsStringSync(
toolOutputFile.readAsStringSync().replaceAll(
'part of ',
'augment library ',
),
);
macroRunner.notifyChange(toolOutputPath);
}
/// Reverts changes to source from any of [patchForAnalyzer], [patchForCfe]
/// and/or [bustCaches].
///
/// [macroRunner] is notified of the change.
void revert(MacroRunner macroRunner) {
final file = File(path);
file.writeAsStringSync(
_resetCacheBusters(
_removeToolAddedLinesFromSource(file.readAsStringSync()),
),
);
macroRunner.notifyChange(path);
final toolOutputFile = File(toolOutputPath);
if (toolOutputFile.existsSync()) {
toolOutputFile.deleteSync();
macroRunner.notifyChange(toolOutputPath);
}
}
/// Returns [source] with lines added by [_addImportAugment] removed.
String _removeToolAddedLinesFromSource(String source) =>
source.split('\n').where((l) => !l.endsWith(_addedMarker)).join('\n');
/// Updates the file to trigger macro rerun.
///
/// The file must contain the string `CACHEBUSTER` in a place that triggers
/// recomputation, for example in a field name.
///
/// If there is an augmentation output file, updates that too.
///
/// Returns whether any change was made to a file.
///
/// [macroRunner] is notified of the change.
bool bustCaches(MacroRunner macroRunner) {
final token =
_random.nextInt(1 << 32).toRadixString(16) +
_random.nextInt(1 << 32).toRadixString(16);
var cacheBusterFound = false;
for (final path in [path, toolOutputPath]) {
final file = File(path);
if (!file.existsSync()) continue;
final source = file.readAsStringSync();
if (source.contains(_cacheBusterRegexp)) {
cacheBusterFound = true;
file.writeAsStringSync(
source.replaceAll(_cacheBusterRegexp, '$_cacheBusterString$token'),
);
macroRunner.notifyChange(path);
}
}
return cacheBusterFound;
}
String _resetCacheBusters(String source) =>
source.replaceAll(_cacheBusterRegexp, _cacheBusterString);
}