| #!/usr/bin/env dart |
| // Copyright (c) 2015, 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. |
| |
| /// Command line tool to merge the SDK libraries and our patch files. |
| /// This is currently designed as an offline tool, but we could automate it. |
| |
| import 'dart:io'; |
| import 'dart:isolate' show RawReceivePort; |
| import 'dart:async'; |
| import 'dart:math' as math; |
| import 'dart:convert' show JSON; |
| |
| import 'package:analyzer/analyzer.dart'; |
| import 'package:analyzer/src/generated/sdk.dart'; |
| import 'package:path/path.dart' as path; |
| |
| import 'package:front_end/src/api_prototype/front_end.dart'; |
| |
| import 'package:front_end/src/base/processed_options.dart'; |
| import 'package:front_end/src/kernel_generator_impl.dart'; |
| import 'package:front_end/src/fasta/util/relativize.dart' show relativizeUri; |
| |
| import 'package:front_end/src/fasta/get_dependencies.dart' show getDependencies; |
| import 'package:front_end/src/fasta/kernel/utils.dart' |
| show writeComponentToFile; |
| |
| import 'package:kernel/target/targets.dart'; |
| import 'package:kernel/target/vm.dart' show VmTarget; |
| import 'package:kernel/target/flutter.dart' show FlutterTarget; |
| import 'package:vm/target/runner.dart' show RunnerTarget; |
| import 'package:compiler/src/kernel/dart2js_target.dart' show Dart2jsTarget; |
| |
| /// Set of input files that were read by this script to generate patched SDK. |
| /// We will dump it out into the depfile for ninja to use. |
| /// |
| /// For more information see GN and Ninja references: |
| /// https://chromium.googlesource.com/chromium/src/+/56807c6cb383140af0c03da8f6731d77785d7160/tools/gn/docs/reference.md#depfile_string_File-name-for-input-dependencies-for-actions |
| /// https://ninja-build.org/manual.html#_depfile |
| /// |
| final deps = new Set<Uri>(); |
| |
| /// Create [File] object from the given path and register it as a dependency. |
| File getInputFile(String path, {canBeMissing: false}) { |
| final file = new File(path); |
| if (!file.existsSync()) { |
| if (!canBeMissing) |
| throw "patch_sdk.dart expects all inputs to exist, missing: $path"; |
| return null; |
| } |
| deps.add(Uri.base.resolveUri(file.uri)); |
| return file; |
| } |
| |
| /// Read the given file synchronously as a string and register this path as |
| /// a dependency. |
| String readInputFile(String path, {canBeMissing: false}) => |
| getInputFile(path, canBeMissing: canBeMissing)?.readAsStringSync(); |
| |
| Future main(List<String> argv) async { |
| var port = new RawReceivePort(); |
| try { |
| await _main(argv); |
| } finally { |
| port.close(); |
| } |
| } |
| |
| void usage(String mode) { |
| var base = path.fromUri(Platform.script); |
| final self = path.relative(base); |
| print('Usage: $self $mode SDK_DIR PATCH_DIR OUTPUT_DIR PACKAGES'); |
| |
| final repositoryDir = path.relative(path.dirname(path.dirname(base))); |
| final sdkExample = path.relative(path.join(repositoryDir, 'sdk')); |
| final patchExample = path.relative( |
| path.join(repositoryDir, 'out', 'DebugX64', 'obj', 'gen', 'patch')); |
| final outExample = path.relative( |
| path.join(repositoryDir, 'out', 'DebugX64', 'obj', 'gen', 'patched_sdk')); |
| final packagesExample = path.relative(path.join(repositoryDir, '.packages')); |
| print('For example:'); |
| print('\$ $self vm $sdkExample $patchExample $outExample $packagesExample'); |
| |
| exit(1); |
| } |
| |
| const validModes = const ['vm', 'flutter', 'runner']; |
| String mode; |
| bool get forVm => mode == 'vm'; |
| bool get forFlutter => mode == 'flutter'; |
| bool get forRunner => mode == 'runner'; |
| |
| Future _main(List<String> argv) async { |
| if (argv.isEmpty) usage('[${validModes.join('|')}]'); |
| mode = argv.first; |
| if (!validModes.contains(mode)) usage('[${validModes.join('|')}]'); |
| if (argv.length != 5) usage(mode); |
| |
| var input = argv[1]; |
| var sdkLibIn = path.join(input, 'lib'); |
| var patchIn = argv[2]; |
| var outDir = argv[3]; |
| var outDirUri = Uri.base.resolveUri(new Uri.directory(outDir)); |
| var sdkOut = path.join(outDir, 'lib'); |
| var packagesFile = argv[4]; |
| |
| await new Directory.fromUri(outDirUri).delete(recursive: true); |
| |
| // Parse libraries.dart |
| var libContents = readInputFile(path.join( |
| sdkLibIn, '_internal', 'sdk_library_metadata', 'lib', 'libraries.dart')); |
| libContents = _updateLibraryMetadata(sdkOut, libContents); |
| var sdkLibraries = _getSdkLibraries(libContents); |
| |
| var locations = <String, Map<String, String>>{}; |
| |
| // Enumerate core libraries and apply patches |
| for (SdkLibrary library in sdkLibraries) { |
| if (library.isDart2JsLibrary) continue; |
| _applyPatch(library, sdkLibIn, patchIn, sdkOut, locations); |
| } |
| |
| _copyExtraLibraries(sdkOut, locations); |
| |
| final Uri platform = outDirUri.resolve('platform.dill.tmp'); |
| final Uri librariesJson = outDirUri.resolve("lib/libraries.json"); |
| final Uri packages = Uri.base.resolveUri(new Uri.file(packagesFile)); |
| TargetFlags flags = new TargetFlags(); |
| Target target; |
| |
| switch (mode) { |
| case 'vm': |
| target = new VmTarget(flags); |
| break; |
| |
| case 'runner': |
| target = new RunnerTarget(flags); |
| break; |
| |
| case 'flutter': |
| case 'flutter_release': |
| target = new FlutterTarget(flags); |
| break; |
| |
| case 'dart2js': |
| target = new Dart2jsTarget(flags); |
| break; |
| |
| default: |
| throw "Unknown mode: $mode"; |
| } |
| |
| await _writeSync( |
| librariesJson.toFilePath(), |
| JSON.encode({ |
| mode: {"libraries": locations} |
| })); |
| |
| await compilePlatform(outDirUri, target, packages, platform); |
| |
| // We generate a dependency file for GN to properly regenerate the patched sdk |
| // folder, outline.dill and platform.dill files when necessary: either when |
| // the sdk sources change or when this script is updated. In particular: |
| // |
| // - sdk changes: we track the actual sources we are compiling. If we are |
| // building the dart2js sdk, this includes the dart2js-specific patch |
| // files. |
| // |
| // These files are tracked by [deps] and passed below to [writeDepsFile] in |
| // the extraDependencies argument. |
| // |
| // - script updates: we track this script file and any code it imports (even |
| // sdk libraries). Note that this script runs on the standalone VM, so any |
| // sdk library used by this script indirectly depends on a VM-specific |
| // patch file. |
| // |
| // These set of files is discovered by `getDependencies` below, and the |
| // [platformForDeps] is always the VM-specific `platform.dill` file. |
| var platformForDeps = platform; |
| var sdkDir = outDirUri; |
| if (forFlutter || forRunner) { |
| // Note: this fails if `$root_out_dir/vm_platform.dill` doesn't exist. The |
| // target to build the flutter patched sdk depends on |
| // //runtime/vm:kernel_platform_files to ensure this file exists. |
| platformForDeps = outDirUri.resolve('../vm_platform.dill'); |
| sdkDir = null; |
| } |
| deps.addAll(await getDependencies(Platform.script, |
| sdk: sdkDir, packages: packages, platform: platformForDeps)); |
| await writeDepsFile( |
| librariesJson, Uri.base.resolveUri(new Uri.file("$outDir.d")), deps); |
| } |
| |
| /// Generates an outline.dill and platform.dill file containing the result of |
| /// compiling a platform's SDK. |
| /// |
| /// Returns a list of dependencies read by the compiler. This list can be used |
| /// to create GN dependency files. |
| Future<List<Uri>> compilePlatform( |
| Uri patchedSdk, Target target, Uri packages, Uri output) async { |
| var options = new CompilerOptions() |
| ..setExitCodeOnProblem = true |
| ..strongMode = false |
| ..compileSdk = true |
| ..sdkRoot = patchedSdk |
| ..packagesFileUri = packages |
| ..chaseDependencies = true |
| ..target = target; |
| |
| var inputs = [Uri.parse('dart:core')]; |
| var result = await generateKernel( |
| new ProcessedOptions( |
| options, |
| // TODO(sigmund): pass all sdk libraries needed here, and make this |
| // hermetic. |
| false, |
| inputs), |
| buildSummary: true, |
| buildComponent: true); |
| await writeComponentToFile(result.component, output); |
| return result.deps; |
| } |
| |
| Future writeDepsFile( |
| Uri output, Uri depsFile, Iterable<Uri> allDependencies) async { |
| if (allDependencies.isEmpty) return; |
| String toRelativeFilePath(Uri uri) { |
| // Ninja expects to find file names relative to the current working |
| // directory. We've tried making them relative to the deps file, but that |
| // doesn't work for downstream projects. Making them absolute also |
| // doesn't work. |
| // |
| // We can test if it works by running ninja twice, for example: |
| // |
| // ninja -C xcodebuild/ReleaseX64 runtime_kernel -d explain |
| // ninja -C xcodebuild/ReleaseX64 runtime_kernel -d explain |
| // |
| // The second time, ninja should say: |
| // |
| // ninja: Entering directory `xcodebuild/ReleaseX64' |
| // ninja: no work to do. |
| // |
| // It's broken if it says something like this: |
| // |
| // ninja explain: expected depfile 'patched_sdk.d' to mention |
| // 'patched_sdk/platform.dill', got |
| // '/.../xcodebuild/ReleaseX64/patched_sdk/platform.dill' |
| return Uri.parse(relativizeUri(uri, base: Uri.base)).toFilePath(); |
| } |
| |
| StringBuffer sb = new StringBuffer(); |
| sb.write(toRelativeFilePath(output)); |
| sb.write(":"); |
| for (Uri uri in allDependencies) { |
| sb.write(" "); |
| sb.write(toRelativeFilePath(uri)); |
| } |
| sb.writeln(); |
| await new File.fromUri(depsFile).writeAsString("$sb"); |
| } |
| |
| /// Updates the contents of |
| /// sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart to include |
| /// declarations for vm internal libraries. |
| String _updateLibraryMetadata(String sdkOut, String libContents) { |
| if (!forVm && !forFlutter && !forRunner) return libContents; |
| var extraLibraries = new StringBuffer(); |
| extraLibraries.write(''' |
| "_builtin": const LibraryInfo( |
| "_builtin/_builtin.dart", |
| categories: "Client,Server", |
| implementation: true, |
| documented: false, |
| platforms: VM_PLATFORM), |
| |
| "profiler": const LibraryInfo( |
| "profiler/profiler.dart", |
| maturity: Maturity.DEPRECATED, |
| documented: false), |
| |
| "_vmservice": const LibraryInfo( |
| "vmservice/vmservice.dart", |
| categories: "Client,Server", |
| implementation: true, |
| documented: false, |
| platforms: VM_PLATFORM), |
| |
| "vmservice_io": const LibraryInfo( |
| "vmservice_io/vmservice_io.dart", |
| categories: "Client,Server", |
| implementation: true, |
| documented: false, |
| platforms: VM_PLATFORM), |
| '''); |
| |
| if (forFlutter) { |
| extraLibraries.write(''' |
| "ui": const LibraryInfo( |
| "ui/ui.dart", |
| categories: "Client,Server", |
| implementation: true, |
| documented: false, |
| platforms: VM_PLATFORM), |
| '''); |
| } |
| |
| if (forRunner) { |
| extraLibraries.write(''' |
| "fuchsia.builtin": const LibraryInfo( |
| "fuchsia.builtin/builtin.dart", |
| categories: "Client,Server", |
| implementation: true, |
| documented: false, |
| platforms: VM_PLATFORM), |
| '''); |
| extraLibraries.write(''' |
| "zircon": const LibraryInfo( |
| "zircon/zircon.dart", |
| categories: "Client,Server", |
| implementation: true, |
| documented: false, |
| platforms: VM_PLATFORM), |
| '''); |
| extraLibraries.write(''' |
| "fuchsia": const LibraryInfo( |
| "fuchsia/fuchsia.dart", |
| categories: "Client,Server", |
| implementation: true, |
| documented: false, |
| platforms: VM_PLATFORM), |
| '''); |
| } |
| |
| libContents = libContents.replaceAll( |
| ' libraries = const {', ' libraries = const { $extraLibraries'); |
| _writeSync( |
| path.join( |
| sdkOut, '_internal', 'sdk_library_metadata', 'lib', 'libraries.dart'), |
| libContents); |
| return libContents; |
| } |
| |
| /// Copy internal libraries that are developed outside the sdk folder into the |
| /// patched_sdk folder. For the VM< this includes files under 'runtime/bin/', |
| /// for flutter, this is includes also the ui library. |
| _copyExtraLibraries(String sdkOut, Map<String, Map<String, String>> locations) { |
| var base = path.fromUri(Platform.script); |
| var dartDir = path.dirname(path.dirname(path.absolute(base))); |
| |
| var builtinLibraryIn = path.join(dartDir, 'runtime', 'bin', 'builtin.dart'); |
| var builtinLibraryOut = path.join(sdkOut, '_builtin', '_builtin.dart'); |
| _writeSync(builtinLibraryOut, readInputFile(builtinLibraryIn)); |
| addLocation(locations, '_builtin', path.join('_builtin', '_builtin.dart')); |
| for (var file in ['loader.dart', 'server.dart', 'vmservice_io.dart']) { |
| var libraryIn = path.join(dartDir, 'runtime', 'bin', 'vmservice', file); |
| var libraryOut = path.join(sdkOut, 'vmservice_io', file); |
| _writeSync(libraryOut, readInputFile(libraryIn)); |
| } |
| addLocation(locations, 'vmservice_io', |
| path.join('vmservice_io', 'vmservice_io.dart')); |
| addLocation( |
| locations, '_vmservice', path.join('vmservice', 'vmservice.dart')); |
| |
| if (forFlutter) { |
| // Flutter repo has this layout: |
| // engine/src/ |
| // third_party/dart/ |
| // [third_party/]flutter/ |
| var srcDir = path |
| .dirname(path.dirname(path.dirname(path.dirname(path.absolute(base))))); |
| var flutterDir = new Directory(path.join(srcDir, 'flutter')); |
| if (!flutterDir.existsSync()) { |
| // In Fuchsia Flutter is under 'third_party'. |
| flutterDir = new Directory(path.join(srcDir, 'third_party', 'flutter')); |
| } |
| var uiLibraryInDir = new Directory(path.join(flutterDir.path, 'lib', 'ui')); |
| for (var file in uiLibraryInDir.listSync()) { |
| if (!file.path.endsWith('.dart')) continue; |
| var name = path.basename(file.path); |
| var uiLibraryOut = path.join(sdkOut, 'ui', name); |
| _writeSync(uiLibraryOut, readInputFile(file.path)); |
| } |
| addLocation(locations, 'ui', path.join('ui', 'ui.dart')); |
| } |
| |
| if (forRunner) { |
| var gnRoot = path |
| .dirname(path.dirname(path.dirname(path.dirname(path.absolute(base))))); |
| |
| var builtinLibraryInDir = new Directory( |
| path.join(gnRoot, 'topaz', 'runtime', 'dart_runner', 'embedder')); |
| for (var file in builtinLibraryInDir.listSync()) { |
| if (!file.path.endsWith('.dart')) continue; |
| var name = path.basename(file.path); |
| var builtinLibraryOut = path.join(sdkOut, 'fuchsia.builtin', name); |
| _writeSync(builtinLibraryOut, readInputFile(file.path)); |
| } |
| addLocation(locations, 'fuchsia.builtin', |
| path.join('fuchsia.builtin', 'builtin.dart')); |
| |
| var zirconLibraryInDir = new Directory( |
| path.join(gnRoot, 'topaz', 'public', 'dart-pkg', 'zircon', 'lib')); |
| for (var file in zirconLibraryInDir.listSync(recursive: true)) { |
| if (!file.path.endsWith('.dart')) continue; |
| var name = file.path.substring(zirconLibraryInDir.path.length + 1); |
| var zirconLibraryOut = path.join(sdkOut, 'zircon', name); |
| _writeSync(zirconLibraryOut, readInputFile(file.path)); |
| } |
| addLocation(locations, 'zircon', path.join('zircon', 'zircon.dart')); |
| |
| var fuchsiaLibraryInDir = new Directory( |
| path.join(gnRoot, 'topaz', 'public', 'dart-pkg', 'fuchsia', 'lib')); |
| for (var file in fuchsiaLibraryInDir.listSync(recursive: true)) { |
| if (!file.path.endsWith('.dart')) continue; |
| var name = file.path.substring(fuchsiaLibraryInDir.path.length + 1); |
| var fuchsiaLibraryOut = path.join(sdkOut, 'fuchsia', name); |
| _writeSync(fuchsiaLibraryOut, readInputFile(file.path)); |
| } |
| addLocation(locations, 'fuchsia', path.join('fuchsia', 'fuchsia.dart')); |
| } |
| } |
| |
| _applyPatch(SdkLibrary library, String sdkLibIn, String patchIn, String sdkOut, |
| Map<String, Map<String, String>> locations) { |
| var libraryOut = path.join(sdkLibIn, library.path); |
| var libraryIn = libraryOut; |
| |
| var libraryFile = getInputFile(libraryIn, canBeMissing: true); |
| if (libraryFile != null) { |
| addLocation(locations, Uri.parse(library.shortName).path, |
| path.relative(libraryOut, from: sdkLibIn)); |
| var outPaths = <String>[libraryOut]; |
| var libraryContents = libraryFile.readAsStringSync(); |
| |
| int inputModifyTime = libraryFile.lastModifiedSync().millisecondsSinceEpoch; |
| var partFiles = <File>[]; |
| for (var part in parseDirectives(libraryContents).directives) { |
| if (part is PartDirective) { |
| var partPath = part.uri.stringValue; |
| outPaths.add(path.join(path.dirname(libraryOut), partPath)); |
| |
| var partFile = |
| getInputFile(path.join(path.dirname(libraryIn), partPath)); |
| partFiles.add(partFile); |
| inputModifyTime = math.max(inputModifyTime, |
| partFile.lastModifiedSync().millisecondsSinceEpoch); |
| } |
| } |
| |
| // See if we can find a patch file. |
| var patchPath = path.join( |
| patchIn, path.basenameWithoutExtension(libraryIn) + '_patch.dart'); |
| |
| var patchFile = getInputFile(patchPath, canBeMissing: true); |
| if (patchFile != null) { |
| inputModifyTime = math.max( |
| inputModifyTime, patchFile.lastModifiedSync().millisecondsSinceEpoch); |
| } |
| |
| // Compute output paths |
| outPaths = outPaths |
| .map((p) => path.join(sdkOut, path.relative(p, from: sdkLibIn))) |
| .toList(); |
| |
| // Compare output modify time with input modify time. |
| bool needsUpdate = false; |
| for (var outPath in outPaths) { |
| var outFile = new File(outPath); |
| if (!outFile.existsSync() || |
| outFile.lastModifiedSync().millisecondsSinceEpoch < inputModifyTime) { |
| needsUpdate = true; |
| break; |
| } |
| } |
| |
| if (needsUpdate) { |
| var contents = <String>[libraryContents]; |
| contents.addAll(partFiles.map((f) => f.readAsStringSync())); |
| if (patchFile != null) { |
| var patchContents = patchFile.readAsStringSync(); |
| contents = _patchLibrary(patchFile.path, contents, patchContents); |
| } |
| |
| for (var i = 0; i < outPaths.length; i++) { |
| _writeSync(outPaths[i], contents[i]); |
| } |
| } |
| } |
| } |
| |
| /// Writes a file, creating the directory if needed. |
| void _writeSync(String filePath, String contents) { |
| var outDir = new Directory(path.dirname(filePath)); |
| if (!outDir.existsSync()) outDir.createSync(recursive: true); |
| |
| new File(filePath).writeAsStringSync(contents); |
| } |
| |
| /// Merges dart:* library code with code from *_patch.dart file. |
| /// |
| /// Takes a list of the library's parts contents, with the main library contents |
| /// first in the list, and the contents of the patch file. |
| /// |
| /// The result will have `@patch` implementations merged into the correct place |
| /// (e.g. the class or top-level function declaration) and all other |
| /// declarations introduced by the patch will be placed into the main library |
| /// file. |
| /// |
| /// This is purely a syntactic transformation. Unlike dart2js patch files, there |
| /// is no semantic meaning given to the *_patch files, and they do not magically |
| /// get their own library scope, etc. |
| /// |
| /// Editorializing: the dart2js approach requires a Dart front end such as |
| /// package:analyzer to semantically model a feature beyond what is specified |
| /// in the Dart language. Since this feature is only for the convenience of |
| /// writing the dart:* libraries, and not a tool given to Dart developers, it |
| /// seems like a non-ideal situation. Instead we keep the preprocessing simple. |
| List<String> _patchLibrary( |
| String name, List<String> partsContents, String patchContents) { |
| var results = <StringEditBuffer>[]; |
| |
| // Parse the patch first. We'll need to extract bits of this as we go through |
| // the other files. |
| final patchFinder = new PatchFinder.parseAndVisit(name, patchContents); |
| |
| // Merge `external` declarations with the corresponding `@patch` code. |
| for (var partContent in partsContents) { |
| var partEdits = new StringEditBuffer(partContent); |
| var partUnit = parseCompilationUnit(partContent); |
| partUnit.accept(new PatchApplier(partEdits, patchFinder)); |
| results.add(partEdits); |
| } |
| |
| if (patchFinder.patches.length != patchFinder.applied.length) { |
| print('Some elements marked as @patch do not have corresponding elements:'); |
| for (var patched in patchFinder.patches.keys) { |
| if (!patchFinder.applied.contains(patched)) { |
| print('*** ${patched}'); |
| } |
| } |
| throw "Failed to apply all @patch-es"; |
| } |
| |
| return new List<String>.from(results.map((e) => e.toString())); |
| } |
| |
| final String injectedCidFields = [ |
| 'Array', |
| 'ExternalOneByteString', |
| 'GrowableObjectArray', |
| 'ImmutableArray', |
| 'OneByteString', |
| 'TwoByteString', |
| 'Bigint' |
| ].map((name) => "static final int cid${name} = 0;").join('\n'); |
| |
| /// Merge `@patch` declarations into `external` declarations. |
| class PatchApplier extends GeneralizingAstVisitor { |
| final StringEditBuffer edits; |
| final PatchFinder patch; |
| |
| bool _isLibrary = true; // until proven otherwise. |
| |
| PatchApplier(this.edits, this.patch); |
| |
| @override |
| visitCompilationUnit(CompilationUnit node) { |
| super.visitCompilationUnit(node); |
| if (_isLibrary) _mergeUnpatched(node); |
| } |
| |
| void _merge(AstNode node, int pos) { |
| var code = patch.contents.substring(node.offset, node.end); |
| |
| // We inject a number of static fields into dart:internal.ClassID class. |
| // These fields represent various VM class ids and are only used to |
| // make core libraries compile. Kernel reader will actually ignore these |
| // fields and instead inject concrete constants into this class. |
| if (node is ClassDeclaration && node.name.name == 'ClassID') { |
| code = code.replaceFirst(new RegExp(r'}$'), injectedCidFields + '}'); |
| } |
| edits.insert(pos, '\n' + code); |
| } |
| |
| /// Merges directives and declarations that are not `@patch` into the library. |
| void _mergeUnpatched(CompilationUnit unit) { |
| // Merge imports from the patch |
| // TODO(jmesserly): remove duplicate imports |
| |
| // To patch a library, we must have a library directive |
| var libDir = unit.directives.first as LibraryDirective; |
| int importPos = unit.directives |
| .lastWhere((d) => d is ImportDirective, orElse: () => libDir) |
| .end; |
| for (var d in patch.unit.directives.where((d) => d is ImportDirective)) { |
| _merge(d, importPos); |
| } |
| |
| int partPos = unit.directives.last.end; |
| for (var d in patch.unit.directives.where((d) => d is PartDirective)) { |
| _merge(d, partPos); |
| } |
| |
| // Merge declarations from the patch |
| int declPos = edits.original.length; |
| for (var d in patch.mergeDeclarations) { |
| _merge(d, declPos); |
| } |
| } |
| |
| @override |
| visitPartOfDirective(PartOfDirective node) { |
| _isLibrary = false; |
| } |
| |
| @override |
| visitFunctionDeclaration(FunctionDeclaration node) { |
| _maybePatch(node); |
| } |
| |
| /// Merge patches and extensions into the class |
| @override |
| visitClassDeclaration(ClassDeclaration node) { |
| node.members.forEach(_maybePatch); |
| |
| var mergeMembers = patch.mergeMembers[_qualifiedName(node)]; |
| if (mergeMembers == null) return; |
| |
| // Merge members from the patch |
| var pos = node.members.last.end; |
| for (var member in mergeMembers) { |
| var code = patch.contents.substring(member.offset, member.end); |
| edits.insert(pos, '\n\n ' + code); |
| } |
| } |
| |
| void _maybePatch(AstNode node) { |
| if (node is FieldDeclaration) return; |
| |
| var externalKeyword = (node as dynamic).externalKeyword; |
| |
| var name = _qualifiedName(node); |
| var patchNode = patch.patches[name]; |
| if (patchNode == null) { |
| if (externalKeyword != null) { |
| print('warning: patch not found for $name: $node'); |
| exitCode = 1; |
| } |
| return; |
| } |
| patch.applied.add(name); |
| |
| Annotation patchMeta = patchNode.metadata.lastWhere(_isPatchAnnotation); |
| int start = patchMeta.endToken.next.offset; |
| var code = patch.contents.substring(start, patchNode.end); |
| |
| // For some node like static fields, the node's offset doesn't include |
| // the external keyword. Also starting from the keyword lets us preserve |
| // documentation comments. |
| edits.replace(externalKeyword?.offset ?? node.offset, node.end, code); |
| } |
| } |
| |
| class PatchFinder extends GeneralizingAstVisitor { |
| final String contents; |
| final CompilationUnit unit; |
| |
| final Map patches = <String, Declaration>{}; |
| final Map mergeMembers = <String, List<ClassMember>>{}; |
| final List mergeDeclarations = <CompilationUnitMember>[]; |
| final Set<String> applied = new Set<String>(); |
| |
| PatchFinder.parseAndVisit(String name, String contents) |
| : contents = contents, |
| unit = parseCompilationUnit(contents, name: name) { |
| visitCompilationUnit(unit); |
| } |
| |
| @override |
| visitCompilationUnitMember(CompilationUnitMember node) { |
| mergeDeclarations.add(node); |
| } |
| |
| @override |
| visitClassDeclaration(ClassDeclaration node) { |
| if (_isPatch(node)) { |
| var members = <ClassMember>[]; |
| for (var member in node.members) { |
| if (_isPatch(member)) { |
| patches[_qualifiedName(member)] = member; |
| } else { |
| members.add(member); |
| } |
| } |
| if (members.isNotEmpty) { |
| mergeMembers[_qualifiedName(node)] = members; |
| } |
| } else { |
| mergeDeclarations.add(node); |
| } |
| } |
| |
| @override |
| visitFunctionDeclaration(FunctionDeclaration node) { |
| if (_isPatch(node)) { |
| patches[_qualifiedName(node)] = node; |
| } else { |
| mergeDeclarations.add(node); |
| } |
| } |
| |
| @override |
| visitFunctionBody(node) {} // skip method bodies |
| } |
| |
| String _qualifiedName(Declaration node) { |
| var parent = node.parent; |
| var className = ''; |
| if (parent is ClassDeclaration) { |
| className = parent.name.name + '.'; |
| } |
| var name = (node as dynamic).name; |
| name = (name != null ? name.name : ''); |
| |
| var accessor = ''; |
| if (node is MethodDeclaration) { |
| if (node.isGetter) |
| accessor = 'get:'; |
| else if (node.isSetter) accessor = 'set:'; |
| } |
| return className + accessor + name; |
| } |
| |
| bool _isPatch(AnnotatedNode node) => node.metadata.any(_isPatchAnnotation); |
| |
| bool _isPatchAnnotation(Annotation m) => |
| m.name.name == 'patch' && m.constructorName == null && m.arguments == null; |
| |
| /// Editable string buffer. |
| /// |
| /// Applies a series of edits (insertions, removals, replacements) using |
| /// original location information, and composes them into the edited string. |
| /// |
| /// For example, starting with a parsed AST with original source locations, |
| /// this type allows edits to be made without regards to other edits. |
| class StringEditBuffer { |
| final String original; |
| final _edits = <_StringEdit>[]; |
| |
| /// Creates a new transaction. |
| StringEditBuffer(this.original); |
| |
| bool get hasEdits => _edits.length > 0; |
| |
| /// Edit the original text, replacing text on the range [begin] and |
| /// exclusive [end] with the [replacement] string. |
| void replace(int begin, int end, String replacement) { |
| _edits.add(new _StringEdit(begin, end, replacement)); |
| } |
| |
| /// Insert [string] at [offset]. |
| /// Equivalent to `replace(offset, offset, string)`. |
| void insert(int offset, String string) => replace(offset, offset, string); |
| |
| /// Remove text from the range [begin] to exclusive [end]. |
| /// Equivalent to `replace(begin, end, '')`. |
| void remove(int begin, int end) => replace(begin, end, ''); |
| |
| /// Applies all pending [edit]s and returns a new string. |
| /// |
| /// This method is non-destructive: it does not discard existing edits or |
| /// change the [original] string. Further edits can be added and this method |
| /// can be called again. |
| /// |
| /// Throws [UnsupportedError] if the edits were overlapping. If no edits were |
| /// made, the original string will be returned. |
| String toString() { |
| var sb = new StringBuffer(); |
| if (_edits.length == 0) return original; |
| |
| // Sort edits by start location. |
| _edits.sort(); |
| |
| int consumed = 0; |
| for (var edit in _edits) { |
| if (consumed > edit.begin) { |
| sb = new StringBuffer(); |
| sb.write('overlapping edits. Insert at offset '); |
| sb.write(edit.begin); |
| sb.write(' but have consumed '); |
| sb.write(consumed); |
| sb.write(' input characters. List of edits:'); |
| for (var e in _edits) { |
| sb.write('\n '); |
| sb.write(e); |
| } |
| throw new UnsupportedError(sb.toString()); |
| } |
| |
| // Add characters from the original string between this edit and the last |
| // one, if any. |
| var betweenEdits = original.substring(consumed, edit.begin); |
| sb.write(betweenEdits); |
| sb.write(edit.replace); |
| consumed = edit.end; |
| } |
| |
| // Add any text from the end of the original string that was not replaced. |
| sb.write(original.substring(consumed)); |
| return sb.toString(); |
| } |
| } |
| |
| class _StringEdit implements Comparable<_StringEdit> { |
| final int begin; |
| final int end; |
| final String replace; |
| |
| _StringEdit(this.begin, this.end, this.replace); |
| |
| int get length => end - begin; |
| |
| String toString() => '(Edit @ $begin,$end: "$replace")'; |
| |
| int compareTo(_StringEdit other) { |
| int diff = begin - other.begin; |
| if (diff != 0) return diff; |
| return end - other.end; |
| } |
| } |
| |
| List<SdkLibrary> _getSdkLibraries(String contents) { |
| var libraryBuilder = new SdkLibrariesReader_LibraryBuilder(false); |
| parseCompilationUnit(contents).accept(libraryBuilder); |
| return libraryBuilder.librariesMap.sdkLibraries; |
| } |
| |
| void addLocation(Map<String, Map<String, String>> locations, String libraryName, |
| String libraryPath) { |
| assert(locations[libraryName] == null); |
| locations[libraryName] = {'uri': '${path.toUri(libraryPath)}'}; |
| } |