Files
sdk/pkg/dev_compiler/tool/patch_sdk.dart
T
Nicholas Shahan 2d5265f59d [dartdevc] Apply always_declare_return_types lint and cleanup violations
In most cases I provided the type that appeared in the return
statements. For a few test files I explicitly typed the return types as
dynamic because it caused diffs in the expected break point locations
when adding more explicit types and it wasn't totally clear what the
new locations should be.

Change-Id: Ifdd80edcccc5fc4a2083ecdbb0e5fbf1c4453e01
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/133339
Reviewed-by: Nate Bosch <nbosch@google.com>
Commit-Queue: Nicholas Shahan <nshahan@google.com>
2020-01-29 07:17:22 +00:00

608 lines
20 KiB
Dart
Executable File

#!/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 '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';
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 = Platform.script.resolve('../../../tools/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));
}
/// 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.
bool 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;
int importPos = unit.directives
.lastWhere((d) => d is ImportDirective, orElse: () => libDir)
.end;
for (var d in patch.unit.directives.whereType<ImportDirective>()) {
_merge(d, importPos);
}
int partPos = unit.directives.last.end;
for (var d in patch.unit.directives.whereType<PartDirective>()) {
_merge(d, partPos);
}
// Merge declarations from the patch
int 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;
}
Annotation patchMeta = patchNode.metadata.lastWhere(_isPatchAnnotation);
int 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();
int 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) {
int diff = begin - other.begin;
if (diff != 0) return diff;
return end - other.end;
}
}
ParseStringResult _parseString(String source, {bool useNnbd}) {
var features = FeatureSet.fromEnableFlags([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) {
String 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.");
}
''';