This repository has been archived by the owner on Feb 22, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27
/
patch_sdk.dart
executable file
·401 lines (334 loc) · 13.3 KB
/
patch_sdk.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
#!/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.
library dev_compiler.tool.patch_sdk;
import 'dart:io';
import 'package:analyzer/analyzer.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:path/path.dart' as path;
void main(List<String> argv) {
if (argv.length < 2) {
var self = path.relative(path.fromUri(Platform.script));
var toolDir = path.relative(path.dirname(path.fromUri(Platform.script)));
var inputExample = path.join(toolDir, 'input_sdk');
var outExample = path.relative(
path.normalize(path.join(toolDir, '..', 'test', 'generated_sdk')));
print('Usage: $self INPUT_DIR OUTPUT_DIR');
print('For example:');
print('\$ $self $inputExample $outExample');
inputExample = path.join(toolDir, 'min_sdk');
outExample = path.join(toolDir, 'out', 'min_sdk');
print('\$ $self $inputExample $outExample');
exit(1);
}
var input = argv[0];
var sdkLibIn = path.join(input, 'lib');
var patchIn = path.join(input, 'patch');
var privateIn = path.join(input, 'private');
var sdkOut = path.join(argv[1], 'lib');
var INTERNAL_PATH = '_internal/compiler/js_lib/';
// Copy libraries.dart and version
var libContents = new File(path.join(sdkLibIn, '_internal', 'libraries.dart'))
.readAsStringSync();
_writeSync(path.join(sdkOut, '_internal', 'libraries.dart'), libContents);
_writeSync(
path.join(
sdkOut, '_internal', 'sdk_library_metadata', 'lib', 'libraries.dart'),
libContents);
_writeSync(path.join(sdkOut, '..', 'version'),
new File(path.join(sdkLibIn, '..', 'version')).readAsStringSync());
// Parse libraries.dart
var sdkLibraries = _getSdkLibraries(libContents);
// Enumerate core libraries and apply patches
for (SdkLibrary library in sdkLibraries) {
// TODO(jmesserly): analyzer does not handle the default case of
// "both platforms" correctly, and treats it as being supported on neither.
// So instead we skip explicitly marked as VM libs.
if (library.isVmLibrary) continue;
var libraryOut = path.join(sdkLibIn, library.path);
var libraryIn;
if (library.path.contains(INTERNAL_PATH)) {
libraryIn =
path.join(privateIn, library.path.replaceAll(INTERNAL_PATH, ''));
} else {
libraryIn = libraryOut;
}
var libraryFile = new File(libraryIn);
if (libraryFile.existsSync()) {
var contents = <String>[];
var paths = <String>[];
var libraryContents = libraryFile.readAsStringSync();
paths.add(libraryOut);
contents.add(libraryContents);
for (var part in parseDirectives(libraryContents).directives) {
if (part is PartDirective) {
var partPath = part.uri.stringValue;
paths.add(path.join(path.dirname(libraryOut), partPath));
contents.add(new File(path.join(path.dirname(libraryIn), partPath))
.readAsStringSync());
}
}
// See if we can find a patch file.
var patchPath = path.join(
patchIn, path.basenameWithoutExtension(libraryIn) + '_patch.dart');
if (new File(patchPath).existsSync()) {
var patchContents = new File(patchPath).readAsStringSync();
contents = _patchLibrary(contents, patchContents);
}
for (var i = 0; i < paths.length; i++) {
var outPath =
path.join(sdkOut, path.relative(paths[i], from: sdkLibIn));
_writeSync(outPath, 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(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.
var patchFinder = new PatchFinder.parseAndVisit(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);
}
return new List<String>.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.
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);
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;
if (externalKeyword == null) return;
var name = _qualifiedName(node);
var patchNode = patch.patches[name];
if (patchNode == null) {
print('warning: patch not found for $name: $node');
return;
}
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.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>[];
PatchFinder.parseAndVisit(String contents)
: contents = contents,
unit = parseCompilationUnit(contents) {
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;
return className + (name != null ? name.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(true);
parseCompilationUnit(contents).accept(libraryBuilder);
return libraryBuilder.librariesMap.sdkLibraries;
}