Chromium Code Reviews

Side by Side Diff: initialize/lib/transformer.dart

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff |
« no previous file with comments | « initialize/lib/src/static_loader.dart ('k') | initialize/pubspec.yaml » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4 library initialize.transformer;
5
6 import 'dart:async';
7 import 'dart:collection' show Queue;
8 import 'package:analyzer/src/generated/ast.dart';
9 import 'package:analyzer/src/generated/element.dart';
10 import 'package:barback/barback.dart';
11 import 'package:code_transformers/assets.dart';
12 import 'package:code_transformers/resolver.dart';
13 import 'package:code_transformers/src/dart_sdk.dart' as dart_sdk;
14 import 'package:dart_style/dart_style.dart';
15 import 'package:glob/glob.dart';
16 import 'package:html/dom.dart' as dom;
17 import 'package:html/parser.dart' show parse;
18 import 'package:path/path.dart' as path;
19
20 import 'build/initializer_plugin.dart';
21 export 'build/initializer_plugin.dart';
22
23 /// Create a new [Asset] which inlines your [Initializer] annotations into
24 /// a new file that bootstraps your application.
25 Asset generateBootstrapFile(Resolver resolver, Transform transform,
26 AssetId primaryAssetId, AssetId newEntryPointId,
27 {bool errorIfNotFound: true, List<InitializerPlugin> plugins,
28 bool appendDefaultPlugin: true}) {
29 if (appendDefaultPlugin) {
30 if (plugins == null) plugins = [];
31 plugins.add(const DefaultInitializerPlugin());
32 }
33 return new _BootstrapFileBuilder(
34 resolver, transform, primaryAssetId, newEntryPointId, errorIfNotFound,
35 plugins: plugins).run();
36 }
37
38 /// Transformer which removes the mirror-based initialization logic and replaces
39 /// it with static logic.
40 class InitializeTransformer extends Transformer {
41 final Resolvers _resolvers;
42 final Iterable<Glob> _entryPointGlobs;
43 final bool _errorIfNotFound;
44 final List<InitializerPlugin> plugins;
45
46 InitializeTransformer(List<String> entryPoints,
47 {bool errorIfNotFound: true, this.plugins})
48 : _entryPointGlobs = entryPoints.map((e) => new Glob(e)),
49 _errorIfNotFound = errorIfNotFound,
50 _resolvers = new Resolvers.fromMock(dart_sdk.mockSdkSources);
51
52 factory InitializeTransformer.asPlugin(BarbackSettings settings) =>
53 new InitializeTransformer(_readFileList(settings, 'entry_points'));
54
55 bool isPrimary(AssetId id) => _entryPointGlobs.any((g) => g.matches(id.path));
56
57 Future apply(Transform transform) {
58 if (transform.primaryInput.id.path.endsWith('.dart')) {
59 return _buildBootstrapFile(transform);
60 } else if (transform.primaryInput.id.path.endsWith('.html')) {
61 return transform.primaryInput.readAsString().then((html) {
62 var document = parse(html);
63 var originalDartFile =
64 _findMainScript(document, transform.primaryInput.id, transform);
65 return _buildBootstrapFile(transform, primaryId: originalDartFile)
66 .then((AssetId newDartFile) {
67 return _replaceEntryWithBootstrap(transform, document,
68 transform.primaryInput.id, originalDartFile, newDartFile);
69 });
70 });
71 } else {
72 transform.logger.warning(
73 'Invalid entry point ${transform.primaryInput.id}. Must be either a '
74 '.dart or .html file.');
75 }
76 return new Future.value();
77 }
78
79 // Returns the AssetId of the newly created bootstrap file.
80 Future<AssetId> _buildBootstrapFile(Transform transform,
81 {AssetId primaryId}) {
82 if (primaryId == null) primaryId = transform.primaryInput.id;
83 var newEntryPointId = new AssetId(primaryId.package,
84 '${path.url.withoutExtension(primaryId.path)}.initialize.dart');
85 return transform.hasInput(newEntryPointId).then((exists) {
86 if (exists) {
87 transform.logger
88 .error('New entry point file $newEntryPointId already exists.');
89 return null;
90 }
91
92 return _resolvers.get(transform, [primaryId]).then((resolver) {
93 transform.addOutput(generateBootstrapFile(
94 resolver, transform, primaryId, newEntryPointId,
95 errorIfNotFound: _errorIfNotFound, plugins: plugins));
96 resolver.release();
97 return newEntryPointId;
98 });
99 });
100 }
101 // Finds the first (and only) dart script on an html page and returns the
102 // [AssetId] that points to it
103 AssetId _findMainScript(
104 dom.Document document, AssetId entryPoint, Transform transform) {
105 var scripts = document.querySelectorAll('script[type="application/dart"]');
106 if (scripts.length != 1) {
107 transform.logger.error('Expected exactly one dart script in $entryPoint '
108 'but found ${scripts.length}.');
109 return null;
110 }
111
112 var src = scripts[0].attributes['src'];
113 if (src == null) {
114 // TODO(jakemac): Support inline scripts,
115 transform.logger.error('Inline scripts are not supported at this time, '
116 'see https://github.com/dart-lang/initialize/issues/20.');
117 return null;
118 }
119
120 return uriToAssetId(
121 entryPoint, src, transform.logger, scripts[0].sourceSpan);
122 }
123
124 // Replaces script tags pointing to [originalDartFile] with [newDartFile] in
125 // [entryPoint].
126 void _replaceEntryWithBootstrap(Transform transform, dom.Document document,
127 AssetId entryPoint, AssetId originalDartFile, AssetId newDartFile) {
128 var found = false;
129
130 var scripts = document
131 .querySelectorAll('script[type="application/dart"]')
132 .where((script) {
133 var assetId = uriToAssetId(entryPoint, script.attributes['src'],
134 transform.logger, script.sourceSpan);
135 return assetId == originalDartFile;
136 }).toList();
137
138 if (scripts.length != 1) {
139 transform.logger.error(
140 'Expected exactly one script pointing to $originalDartFile in '
141 '$entryPoint, but found ${scripts.length}.');
142 return;
143 }
144 scripts[0].attributes['src'] = path.url.relative(newDartFile.path,
145 from: path.dirname(entryPoint.path));
146 transform.addOutput(new Asset.fromString(entryPoint, document.outerHtml));
147 }
148 }
149
150 // Class which builds a bootstrap file.
151 class _BootstrapFileBuilder {
152 final Resolver _resolver;
153 final Transform _transform;
154 final bool _errorIfNotFound;
155 AssetId _entryPoint;
156 AssetId _newEntryPoint;
157
158 /// The resolved initialize library.
159 LibraryElement _initializeLibrary;
160 /// The resolved Initializer class from the initialize library.
161 ClassElement _initializer;
162
163 /// Queue for intialization annotations.
164 final _initQueue = new Queue<InitializerData>();
165 /// All the annotations we have seen for each element
166 final _seenAnnotations = new Map<Element, Set<ElementAnnotation>>();
167
168 /// The list of [InitializerPlugin]s to apply. The first plugin which asks to
169 /// be applied to a given initializer is the only one that will apply.
170 List<InitializerPlugin> _plugins;
171
172 TransformLogger _logger;
173
174 _BootstrapFileBuilder(this._resolver, this._transform, this._entryPoint,
175 this._newEntryPoint, this._errorIfNotFound,
176 {List<InitializerPlugin> plugins}) {
177 _logger = _transform.logger;
178 _initializeLibrary =
179 _resolver.getLibrary(new AssetId('initialize', 'lib/initialize.dart'));
180 if (_initializeLibrary != null) {
181 _initializer = _initializeLibrary.getType('Initializer');
182 } else if (_errorIfNotFound) {
183 _logger.warning('Unable to read "package:initialize/initialize.dart". '
184 'This file must be imported via $_entryPoint or a transitive '
185 'dependency.');
186 }
187 _plugins = plugins != null ? plugins : [const DefaultInitializerPlugin()];
188 }
189
190 /// Creates and returns the new bootstrap file.
191 Asset run() {
192 var entryLib = _resolver.getLibrary(_entryPoint);
193 _readLibraries(entryLib);
194
195 return new Asset.fromString(_newEntryPoint, _buildNewEntryPoint(entryLib));
196 }
197
198 /// Reads Initializer annotations on this library and all its dependencies in
199 /// post-order.
200 void _readLibraries(LibraryElement library, [Set<LibraryElement> seen]) {
201 if (seen == null) seen = new Set<LibraryElement>();
202 seen.add(library);
203
204 // Visit all our dependencies.
205 for (var library in _sortedLibraryDependencies(library)) {
206 // Don't include anything from the sdk.
207 if (library.isInSdk) continue;
208 if (seen.contains(library)) continue;
209 _readLibraries(library, seen);
210 }
211
212 // Read annotations in this order: library, top level methods, classes.
213 _readAnnotations(library);
214 for (var method in _topLevelMethodsOfLibrary(library, seen)) {
215 _readAnnotations(method);
216 }
217 for (var clazz in _classesOfLibrary(library, seen)) {
218 readSuperClassAnnotations(InterfaceType superClass) {
219 if (superClass == null) return;
220 readSuperClassAnnotations(superClass.superclass);
221 if (_readAnnotations(superClass.element) &&
222 superClass.element.library != clazz.library) {
223 _logger.warning(
224 'We have detected a cycle in your import graph when running '
225 'initializers on ${clazz.name}. This means the super class '
226 '${superClass.name} has a dependency on this library '
227 '(possibly transitive).');
228 }
229 }
230 readSuperClassAnnotations(clazz.supertype);
231 _readAnnotations(clazz);
232 }
233 }
234
235 bool _readAnnotations(Element element) {
236 var found = false;
237 if (element.metadata.isEmpty) return found;
238
239 var metaNodes;
240 var node = element.node;
241 if (node is SimpleIdentifier && node.parent is LibraryIdentifier) {
242 metaNodes = node.parent.parent.metadata;
243 } else if (node is ClassDeclaration || node is FunctionDeclaration) {
244 metaNodes = node.metadata;
245 } else {
246 return found;
247 }
248
249 metaNodes.where((Annotation metaNode) {
250 // First filter out anything that is not a Initializer.
251 var meta = metaNode.elementAnnotation;
252 var e = meta.element;
253 if (e is PropertyAccessorElement) {
254 return _isInitializer(e.variable.evaluationResult.value.type);
255 } else if (e is ConstructorElement) {
256 return _isInitializer(e.returnType);
257 }
258 return false;
259 }).where((Annotation metaNode) {
260 var meta = metaNode.elementAnnotation;
261 _seenAnnotations.putIfAbsent(element, () => new Set<ElementAnnotation>());
262 return !_seenAnnotations[element].contains(meta);
263 }).forEach((Annotation metaNode) {
264 var meta = metaNode.elementAnnotation;
265 _seenAnnotations[element].add(meta);
266 _initQueue.addLast(new InitializerData._(node, metaNode));
267 found = true;
268 });
269 return found;
270 }
271
272 String _buildNewEntryPoint(LibraryElement entryLib) {
273 var importsBuffer = new StringBuffer();
274 var initializersBuffer = new StringBuffer();
275 var libraryPrefixes = new Map<LibraryElement, String>();
276
277 // Import the static_loader, initializer, and original entry point.
278 importsBuffer
279 .writeln("import 'package:initialize/src/static_loader.dart';");
280 importsBuffer.writeln("import 'package:initialize/initialize.dart';");
281 libraryPrefixes[entryLib] = 'i0';
282
283 initializersBuffer.writeln('initializers.addAll([');
284 while (_initQueue.isNotEmpty) {
285 var next = _initQueue.removeFirst();
286
287 libraryPrefixes.putIfAbsent(
288 next.targetElement.library, () => 'i${libraryPrefixes.length}');
289 libraryPrefixes.putIfAbsent(next.annotationElement.element.library,
290 () => 'i${libraryPrefixes.length}');
291
292 // Run the first plugin which asks to be ran and then stop.
293 var data = new InitializerPluginData(
294 next, _newEntryPoint, libraryPrefixes, _resolver, _logger);
295 var plugin = _plugins.firstWhere((p) => p.shouldApply(data), orElse: () {
296 _logger.error('No InitializerPlugin handled the annotation: '
297 '${next.annotationElement} on: ${next.targetElement}.');
298 });
299 if (plugin == null) continue;
300
301 var text = plugin.apply(data);
302 if (text != null) initializersBuffer.writeln('$text,');
303 }
304 initializersBuffer.writeln(']);');
305
306 libraryPrefixes
307 .forEach((lib, prefix) => _writeImport(lib, prefix, importsBuffer));
308
309 // TODO(jakemac): copyright and library declaration
310 return new DartFormatter().format('''
311 $importsBuffer
312 main() {
313 $initializersBuffer
314 return i0.main();
315 }
316 ''');
317 }
318
319 _writeImport(LibraryElement lib, String prefix, StringBuffer buffer) {
320 AssetId id = (lib.source as dynamic).assetId;
321
322 if (id.path.startsWith('lib/')) {
323 var packagePath = id.path.replaceFirst('lib/', '');
324 buffer.write("import 'package:${id.package}/${packagePath}'");
325 } else if (id.package != _newEntryPoint.package) {
326 _logger.error("Can't import `${id}` from `${_newEntryPoint}`");
327 } else if (path.url.split(id.path)[0] ==
328 path.url.split(_newEntryPoint.path)[0]) {
329 var relativePath = path.url.relative(id.path,
330 from: path.url.dirname(_newEntryPoint.path));
331 buffer.write("import '${relativePath}'");
332 } else {
333 _logger.error("Can't import `${id}` from `${_newEntryPoint}`");
334 }
335 buffer.writeln(' as $prefix;');
336 }
337
338 bool _isInitializer(InterfaceType type) {
339 // If `_initializer` wasn't found then it was never loaded (even
340 // transitively), and so no annotations can be initializers.
341 if (_initializer == null) return false;
342 if (type == null) return false;
343 if (type.element.type == _initializer.type) return true;
344 if (_isInitializer(type.superclass)) return true;
345 for (var interface in type.interfaces) {
346 if (_isInitializer(interface)) return true;
347 }
348 return false;
349 }
350
351 /// Retrieves all top-level methods that are visible if you were to import
352 /// [lib]. This includes exported methods from other libraries too.
353 List<FunctionElement> _topLevelMethodsOfLibrary(
354 LibraryElement library, Set<LibraryElement> seen) {
355 var methods = [];
356
357 var orderedExports = new List.from(library.exports)
358 ..sort((a, b) => a.uriOffset.compareTo(b.uriOffset));
359 for (var export in orderedExports) {
360 if (seen.contains(export.exportedLibrary)) continue;
361 methods.addAll(_topLevelMethodsOfLibrary(export.exportedLibrary, seen));
362 }
363
364 for (CompilationUnitElement unit in _orderedUnits(library)) {
365 methods.addAll(new List.from(unit.functions)
366 ..sort((a, b) => a.nameOffset.compareTo(b.nameOffset)));
367 }
368
369 return methods;
370 }
371
372 /// Retrieves all classes that are visible if you were to import [lib]. This
373 /// includes exported classes from other libraries.
374 List<ClassElement> _classesOfLibrary(
375 LibraryElement library, Set<LibraryElement> seen) {
376 var classes = [];
377
378 var orderedExports = new List.from(library.exports)
379 ..sort((a, b) => a.uriOffset.compareTo(b.uriOffset));
380 for (var export in orderedExports) {
381 if (seen.contains(export.exportedLibrary)) continue;
382 classes.addAll(_classesOfLibrary(export.exportedLibrary, seen));
383 }
384
385 for (var unit in _orderedUnits(library)) {
386 classes.addAll(new List.from(unit.types)
387 ..sort((a, b) => a.nameOffset.compareTo(b.nameOffset)));
388 }
389
390 return classes;
391 }
392
393 List<CompilationUnitElement> _orderedUnits(LibraryElement library) {
394 var definingUnit = library.definingCompilationUnit;
395 // The first item is the source library, remove it for now.
396 return new List.from(library.units)
397 ..sort((a, b) {
398 if (a == definingUnit) return 1;
399 if (b == definingUnit) return -1;
400 return a.uri.compareTo(b.uri);
401 });
402 }
403
404 Iterable<LibraryElement> _sortedLibraryDependencies(LibraryElement library) {
405 // TODO(jakemac): Investigate supporting annotations on part-of directives.
406 getLibrary(UriReferencedElement element) {
407 if (element is ImportElement) return element.importedLibrary;
408 if (element is ExportElement) return element.exportedLibrary;
409 }
410
411 return (new List.from(library.imports)
412 ..addAll(library.exports)
413 ..sort((a, b) => a.nameOffset.compareTo(b.nameOffset))).map(getLibrary);
414 }
415 }
416
417 /// An [Initializer] annotation and the target of that annotation.
418 class InitializerData {
419 /// The target [AstNode] of the annotation.
420 final AstNode targetNode;
421
422 /// The [Annotation] representing the annotation itself.
423 final Annotation annotationNode;
424
425 /// The [ElementAnnotation] representing the annotation itself.
426 ElementAnnotation get annotationElement => annotationNode.elementAnnotation;
427
428 /// The target [Element] of the annotation.
429 Element get targetElement {
430 if (targetNode is SimpleIdentifier &&
431 targetNode.parent is LibraryIdentifier) {
432 return (targetNode.parent.parent as LibraryDirective).element;
433 } else if (targetNode is ClassDeclaration ||
434 targetNode is FunctionDeclaration) {
435 return (targetNode as dynamic).element;
436 } else {
437 return null;
438 }
439 }
440
441 InitializerData._(this.targetNode, this.annotationNode);
442 }
443
444 // Reads a file list from a barback settings configuration field.
445 _readFileList(BarbackSettings settings, String field) {
446 var value = settings.configuration[field];
447 if (value == null) return null;
448 var files = [];
449 bool error;
450 if (value is List) {
451 files = value;
452 error = value.any((e) => e is! String);
453 } else if (value is String) {
454 files = [value];
455 error = false;
456 } else {
457 error = true;
458 }
459 if (error) {
460 print('Bad value for "$field" in the initialize transformer. '
461 'Expected either one String or a list of Strings.');
462 }
463 return files;
464 }
OLDNEW
« no previous file with comments | « initialize/lib/src/static_loader.dart ('k') | initialize/pubspec.yaml » ('j') | no next file with comments »

Powered by Google App Engine