| // 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); |
| } |