Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1284)

Side by Side Diff: pkg/polymer/lib/src/build/script_compactor.dart

Issue 211393006: Enables codegen support in polymer (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 /// Transfomer that combines multiple dart script tags into a single one. 5 /// Transfomer that combines multiple dart script tags into a single one.
6 library polymer.src.build.script_compactor; 6 library polymer.src.build.script_compactor;
7 7
8 import 'dart:async'; 8 import 'dart:async';
9 import 'dart:convert'; 9 import 'dart:convert';
10 10
11 import 'package:html5lib/dom.dart' show Document, Element; 11 import 'package:html5lib/dom.dart' show Document, Element, Text;
12 import 'package:html5lib/dom_parsing.dart';
12 import 'package:analyzer/src/generated/ast.dart'; 13 import 'package:analyzer/src/generated/ast.dart';
14 import 'package:analyzer/src/generated/element.dart' hide Element;
15 import 'package:analyzer/src/generated/element.dart' as analyzer show Element;
13 import 'package:barback/barback.dart'; 16 import 'package:barback/barback.dart';
14 import 'package:code_transformers/assets.dart'; 17 import 'package:code_transformers/assets.dart';
15 import 'package:path/path.dart' as path; 18 import 'package:path/path.dart' as path;
16 import 'package:source_maps/span.dart' show SourceFile; 19 import 'package:source_maps/span.dart' show SourceFile;
20 import 'package:smoke/codegen/generator.dart';
21 import 'package:smoke/codegen/recorder.dart';
22 import 'package:code_transformers/resolver.dart';
23 import 'package:code_transformers/src/dart_sdk.dart';
24
25 import 'package:polymer_expressions/expression.dart' as pe;
26 import 'package:polymer_expressions/parser.dart' as pe;
27 import 'package:polymer_expressions/visitor.dart' as pe;
17 28
18 import 'import_inliner.dart' show ImportInliner; // just for docs. 29 import 'import_inliner.dart' show ImportInliner; // just for docs.
19 import 'common.dart'; 30 import 'common.dart';
20 31
21 /// Combines Dart script tags into a single script tag, and creates a new Dart 32 /// Combines Dart script tags into a single script tag, and creates a new Dart
22 /// file that calls the main function of each of the original script tags. 33 /// file that calls the main function of each of the original script tags.
23 /// 34 ///
24 /// This transformer assumes that all script tags point to external files. To 35 /// This transformer assumes that all script tags point to external files. To
25 /// support script tags with inlined code, use this transformer after running 36 /// support script tags with inlined code, use this transformer after running
26 /// [ImportInliner] on an earlier phase. 37 /// [ImportInliner] on an earlier phase.
27 /// 38 ///
28 /// Internally, this transformer will convert each script tag into an import 39 /// Internally, this transformer will convert each script tag into an import
29 /// statement to a library, and then uses `initPolymer` (see polymer.dart) to 40 /// statement to a library, and then uses `initPolymer` (see polymer.dart) to
30 /// process `@initMethod` and `@CustomTag` annotations in those libraries. 41 /// process `@initMethod` and `@CustomTag` annotations in those libraries.
31 class ScriptCompactor extends Transformer { 42 class ScriptCompactor extends Transformer {
43 final Resolvers resolvers;
Jennifer Messerly 2014/03/27 02:20:32 this is an issue with the code_transformer pkg, bu
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 Yeah, that's a good point. Just chatted with Pete
32 final TransformOptions options; 44 final TransformOptions options;
33 45
34 ScriptCompactor(this.options); 46 ScriptCompactor(this.options, {String sdkDir})
47 : resolvers = new Resolvers(sdkDir != null ? sdkDir : dartSdkDirectory);
35 48
36 /// Only run on entry point .html files. 49 /// Only run on entry point .html files.
37 Future<bool> isPrimary(Asset input) => 50 Future<bool> isPrimary(Asset input) =>
38 new Future.value(options.isHtmlEntryPoint(input.id)); 51 new Future.value(options.isHtmlEntryPoint(input.id));
39 52
40 Future apply(Transform transform) => 53 Future apply(Transform transform) =>
41 new _ScriptCompactor(transform, options).apply(); 54 new _ScriptCompactor(transform, options, resolvers).apply();
42 } 55 }
43 56
44 /// Helper class mainly use to flatten the async code. 57 /// Helper class mainly use to flatten the async code.
45 class _ScriptCompactor extends PolymerTransformer { 58 class _ScriptCompactor extends PolymerTransformer {
46 final TransformOptions options; 59 final TransformOptions options;
47 final Transform transform; 60 final Transform transform;
48 final TransformLogger logger; 61 final TransformLogger logger;
49 final AssetId docId; 62 final AssetId docId;
50 final AssetId bootstrapId; 63 final AssetId bootstrapId;
51 64
65 /// HTML document parsed from [docId].
52 Document document; 66 Document document;
67
68 /// List of ids for each Dart entry script tag (the main tag and any tag
69 /// included on each custom element definition).
53 List<AssetId> entryLibraries; 70 List<AssetId> entryLibraries;
71
72 /// The id of the main Dart program.
54 AssetId mainLibraryId; 73 AssetId mainLibraryId;
74
75 /// Script tag that loads the Dart entry point.
55 Element mainScriptTag; 76 Element mainScriptTag;
56 final Map<AssetId, List<_Initializer>> initializers = {};
57 77
58 _ScriptCompactor(Transform transform, this.options) 78 /// Initializers that will register custom tags or invoke `initMethod`s.
79 final List<_Initializer> initializers = [];
80
81 /// Attributes published on a custom-tag. We make these available via
82 /// reflection even if @published was not used.
83 final Map<String, List<String>> publishedAttributes = {};
84
85 /// Those custom-tags for which we have found a corresponding class in code.
86 final Set<String> tagsWithClasses = new Set();
87
88 /// Hook needed to access the analyzer within barback transformers.
89 final Resolvers resolvers;
90
91 /// The resolver instance associated with a single run of this transformer.
92 Resolver resolver;
93
94 /// Element representing `HtmlElement`.
95 ClassElement _htmlElementElement;
Jennifer Messerly 2014/03/27 02:20:32 A thought here: there's a lot of state in this cla
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 good idea. I moved these out in to a separate clas
96
97 /// Element representing the constructor of `@CustomTag`.
98 ConstructorElement _customTagConstructor;
99
100 /// Element representing the type of `@published`.
101 ClassElement _publishedElement;
102
103 /// Element representing the type of `@observable`.
104 ClassElement _observableElement;
105
106 /// Element representing the type of `@ObserveProperty`.
107 ClassElement _observePropertyElement;
108
109 /// Element representing the `@initMethod` annotation.
110 TopLevelVariableElement _initMethodElement;
111
112 /// Code generator used to create the static initialization for smoke.
113 final SmokeCodeGenerator generator = new SmokeCodeGenerator();
Jennifer Messerly 2014/03/27 02:20:32 IMO, it's nice to not repeat the type annotation f
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 Done. It seems fine in this particular example, b
114
115 /// Recorder that uses analyzer data to feed data to [generator].
116 Recorder recorder;
117
118 _ScriptCompactor(Transform transform, this.options, this.resolvers)
59 : transform = transform, 119 : transform = transform,
60 logger = transform.logger, 120 logger = transform.logger,
61 docId = transform.primaryInput.id, 121 docId = transform.primaryInput.id,
62 bootstrapId = transform.primaryInput.id.addExtension('_bootstrap.dart'); 122 bootstrapId = transform.primaryInput.id.addExtension('_bootstrap.dart');
63 123
64 Future apply() => 124 Future apply() =>
65 _loadDocument() 125 _loadDocument()
66 .then(_loadEntryLibraries) 126 .then(_loadEntryLibraries)
67 .then(_processHtml) 127 .then(_processHtml)
68 .then(_emitNewEntrypoint); 128 .then(_emitNewEntrypoint);
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
113 Future _emitNewEntrypoint(_) { 173 Future _emitNewEntrypoint(_) {
114 if (mainScriptTag == null) { 174 if (mainScriptTag == null) {
115 // We didn't find any main library, nothing to do. 175 // We didn't find any main library, nothing to do.
116 transform.addOutput(transform.primaryInput); 176 transform.addOutput(transform.primaryInput);
117 return null; 177 return null;
118 } 178 }
119 179
120 // Emit the bootstrap .dart file 180 // Emit the bootstrap .dart file
121 mainScriptTag.attributes['src'] = path.url.basename(bootstrapId.path); 181 mainScriptTag.attributes['src'] = path.url.basename(bootstrapId.path);
122 entryLibraries.add(mainLibraryId); 182 entryLibraries.add(mainLibraryId);
123 return _computeInitializers().then(_createBootstrapCode).then((code) { 183
124 transform.addOutput(new Asset.fromString(bootstrapId, code)); 184 return _initResolver()
125 transform.addOutput(new Asset.fromString(docId, document.outerHtml)); 185 .then(_extractUsesOfMirrors)
186 .then(_emitFiles)
187 .then((_) => resolver.release());
188 }
189
190 /// Load a resolver that computes information for every library in
191 /// [entryLibraries], then use it to initialize the [recorder] (for import
192 /// resolution) and to resolve specific elements (for analyzing the user's
193 /// code).
194 Future _initResolver() => resolvers.get(transform, entryLibraries).then((r) {
195 resolver = r;
196 recorder = new Recorder(generator,
197 (lib) => resolver.getImportUri(lib, from: bootstrapId).toString());
198
199 // Load class elements that are used in queries for codegen.
200 var polymerLib = r.getLibrary(new AssetId('polymer', 'lib/polymer.dart'));
Jennifer Messerly 2014/03/27 02:20:32 just curious, any reason to prefer AssetIds over p
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 code_transformers indexes the libraries that are r
201 if (polymerLib == null) _definitionError('the polymer library');
202 var htmlLib = r.getLibraryByUri(Uri.parse('dart:html'));
203 if (htmlLib == null) _definitionError('the "dart:html" library');
204 var observeLib = r.getLibrary(
205 new AssetId('observe', 'lib/src/metadata.dart'));
206 if (observeLib == null) _definitionError('the observe library');
207
208 for (var unit in polymerLib.parts) {
209 if (unit.uri == 'src/loader.dart') {
210 _initMethodElement = unit.topLevelVariables.firstWhere(
211 (t) => t.displayName == 'initMethod');
212 break;
213 }
214 }
215 _customTagConstructor =
216 _lookupType(polymerLib, 'CustomTag').constructors.first;
217 _publishedElement = _lookupType(polymerLib, 'PublishedProperty');
218 _observableElement = _lookupType(observeLib, 'ObservableProperty');
219 _observePropertyElement = _lookupType(polymerLib, 'ObserveProperty');
220 _htmlElementElement = _lookupType(htmlLib, 'HtmlElement');
221 if (_initMethodElement == null) _definitionError('@initMethod');
222 });
223
224 _lookupType(LibraryElement lib, String typeName) {
225 var result = lib.getType(typeName);
226 if (result == null) _definitionError(typeName);
227 return result;
228 }
229
230 _definitionError(name) {
231 throw new StateError("Internal error in polymer-builder: couldn't find "
Jennifer Messerly 2014/03/27 02:20:32 could this error be cased by the user forgetting t
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 It was happening in our tests before I added the m
232 "definition of $name.");
233 }
234
235 /// Inspects the entire program to find out anything that polymer accesses
236 /// using mirrors and produces static information that can be used to replace
237 /// the mirror-based loader and the uses of mirrors through the `smoke`
238 /// package. This includes:
239 ///
240 /// * visiting entry-libraries to extract initializers,
241 /// * visiting polymer-expressions to extract getters and setters,
242 /// * looking for published fields of custom elements, and
243 /// * looking for event handlers and callbacks of change notifications.
244 ///
245 void _extractUsesOfMirrors(_) {
Jennifer Messerly 2014/03/27 02:20:32 Just wanted to say this method is awesome. It read
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 :)
246 // Generate getters and setters needed to evaluate polymer expressions, and
247 // extract information about published attributes.
248 new _HtmlExtractor(generator, publishedAttributes).visit(document);
249
250 // Process all classes and top-level functions to include initializers,
251 // register custom elements, and include special fields and methods in
252 // custom element classes.
253 for (var id in entryLibraries) {
254 var lib = resolver.getLibrary(id);
255 for (var cls in _visibleClassesOf(lib)) {
256 _processClass(cls, id);
257 }
258
259 for (var fun in _visibleTopLevelMethodsOf(lib)) {
260 _processFunction(fun, id);
261 }
262 }
263
264 // Warn about tagNames with no corresponding Dart class
265 // TODO(sigmund): is there a way to exclude polymer.js elements? should we
266 // remove the warning below?
Jennifer Messerly 2014/03/27 02:20:32 yes, please, let's remove :) (even for Dart, thin
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 Done.
267 publishedAttributes.forEach((tagName, attrs) {
268 if (tagsWithClasses.contains(tagName)) return;
269 logger.warning('Class for custom-element "$tagName" not found. '
270 'Code-generation might be incomplete.');
271 // We include accessors for the missing attributes because they help get
272 // better warning messages at runtime.
273 for (var attr in attrs) {
274 generator.addGetter(attr);
275 generator.addSetter(attr);
276 generator.addSymbol(attr);
277 }
126 }); 278 });
127 } 279 }
128 280
129 /// Emits the actual bootstrap code. 281 /// Retrieves all classses that are visible if you were to import [lib]. This
130 String _createBootstrapCode(_) { 282 /// includes exported classes from other libraries.
283 List<ClassElement> _visibleClassesOf(LibraryElement lib) {
284 var result = [];
285 result.addAll(lib.units.expand((u) => u.types));
286 for (var e in lib.exports) {
287 var exported = e.exportedLibrary.units.expand((u) => u.types).toList();
288 _filter(exported, e.combinators);
289 result.addAll(exported);
290 }
291 return result;
292 }
293
294 /// Retrieves all top-level methods that are visible if you were to import
295 /// [lib]. This includes exported methods from other libraries too.
296 List<ClassElement> _visibleTopLevelMethodsOf(LibraryElement lib) {
297 var result = [];
298 result.addAll(lib.units.expand((u) => u.functions));
299 for (var e in lib.exports) {
300 var exported = e.exportedLibrary.units
301 .expand((u) => u.functions).toList();
302 _filter(exported, e.combinators);
303 result.addAll(exported);
304 }
305 return result;
306 }
307
308 /// Filters [elements] that come from an export, according to its show/hide
309 /// combinators. This modifies [elements] in place.
310 void _filter(List<analyzer.Element> elements,
311 List<NamespaceCombinator> combinators) {
312 for (var c in combinators) {
313 if (c is ShowElementCombinator) {
314 var show = c.shownNames.toSet();
315 elements.retainWhere((e) => show.contains(e.displayName));
316 } else if (c is HideElementCombinator) {
317 var hide = c.hiddenNames.toSet();
318 elements.removeWhere((e) => hide.contains(e.displayName));
319 }
320 }
321 }
322
323 /// Process a class ([cls]). If it contains an appropriate [CustomTag]
Jennifer Messerly 2014/03/27 02:20:32 so, today I can have a Dart class, mark it @reflec
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 This is a great idea! I made the switch. The only
Jennifer Messerly 2014/03/28 21:48:35 ah, yeah that makes sense. seems like several prob
324 /// annotation, we include an initializer to register this class, and make
325 /// sure to include everything that might be accessed or queried from them
326 /// using the smoke package. In particular, polymer uses smoke for the
327 /// following:
328 /// * invoke #registerCallback on custom elements classes, if present.
329 /// * query for methods ending in `*Changed`.
330 /// * query for methods with the `@ObserveProperty` annotation.
331 /// * query for non-final properties labeled with `@published`.
332 /// * read declarations of properties named in the `attributes` attribute.
333 /// * read/write the value of published properties .
334 /// * invoke methods in event handlers.
335 _processClass(ClassElement cls, AssetId id) {
336 // Check whether the class has a @CustomTag annotation. Typically we expect
337 // a single @CustomTag, but it's possible to have several.
338 var tagNames = [];
339 for (var meta in cls.node.metadata) {
340 var tagName = _extractTagName(meta, cls);
341 if (tagName != null) tagNames.add(tagName);
342 }
343 if (tagNames.isEmpty) return;
344
345 if (cls.isPrivate) {
346 logger.error('@CustomTag is no longer supported on private classes:'
Jennifer Messerly 2014/03/27 02:20:32 just curious, is this something we could lift in t
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 (Note that this warning was there before this chan
347 ' ${tagNames.first}', span: _spanForNode(cls, cls.node.name));
348 return;
349 }
350
351 // Include #registerCallback if it exists. Note that by default lookupMember
352 // and query will also add the corresponding getters and setters.
353 recorder.lookupMember(cls, 'registerCallback');
Jennifer Messerly 2014/03/27 02:20:32 this reminds me, #registerCallback needs to go awa
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 it's actually funny because smoke initially didn't
354
355 // Include methods that end with *Changed.
356 recorder.runQuery(cls, new QueryOptions(
357 includeFields: false, includeProperties: false,
358 includeInherited: true, includeMethods: true,
359 includeUpTo: _htmlElementElement,
360 matches: (n) => n.endsWith('Changed') && n != 'attributeChanged'));
361
362 // Include methods marked with @ObserveProperty.
363 recorder.runQuery(cls, new QueryOptions(
364 includeFields: false, includeProperties: false,
365 includeInherited: true, includeMethods: true,
366 includeUpTo: _htmlElementElement,
367 withAnnotations: [_observePropertyElement]));
368
369 // Include @published and @observable properties.
370 // Symbols in @published are used when resolving bindings on published
371 // attributes, symbols for @observable are used via path observers when
372 // implementing *Changed an @ObserveProperty.
373 // TODO(sigmund): consider including only those symbols mentioned in
374 // *Changed and @ObserveProperty instead.
375 recorder.runQuery(cls, new QueryOptions(includeUpTo: _htmlElementElement,
376 withAnnotations: [_publishedElement, _observableElement]));
377
378 for (var tagName in tagNames) {
379 tagsWithClasses.add(tagName);
380 // Include an initializer that will call Polymer.register
381 initializers.add(new _CustomTagInitializer(id, tagName, cls.displayName));
382
383 // Include also properties published via the `attributes` attribute.
384 var attrs = publishedAttributes[tagName];
385 if (attrs == null) continue;
386 for (var attr in attrs) {
387 recorder.lookupMember(cls, attr, recursive: true);
388 }
389 }
390 }
391
392 /// If [meta] is [CustomTag], extract the name associated with the tag.
393 String _extractTagName(Annotation meta, ClassElement cls) {
394 if (meta.element != _customTagConstructor) return null;
395
396 // Read argument from the AST
397 var args = meta.arguments.arguments;
398 if (args == null || args.length == 0) {
399 logger.error('Missing argument in @CustomTag annotation',
400 span: _spanForNode(cls, meta));
401 return null;
402 }
403
404 if (args[0] is! StringLiteral) {
405 logger.error('Only string literals are currently supported by the polymer'
Jennifer Messerly 2014/03/27 02:20:32 totally fine to fix in a follow up, but it looks l
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 nice! Done
Jennifer Messerly 2014/03/28 21:48:35 it worked? awesome! :D
Siggi Cherem (dart-lang) 2014/03/28 22:08:47 yeah! I was so surprised that I had to explicitly
406 ' transformers. See dartbug.com/17739.',
407 span: _spanForNode(cls, meta));
408 return null;
409 }
410 return args[0].stringValue;
411 }
412
413 /// Adds the top-level [function] as an initalizer if it's marked with
414 /// `@initMethod`.
415 _processFunction(FunctionElement function, AssetId id) {
416 bool initMethodFound = false;
417 for (var meta in function.metadata) {
418 var e = meta.element;
419 if (e is PropertyAccessorElement && e.variable == _initMethodElement) {
420 initMethodFound = true;
421 break;
422 }
423 }
424 if (!initMethodFound) return;
425 if (function.isPrivate) {
426 logger.error('@initMethod is no longer supported on private '
427 'functions: ${function.displayName}',
428 span: _spanForNode(function, function.node.name));
429 return;
430 }
431 initializers.add(new _InitMethodInitializer(id, function.displayName));
432 }
433
434 /// Writes the final output for the bootstrap Dart file and entrypoint HTML
435 /// file.
436 void _emitFiles(_) {
131 StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER); 437 StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER);
132 for (int i = 0; i < entryLibraries.length; i++) { 438 Map<AssetId, String> prefixes = {};
133 var url = assetUrlFor(entryLibraries[i], bootstrapId, logger); 439 int i = 0;
134 if (url != null) code.writeln("import '$url' as i$i;"); 440 for (var id in entryLibraries) {
135 } 441 var url = assetUrlFor(id, bootstrapId, logger);
136 442 if (url == null) continue;
137 code..write('\n') 443 code.writeln("import '$url' as i$i;");
138 ..writeln('void main() {') 444 prefixes[id] = 'i$i';
139 ..writeln(' configureForDeployment(['); 445 i++;
140 446 }
141 // Inject @CustomTag and @initMethod initializations for each library 447
142 // that is sourced in a script tag. 448 // Include smoke initialization.
143 for (int i = 0; i < entryLibraries.length; i++) { 449 generator.writeImports(code);
144 for (var init in initializers[entryLibraries[i]]) { 450 generator.writeTopLevelDeclarations(code);
145 var initCode = init.asCode('i$i'); 451 code.writeln('\nvoid main() {');
146 code.write(" $initCode,\n"); 452 generator.writeInitCall(code);
147 } 453 code.writeln(' configureForDeployment([');
454
455 // Include initializers to switch from mirrors_loader to static_loader.
456 // TODO(sigmund): do we need to sort out initializers to ensure that parent
457 // classes are initialized first?
Jennifer Messerly 2014/03/27 02:20:32 currently no -- Polymer.js will handle it using th
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 yeah, we should be preserving the same order here,
458 for (var init in initializers) {
459 var initCode = init.asCode(prefixes[init.assetId]);
460 code.write(" $initCode,\n");
148 } 461 }
149 code..writeln(' ]);') 462 code..writeln(' ]);')
150 ..writeln(' i${entryLibraries.length - 1}.main();') 463 ..writeln(' i${entryLibraries.length - 1}.main();')
151 ..writeln('}'); 464 ..writeln('}');
152 return code.toString(); 465 transform.addOutput(new Asset.fromString(bootstrapId, code.toString()));
153 } 466 transform.addOutput(new Asset.fromString(docId, document.outerHtml));
154 467 }
155 /// Computes initializers needed for each library in [entryLibraries]. Results 468
156 /// are available afterwards in [initializers]. 469 _spanForNode(analyzer.Element context, AstNode node) {
157 Future _computeInitializers() => Future.forEach(entryLibraries, (lib) { 470 var file = resolver.getSourceFile(context);
158 return _initializersOf(lib).then((res) { 471 return file.span(node.offset, node.end);
159 initializers[lib] = res;
160 });
161 });
162
163 /// Computes the initializers of [dartLibrary]. That is, a closure that calls
164 /// Polymer.register for each @CustomTag, and any public top-level methods
165 /// labeled with @initMethod.
166 Future<List<_Initializer>> _initializersOf(AssetId dartLibrary) {
167 var result = [];
168 return transform.readInputAsString(dartLibrary).then((code) {
169 var file = new SourceFile.text(_simpleUriForSource(dartLibrary), code);
170 var unit = parseCompilationUnit(code);
171
172 return Future.forEach(unit.directives, (directive) {
173 // Include anything from parts.
174 if (directive is PartDirective) {
175 var targetId = uriToAssetId(dartLibrary, directive.uri.stringValue,
176 logger, _getSpan(file, directive));
177 return _initializersOf(targetId).then(result.addAll);
178 }
179
180 // Similarly, include anything from exports except what's filtered by
181 // the show/hide combinators.
182 if (directive is ExportDirective) {
183 var targetId = uriToAssetId(dartLibrary, directive.uri.stringValue,
184 logger, _getSpan(file, directive));
185 return _initializersOf(targetId).then(
186 (r) => _processExportDirective(directive, r, result));
187 }
188 }).then((_) {
189 // Scan the code for classes and top-level functions.
190 for (var node in unit.declarations) {
191 if (node is ClassDeclaration) {
192 _processClassDeclaration(node, result, file, logger);
193 } else if (node is FunctionDeclaration &&
194 node.metadata.any(_isInitMethodAnnotation)) {
195 _processFunctionDeclaration(node, result, file, logger);
196 }
197 }
198 return result;
199 });
200 });
201 }
202
203 static String _simpleUriForSource(AssetId source) =>
204 source.path.startsWith('lib/')
205 ? 'package:${source.package}/${source.path.substring(4)}' : source.path;
206
207 /// Filter [exportedInitializers] according to [directive]'s show/hide
208 /// combinators and add the result to [result].
209 // TODO(sigmund): call the analyzer's resolver instead?
210 static _processExportDirective(ExportDirective directive,
211 List<_Initializer> exportedInitializers,
212 List<_Initializer> result) {
213 for (var combinator in directive.combinators) {
214 if (combinator is ShowCombinator) {
215 var show = combinator.shownNames.map((n) => n.name).toSet();
216 exportedInitializers.retainWhere((e) => show.contains(e.symbolName));
217 } else if (combinator is HideCombinator) {
218 var hide = combinator.hiddenNames.map((n) => n.name).toSet();
219 exportedInitializers.removeWhere((e) => hide.contains(e.symbolName));
220 }
221 }
222 result.addAll(exportedInitializers);
223 }
224
225 /// Add an initializer to register [node] as a polymer element if it contains
226 /// an appropriate [CustomTag] annotation.
227 static _processClassDeclaration(ClassDeclaration node,
228 List<_Initializer> result, SourceFile file,
229 TransformLogger logger) {
230 for (var meta in node.metadata) {
231 if (!_isCustomTagAnnotation(meta)) continue;
232 var args = meta.arguments.arguments;
233 if (args == null || args.length == 0) {
234 logger.error('Missing argument in @CustomTag annotation',
235 span: _getSpan(file, meta));
236 continue;
237 }
238
239 var tagName = args[0].stringValue;
240 var typeName = node.name.name;
241 if (typeName.startsWith('_')) {
242 logger.error('@CustomTag is no longer supported on private '
243 'classes: $tagName', span: _getSpan(file, node.name));
244 continue;
245 }
246 result.add(new _CustomTagInitializer(tagName, typeName));
247 }
248 }
249
250 /// Add a method initializer for [function].
251 static _processFunctionDeclaration(FunctionDeclaration function,
252 List<_Initializer> result, SourceFile file,
253 TransformLogger logger) {
254 var name = function.name.name;
255 if (name.startsWith('_')) {
256 logger.error('@initMethod is no longer supported on private '
257 'functions: $name', span: _getSpan(file, function.name));
258 return;
259 }
260 result.add(new _InitMethodInitializer(name));
261 } 472 }
262 } 473 }
263 474
264 // TODO(sigmund): consider support for importing annotations with prefixes.
265 bool _isInitMethodAnnotation(Annotation node) =>
266 node.name.name == 'initMethod' && node.constructorName == null &&
267 node.arguments == null;
268 bool _isCustomTagAnnotation(Annotation node) => node.name.name == 'CustomTag';
269
270 abstract class _Initializer { 475 abstract class _Initializer {
476 AssetId get assetId;
271 String get symbolName; 477 String get symbolName;
272 String asCode(String prefix); 478 String asCode(String prefix);
273 } 479 }
274 480
275 class _InitMethodInitializer implements _Initializer { 481 class _InitMethodInitializer implements _Initializer {
276 String methodName; 482 final AssetId assetId;
483 final String methodName;
277 String get symbolName => methodName; 484 String get symbolName => methodName;
278 _InitMethodInitializer(this.methodName); 485 _InitMethodInitializer(this.assetId, this.methodName);
279 486
280 String asCode(String prefix) => "$prefix.$methodName"; 487 String asCode(String prefix) => "$prefix.$methodName";
281 } 488 }
282 489
283 class _CustomTagInitializer implements _Initializer { 490 class _CustomTagInitializer implements _Initializer {
284 String tagName; 491 final AssetId assetId;
285 String typeName; 492 final String tagName;
493 final String typeName;
286 String get symbolName => typeName; 494 String get symbolName => typeName;
287 _CustomTagInitializer(this.tagName, this.typeName); 495 _CustomTagInitializer(this.assetId, this.tagName, this.typeName);
288 496
289 String asCode(String prefix) => 497 String asCode(String prefix) =>
290 "() => Polymer.register('$tagName', $prefix.$typeName)"; 498 "() => Polymer.register('$tagName', $prefix.$typeName)";
291 } 499 }
292 500
293 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); 501 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end);
294 502
295 const MAIN_HEADER = """ 503 const MAIN_HEADER = """
296 library app_bootstrap; 504 library app_bootstrap;
297 505
298 import 'package:polymer/polymer.dart'; 506 import 'package:polymer/polymer.dart';
299 import 'package:smoke/static.dart' as smoke;
300 """; 507 """;
508
509 /// An html visitor that:
510 /// * finds all polymer expressions and records the getters and setters that
511 /// will be needed to evaluate them at runtime.
512 /// * extracts all attributes declared in the `attribute` attributes of
513 /// polymer elements.
514 class _HtmlExtractor extends TreeVisitor {
515 final Map<String, List<String>> publishedAttributes;
516 final SmokeCodeGenerator generator;
517 final _SubExpressionVisitor visitor;
518 bool _inTemplate = false;
519
520 _HtmlExtractor(SmokeCodeGenerator generator, this.publishedAttributes)
521 : generator = generator,
522 visitor = new _SubExpressionVisitor(generator);
523
524 void visitElement(Element node) {
525 if (_inTemplate) _processNormalElement(node);
526 if (node.localName == 'polymer-element') {
527 _processPolymerElement(node);
528 _processNormalElement(node);
529 }
530
531 if (node.localName == 'template') {
532 var last = _inTemplate;
533 _inTemplate = true;
534 super.visitElement(node);
535 _inTemplate = last;
536 } else {
537 super.visitElement(node);
538 }
539 }
540
541 void visitText(Text node) {
542 if (!_inTemplate) return;
543 var bindings = _parseMustaches(node.data);
544 if (bindings == null) return;
545 for (var e in bindings.expressions) {
546 _addExpression(e, false, false);
547 }
548 }
549
550 /// Regex to split names in the attributes attribute, which supports 'a b c',
551 /// 'a,b,c', or even 'a b,c'.
552 static final _ATTRIBUTES_REGEX = new RegExp(r'\s|,');
553
554 /// Registers getters and setters for all published attributes.
555 void _processPolymerElement(Element node) {
556 var tagName = node.attributes['name'];
557 var value = node.attributes['attributes'];
558 if (value != null) {
559 publishedAttributes[tagName] =
560 value.split(_ATTRIBUTES_REGEX).map((a) => a.trim()).toList();
561 }
562 }
563
564 /// Produces warnings for misuses of on-foo event handlers, and for instanting
565 /// custom tags incorrectly.
566 void _processNormalElement(Element node) {
567 var tag = node.localName;
568 var isCustomTag = isCustomTagName(tag) || node.attributes['is'] != null;
569
570 // Event handlers only allowed inside polymer-elements
571 node.attributes.forEach((name, value) {
572 var bindings = _parseMustaches(value);
573 if (bindings == null) return;
574 var isEvent = false;
575 var isTwoWay = false;
576 if (name is String) {
577 name = name.toLowerCase();
578 isEvent = name.startsWith('on-');
579 isTwoWay = !isEvent && bindings.isWhole && (isCustomTag ||
Jennifer Messerly 2014/03/27 02:20:32 how important is this? it worries me on the maint
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 We could, not sure. We'd have to measure how much
Jennifer Messerly 2014/03/28 21:48:35 yeah, it would be nice to be in template_binding,
Siggi Cherem (dart-lang) 2014/03/28 22:08:47 Sounds great. I'll file a bug to track as well.
580 tag == 'input' && (name == 'value' || name =='checked') ||
581 tag == 'select' && (name == 'selectedindex' || name == 'value') ||
582 tag == 'textarea' && name == 'value');
583 }
584 for (var exp in bindings.expressions) {
585 _addExpression(exp, isEvent, isTwoWay);
586 }
587 });
588 }
589
590 void _addExpression(String stringExpression, bool inEvent, bool isTwoWay) {
591 if (inEvent) {
592 if (!stringExpression.startsWith("@")) {
593 generator.addGetter(stringExpression);
594 generator.addSymbol(stringExpression);
595 return;
596 }
597 stringExpression = stringExpression.substring(1);
598 }
599 visitor.run(pe.parse(stringExpression), isTwoWay);
600 }
601 }
602
603 /// A polymer-expression visitor that records every getter and setter that will
604 /// be needed to evaluate a single expression at runtime.
605 class _SubExpressionVisitor extends pe.RecursiveVisitor {
606 final SmokeCodeGenerator generator;
607 bool _includeSetter;
608
609 _SubExpressionVisitor(this.generator);
610
611 /// Visit [exp], and record getters and setters that are needed in order to
612 /// evaluate it at runtime. [includeSetter] is only true if this expression
613 /// occured in a context where it could be updated, for example in two-way
614 /// bindings such as `<input value={{exp}}>`.
615 void run(pe.Expression exp, bool includeSetter) {
616 _includeSetter = includeSetter;
617 visit(exp);
618 }
619
620 /// Adds a getter and symbol for [name], and optionally a setter.
621 _add(String name) {
622 generator.addGetter(name);
623 generator.addSymbol(name);
624 if (_includeSetter) generator.addSetter(name);
625 }
626
627 void preVisitExpression(e) {
628 // For two-way bindings the outermost expression may be updated, so we need
629 // both the getter and the setter, but subexpressions only need the getter.
630 // So we exclude setters as soon as we go deeper in the tree.
631 _includeSetter = false;
632 }
633
634 visitIdentifier(pe.Identifier e) {
635 if (e.value != 'this') _add(e.value);
636 super.visitIdentifier(e);
637 }
638
639 visitGetter(pe.Getter e) {
640 _add(e.name);
641 super.visitGetter(e);
642 }
643
644 visitInvoke(pe.Invoke e) {
645 _includeSetter = false; // Invoke is only valid as an r-value.
646 _add(e.method);
647 super.visitInvoke(e);
648 }
649 }
650
651 class _Mustaches {
652 /// Each expression that appears within `{{...}}` and `[[...]]`.
653 List<String> expressions = [];
654
655 /// Whether the whole text returned by [_parseMustaches] was a single mustache
656 /// expression.
657 bool isWhole;
658
659 _Mustaches(this.isWhole);
660 }
661
662 // TODO(sigmund): this is a simplification of what template-binding does. Could
663 // we share some code here and in template_binding/src/mustache_tokens.dart?
Jennifer Messerly 2014/03/27 02:20:32 could you just import that file? it doesn't look l
Siggi Cherem (dart-lang) 2014/03/28 01:04:26 Done. I managed to refactor it slightly and now I'
664 _Mustaches _parseMustaches(String text) {
665 if (text == null || text.isEmpty) return null;
666 var bindings = null;
667 var length = text.length;
668 var lastIndex = 0;
669 while (lastIndex < length) {
670 var startIndex = text.indexOf('{{', lastIndex);
671 var oneTimeStart = text.indexOf('[[', lastIndex);
672 var oneTime = false;
673 var terminator = '}}';
674
675 if (oneTimeStart >= 0 &&
676 (startIndex < 0 || oneTimeStart < startIndex)) {
677 startIndex = oneTimeStart;
678 oneTime = true;
679 terminator = ']]';
680 }
681
682 var endIndex = -1;
683 if (startIndex >= 0) {
684 endIndex = text.indexOf(terminator, startIndex + 2);
685 }
686
687 if (endIndex < 0) return bindings;
688
689 if (bindings == null) {
690 bindings = new _Mustaches(startIndex == 0 && endIndex == length - 2);
691 }
692 var pathString = text.substring(startIndex + 2, endIndex).trim();
693 bindings.expressions.add(pathString);
694 lastIndex = endIndex + 2;
695 }
696 return bindings;
697 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698