#!/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. // @dart = 2.9 /// 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 'package:_fe_analyzer_shared/src/util/relativize.dart'; import 'package:analyzer/dart/analysis/features.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:args/args.dart'; import 'package:front_end/src/base/libraries_specification.dart'; import 'package:front_end/src/fasta/resolve_input_uri.dart'; import 'package:pub_semver/pub_semver.dart'; void main(List argv) { var args = _parser.parse(argv); if (args['libraries'] == null || args['out'] == null) { var self = relativizeUri(Uri.base, Platform.script, isWindows); var librariesJson = relativizeUri(Uri.base, Platform.script.resolve('../../../sdk/lib/libraries.json'), isWindows); print('Usage: $self [other options]' ' --libraries --out '); print('For example:'); print('\$ $self --nnbd --libraries $librariesJson --out patched-sdk-dir'); exit(1); } var useNnbd = args['nnbd'] as bool; var target = args['target'] as String; var jsonUri = resolveInputUri(args['libraries'] as String); var libRoot = jsonUri.resolve('./'); var outPath = args['out'] as String; var outDir = resolveInputUri(outPath.endsWith('/') ? outPath : '$outPath/'); var outLibRoot = outDir.resolve('lib/'); var inputVersion = Uri.file(Platform.executable).resolve('../version'); var outVersion = outDir.resolve('version'); var specification = LibrariesSpecification.parse( jsonUri, File.fromUri(jsonUri).readAsStringSync()) .specificationFor(target); // Copy libraries.dart and version _writeSync(outVersion, File.fromUri(inputVersion).readAsStringSync()); // Enumerate sdk libraries and apply patches for (var library in specification.allLibraries) { var libraryFile = File.fromUri(library.uri); var libraryOut = outLibRoot.resolve(relativizeLibraryUri(libRoot, library.uri, useNnbd)); if (libraryFile.existsSync()) { var outUris = [libraryOut]; var libraryContents = libraryFile.readAsStringSync(); var contents = [libraryContents]; for (var part in _parseString(libraryContents, useNnbd: useNnbd).unit.directives) { if (part is PartDirective) { var partPath = part.uri.stringValue; outUris.add(libraryOut.resolve(partPath)); contents.add( File.fromUri(library.uri.resolve(partPath)).readAsStringSync()); } } if (args['merge-parts'] as bool && outUris.length > 1) { outUris.length = 1; contents = [ contents .join('\n') .replaceAll(RegExp('^part [^\n]*\$', multiLine: true), '') ]; } var buffer = StringBuffer(); for (var patchUri in library.patches) { // Note: VM targets enumerate more than one patch file, they are // currently written so that the first file is a valid patch file and // all other files can be appended at the end. buffer.write(File.fromUri(patchUri).readAsStringSync()); } var patchContents = '$buffer'; if (patchContents.isNotEmpty) { contents = _patchLibrary(contents, patchContents, useNnbd: useNnbd); } if (contents != null) { for (var i = 0; i < outUris.length; i++) { _writeSync(outUris[i], contents[i]); } } else { exitCode = 2; } } } var outLibrariesDart = outLibRoot.resolve('_internal/sdk_library_metadata/lib/libraries.dart'); _writeSync(outLibrariesDart, _generateLibrariesDart(libRoot, specification, useNnbd)); var experimentsPath = '_internal/allowed_experiments.json'; _writeSync( outLibRoot.resolve(experimentsPath), File.fromUri(libRoot.resolve(experimentsPath)).readAsStringSync(), ); } /// Writes a file, creating the directory if needed. void _writeSync(Uri fileUri, String contents) { var outDir = Directory.fromUri(fileUri.resolve('.')); if (!outDir.existsSync()) outDir.createSync(recursive: true); File.fromUri(fileUri).writeAsStringSync(contents); } final _parser = ArgParser() ..addFlag('nnbd', help: 'Whether to enable the nnbd feature.', defaultsTo: false) ..addFlag('merge-parts', help: 'Whether to merge part files. ' 'Technically this is not necessary, but dartanalyzer ' 'produces less warnings when enabling this flag.', defaultsTo: false) ..addOption('libraries', help: 'Path to a libraries.json specification file (required). ' 'All libary URIs within libraries.json are expected to be somewhere ' 'under the directory containing the libraries.json file. Reaching ' 'out above such directory is generally not supported. Today it is ' 'only allowed for the nnbd sdk to reuse libraries of the non-nnbd ' 'sdk, in which case the path starts with "../../sdk/lib/".') ..addOption('out', help: 'Path to an output folder (required).') ..addOption('target', help: 'The target tool. ' 'This name matches one of the possible targets in libraries.json ' 'and it is used to pick which patch files will be applied.', allowed: ['dartdevc', 'dart2js', 'dart2js_server', 'vm', 'flutter']); /// 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 _patchLibrary(List partsContents, String patchContents, {bool useNnbd = false}) { var results = []; // Parse the patch first. We'll need to extract bits of this as we go through // the other files. var patchFinder = PatchFinder.parseAndVisit(patchContents, useNnbd: useNnbd); // Merge `external` declarations with the corresponding `@patch` code. var failed = false; for (var partContent in partsContents) { var partEdits = StringEditBuffer(partContent); var partUnit = _parseString(partContent, useNnbd: useNnbd).unit; var patcher = PatchApplier(partEdits, patchFinder); partUnit.accept(patcher); if (!failed) failed = patcher.patchWasMissing; results.add(partEdits); } if (failed) return null; return List.from(results.map((e) => e.toString())); } /// Merge `@patch` declarations into `external` declarations. class PatchApplier extends GeneralizingAstVisitor { final StringEditBuffer edits; final PatchFinder patch; bool _isLibrary = true; // until proven otherwise. bool patchWasMissing = false; PatchApplier(this.edits, this.patch); @override void 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); 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; var importPos = unit.directives .lastWhere((d) => d is ImportDirective, orElse: () => libDir) .end; for (var d in patch.unit.directives.whereType()) { _merge(d, importPos); } var partPos = unit.directives.last.end; for (var d in patch.unit.directives.whereType()) { _merge(d, partPos); } // Merge declarations from the patch var declPos = edits.original.length; for (var d in patch.mergeDeclarations) { _merge(d, declPos); } } @override void visitPartOfDirective(PartOfDirective node) { _isLibrary = false; } @override void visitFunctionDeclaration(FunctionDeclaration node) { _maybePatch(node); } /// Merge patches and extensions into the class @override void 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(Declaration node) { if (node is FieldDeclaration) return; var externalKeyword = (node as dynamic).externalKeyword as Token; if (externalKeyword == null) return; var name = _qualifiedName(node); var patchNode = patch.patches[name]; if (patchNode == null) { // *.fromEnvironment are left unpatched by dart2js and are handled via // codegen. if (name != 'bool.fromEnvironment' && name != 'int.fromEnvironment' && name != 'String.fromEnvironment') { print('warning: patch not found for $name: $node'); // TODO(sigmund): delete this fail logic? Rather than emit an empty // file, it's more useful to emit a file with missing patches. // patchWasMissing = true; } return; } var patchMeta = patchNode.metadata.lastWhere(_isPatchAnnotation); var start = patchMeta.endToken.next.offset; var code = patch.contents.substring(start, patchNode.end); // Const factory constructors can't be legally parsed from the patch file, // so we need to omit the "const" there, but still preserve it. if (node is ConstructorDeclaration && node.constKeyword != null && patchNode is ConstructorDeclaration && patchNode.constKeyword == null) { code = 'const $code'; } // 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.end, code); } } class PatchFinder extends GeneralizingAstVisitor { final String contents; final CompilationUnit unit; final patches = {}; final mergeMembers = >{}; final mergeDeclarations = []; PatchFinder.parseAndVisit(String contents, {bool useNnbd}) : contents = contents, unit = _parseString(contents, useNnbd: useNnbd).unit { visitCompilationUnit(unit); } @override void visitCompilationUnitMember(CompilationUnitMember node) { mergeDeclarations.add(node); } @override void visitClassDeclaration(ClassDeclaration node) { if (_isPatch(node)) { var members = []; 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 void visitFunctionDeclaration(FunctionDeclaration node) { if (_isPatch(node)) { patches[_qualifiedName(node)] = node; } else { mergeDeclarations.add(node); } } @override void visitFunctionBody(node) {} // skip method bodies } String _qualifiedName(Declaration node) { var result = ''; var parent = node.parent; if (parent is ClassDeclaration) { result = '${parent.name.name}.'; } var name = (node as dynamic).name as SimpleIdentifier; if (name != null) result += name.name; // Make sure setters and getters don't collide. if (node is FunctionDeclaration && node.isSetter || node is MethodDeclaration && node.isSetter) { result += '='; } return result; } 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.isNotEmpty; /// 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(_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. @override String toString() { var sb = StringBuffer(); if (_edits.isEmpty) return original; // Sort edits by start location. _edits.sort(); var consumed = 0; for (var edit in _edits) { if (consumed > edit.begin) { sb = 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 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; @override String toString() => '(Edit @ $begin,$end: "$replace")'; @override int compareTo(_StringEdit other) { var diff = begin - other.begin; if (diff != 0) return diff; return end - other.end; } } ParseStringResult _parseString(String source, {bool useNnbd}) { var features = FeatureSet.fromEnableFlags2( sdkLanguageVersion: Version.parse('2.10.0'), flags: [if (useNnbd) 'non-nullable'], ); return parseString(content: source, featureSet: features); } /// Use the data from a libraries.json specification to generate the contents /// of `sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart`, which is /// needed by dartdevc-legacy and dartanalyzer. String _generateLibrariesDart( Uri libBaseUri, TargetLibrariesSpecification specification, bool usdNnbd) { var contents = StringBuffer(); contents.write(_LIBRARIES_DART_PREFIX); for (var library in specification.allLibraries) { var path = relativizeLibraryUri(libBaseUri, library.uri, usdNnbd); contents.write(' "${library.name}": \n' ' const LibraryInfo("$path",\n' ' categories: "Client,Server"),\n'); } contents.write(_LIBRARIES_DART_SUFFIX); return '$contents'; } String relativizeLibraryUri(Uri libRoot, Uri uri, bool useNnbd) { var relativePath = relativizeUri(libRoot, uri, isWindows); // During the nnbd-migration we may have paths that reach out into the // non-nnbd directory. if (relativePath.startsWith('..')) { if (!useNnbd || !relativePath.startsWith('../../sdk/lib/')) { print("error: can't handle libraries that live out of the sdk folder" ': $relativePath'); exit(1); } relativePath = relativePath.replaceFirst('../../sdk/lib/', ''); } return relativePath; } final _LIBRARIES_DART_PREFIX = r''' library libraries; const int DART2JS_PLATFORM = 1; const int VM_PLATFORM = 2; enum Category { client, server, embedded } Category parseCategory(String name) { switch (name) { case "Client": return Category.client; case "Server": return Category.server; case "Embedded": return Category.embedded; } return null; } const Map libraries = const { '''; final _LIBRARIES_DART_SUFFIX = r''' }; class LibraryInfo { final String path; final String _categories; final String dart2jsPath; final String dart2jsPatchPath; final bool documented; final int platforms; final bool implementation; final Maturity maturity; const LibraryInfo(this.path, {String categories: "", this.dart2jsPath, this.dart2jsPatchPath, this.implementation: false, this.documented: true, this.maturity: Maturity.UNSPECIFIED, this.platforms: DART2JS_PLATFORM | VM_PLATFORM}) : _categories = categories; bool get isDart2jsLibrary => (platforms & DART2JS_PLATFORM) != 0; bool get isVmLibrary => (platforms & VM_PLATFORM) != 0; List get categories { // `"".split(,)` returns [""] not [], so we handle that case separately. if (_categories == "") return const []; return _categories.split(",").map(parseCategory).toList(); } bool get isInternal => categories.isEmpty; String get categoriesString => _categories; } class Maturity { final int level; final String name; final String description; const Maturity(this.level, this.name, this.description); String toString() => "$name: $level\n$description\n"; static const Maturity DEPRECATED = const Maturity(0, "Deprecated", "This library will be remove before next major release."); static const Maturity EXPERIMENTAL = const Maturity( 1, "Experimental", "This library is experimental and will likely change or be removed\n" "in future versions."); static const Maturity UNSTABLE = const Maturity( 2, "Unstable", "This library is in still changing and have not yet endured\n" "sufficient real-world testing.\n" "Backwards-compatibility is NOT guaranteed."); static const Maturity WEB_STABLE = const Maturity( 3, "Web Stable", "This library is tracking the DOM evolution as defined by WC3.\n" "Backwards-compatibility is NOT guaranteed."); static const Maturity STABLE = const Maturity( 4, "Stable", "The library is stable. API backwards-compatibility is guaranteed.\n" "However implementation details might change."); static const Maturity LOCKED = const Maturity(5, "Locked", "This library will not change except when serious bugs are encountered."); static const Maturity UNSPECIFIED = const Maturity(-1, "Unspecified", "The maturity for this library has not been specified."); } ''';