| #!/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<String> 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 <libraries.json> --out <output-dir>'); |
| 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 = <Uri>[libraryOut]; |
| var libraryContents = libraryFile.readAsStringSync(); |
| var contents = <String>[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<String> _patchLibrary(List<String> partsContents, String patchContents, |
| {bool useNnbd = false}) { |
| var results = <StringEditBuffer>[]; |
| |
| // 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<String>.from(results.map((e) => e.toString())); |
| } |
| |
| /// Merge `@patch` declarations into `external` declarations. |
| class PatchApplier extends GeneralizingAstVisitor<void> { |
| 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<ImportDirective>()) { |
| _merge(d, importPos); |
| } |
| |
| var partPos = unit.directives.last.end; |
| for (var d in patch.unit.directives.whereType<PartDirective>()) { |
| _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<void> { |
| final String contents; |
| final CompilationUnit unit; |
| |
| final patches = <String, Declaration>{}; |
| final mergeMembers = <String, List<ClassMember>>{}; |
| final mergeDeclarations = <CompilationUnitMember>[]; |
| |
| 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 = <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 |
| 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<String, LibraryInfo> 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<Category> get categories { |
| // `"".split(,)` returns [""] not [], so we handle that case separately. |
| if (_categories == "") return const <Category>[]; |
| 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."); |
| } |
| '''; |