 Chromium Code Reviews
 Chromium Code Reviews Issue 211393006:
  Enables codegen support in polymer  (Closed) 
  Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
    
  
    Issue 211393006:
  Enables codegen support in polymer  (Closed) 
  Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart| Index: pkg/polymer/lib/src/build/script_compactor.dart | 
| diff --git a/pkg/polymer/lib/src/build/script_compactor.dart b/pkg/polymer/lib/src/build/script_compactor.dart | 
| index 61a6b7199e5a41ac3ffd1a06f5e70a21d9a4b527..9344c76a803e4a33b63ac00728773d3f5b0f73d7 100644 | 
| --- a/pkg/polymer/lib/src/build/script_compactor.dart | 
| +++ b/pkg/polymer/lib/src/build/script_compactor.dart | 
| @@ -8,12 +8,23 @@ library polymer.src.build.script_compactor; | 
| import 'dart:async'; | 
| import 'dart:convert'; | 
| -import 'package:html5lib/dom.dart' show Document, Element; | 
| +import 'package:html5lib/dom.dart' show Document, Element, Text; | 
| +import 'package:html5lib/dom_parsing.dart'; | 
| import 'package:analyzer/src/generated/ast.dart'; | 
| +import 'package:analyzer/src/generated/element.dart' hide Element; | 
| +import 'package:analyzer/src/generated/element.dart' as analyzer show Element; | 
| import 'package:barback/barback.dart'; | 
| import 'package:code_transformers/assets.dart'; | 
| import 'package:path/path.dart' as path; | 
| import 'package:source_maps/span.dart' show SourceFile; | 
| +import 'package:smoke/codegen/generator.dart'; | 
| +import 'package:smoke/codegen/recorder.dart'; | 
| +import 'package:code_transformers/resolver.dart'; | 
| +import 'package:code_transformers/src/dart_sdk.dart'; | 
| + | 
| +import 'package:polymer_expressions/expression.dart' as pe; | 
| +import 'package:polymer_expressions/parser.dart' as pe; | 
| +import 'package:polymer_expressions/visitor.dart' as pe; | 
| import 'import_inliner.dart' show ImportInliner; // just for docs. | 
| import 'common.dart'; | 
| @@ -29,16 +40,18 @@ import 'common.dart'; | 
| /// statement to a library, and then uses `initPolymer` (see polymer.dart) to | 
| /// process `@initMethod` and `@CustomTag` annotations in those libraries. | 
| class ScriptCompactor extends Transformer { | 
| + 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
 | 
| final TransformOptions options; | 
| - ScriptCompactor(this.options); | 
| + ScriptCompactor(this.options, {String sdkDir}) | 
| + : resolvers = new Resolvers(sdkDir != null ? sdkDir : dartSdkDirectory); | 
| /// Only run on entry point .html files. | 
| Future<bool> isPrimary(Asset input) => | 
| new Future.value(options.isHtmlEntryPoint(input.id)); | 
| Future apply(Transform transform) => | 
| - new _ScriptCompactor(transform, options).apply(); | 
| + new _ScriptCompactor(transform, options, resolvers).apply(); | 
| } | 
| /// Helper class mainly use to flatten the async code. | 
| @@ -49,13 +62,60 @@ class _ScriptCompactor extends PolymerTransformer { | 
| final AssetId docId; | 
| final AssetId bootstrapId; | 
| + /// HTML document parsed from [docId]. | 
| Document document; | 
| + | 
| + /// List of ids for each Dart entry script tag (the main tag and any tag | 
| + /// included on each custom element definition). | 
| List<AssetId> entryLibraries; | 
| + | 
| + /// The id of the main Dart program. | 
| AssetId mainLibraryId; | 
| + | 
| + /// Script tag that loads the Dart entry point. | 
| Element mainScriptTag; | 
| - final Map<AssetId, List<_Initializer>> initializers = {}; | 
| - _ScriptCompactor(Transform transform, this.options) | 
| + /// Initializers that will register custom tags or invoke `initMethod`s. | 
| + final List<_Initializer> initializers = []; | 
| + | 
| + /// Attributes published on a custom-tag. We make these available via | 
| + /// reflection even if @published was not used. | 
| + final Map<String, List<String>> publishedAttributes = {}; | 
| + | 
| + /// Those custom-tags for which we have found a corresponding class in code. | 
| + final Set<String> tagsWithClasses = new Set(); | 
| + | 
| + /// Hook needed to access the analyzer within barback transformers. | 
| + final Resolvers resolvers; | 
| + | 
| + /// The resolver instance associated with a single run of this transformer. | 
| + Resolver resolver; | 
| + | 
| + /// Element representing `HtmlElement`. | 
| + 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
 | 
| + | 
| + /// Element representing the constructor of `@CustomTag`. | 
| + ConstructorElement _customTagConstructor; | 
| + | 
| + /// Element representing the type of `@published`. | 
| + ClassElement _publishedElement; | 
| + | 
| + /// Element representing the type of `@observable`. | 
| + ClassElement _observableElement; | 
| + | 
| + /// Element representing the type of `@ObserveProperty`. | 
| + ClassElement _observePropertyElement; | 
| + | 
| + /// Element representing the `@initMethod` annotation. | 
| + TopLevelVariableElement _initMethodElement; | 
| + | 
| + /// Code generator used to create the static initialization for smoke. | 
| + 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
 | 
| + | 
| + /// Recorder that uses analyzer data to feed data to [generator]. | 
| + Recorder recorder; | 
| + | 
| + _ScriptCompactor(Transform transform, this.options, this.resolvers) | 
| : transform = transform, | 
| logger = transform.logger, | 
| docId = transform.primaryInput.id, | 
| @@ -120,171 +180,319 @@ class _ScriptCompactor extends PolymerTransformer { | 
| // Emit the bootstrap .dart file | 
| mainScriptTag.attributes['src'] = path.url.basename(bootstrapId.path); | 
| entryLibraries.add(mainLibraryId); | 
| - return _computeInitializers().then(_createBootstrapCode).then((code) { | 
| - transform.addOutput(new Asset.fromString(bootstrapId, code)); | 
| - transform.addOutput(new Asset.fromString(docId, document.outerHtml)); | 
| - }); | 
| + | 
| + return _initResolver() | 
| + .then(_extractUsesOfMirrors) | 
| + .then(_emitFiles) | 
| + .then((_) => resolver.release()); | 
| } | 
| - /// Emits the actual bootstrap code. | 
| - String _createBootstrapCode(_) { | 
| - StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER); | 
| - for (int i = 0; i < entryLibraries.length; i++) { | 
| - var url = assetUrlFor(entryLibraries[i], bootstrapId, logger); | 
| - if (url != null) code.writeln("import '$url' as i$i;"); | 
| + /// Load a resolver that computes information for every library in | 
| + /// [entryLibraries], then use it to initialize the [recorder] (for import | 
| + /// resolution) and to resolve specific elements (for analyzing the user's | 
| + /// code). | 
| + Future _initResolver() => resolvers.get(transform, entryLibraries).then((r) { | 
| + resolver = r; | 
| + recorder = new Recorder(generator, | 
| + (lib) => resolver.getImportUri(lib, from: bootstrapId).toString()); | 
| + | 
| + // Load class elements that are used in queries for codegen. | 
| + 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
 | 
| + if (polymerLib == null) _definitionError('the polymer library'); | 
| + var htmlLib = r.getLibraryByUri(Uri.parse('dart:html')); | 
| + if (htmlLib == null) _definitionError('the "dart:html" library'); | 
| + var observeLib = r.getLibrary( | 
| + new AssetId('observe', 'lib/src/metadata.dart')); | 
| + if (observeLib == null) _definitionError('the observe library'); | 
| + | 
| + for (var unit in polymerLib.parts) { | 
| + if (unit.uri == 'src/loader.dart') { | 
| + _initMethodElement = unit.topLevelVariables.firstWhere( | 
| + (t) => t.displayName == 'initMethod'); | 
| + break; | 
| + } | 
| } | 
| + _customTagConstructor = | 
| + _lookupType(polymerLib, 'CustomTag').constructors.first; | 
| + _publishedElement = _lookupType(polymerLib, 'PublishedProperty'); | 
| + _observableElement = _lookupType(observeLib, 'ObservableProperty'); | 
| + _observePropertyElement = _lookupType(polymerLib, 'ObserveProperty'); | 
| + _htmlElementElement = _lookupType(htmlLib, 'HtmlElement'); | 
| + if (_initMethodElement == null) _definitionError('@initMethod'); | 
| + }); | 
| + | 
| + _lookupType(LibraryElement lib, String typeName) { | 
| + var result = lib.getType(typeName); | 
| + if (result == null) _definitionError(typeName); | 
| + return result; | 
| + } | 
| - code..write('\n') | 
| - ..writeln('void main() {') | 
| - ..writeln(' configureForDeployment(['); | 
| + _definitionError(name) { | 
| + 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
 | 
| + "definition of $name."); | 
| + } | 
| + | 
| + /// Inspects the entire program to find out anything that polymer accesses | 
| + /// using mirrors and produces static information that can be used to replace | 
| + /// the mirror-based loader and the uses of mirrors through the `smoke` | 
| + /// package. This includes: | 
| + /// | 
| + /// * visiting entry-libraries to extract initializers, | 
| + /// * visiting polymer-expressions to extract getters and setters, | 
| + /// * looking for published fields of custom elements, and | 
| + /// * looking for event handlers and callbacks of change notifications. | 
| + /// | 
| + 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
:)
 | 
| + // Generate getters and setters needed to evaluate polymer expressions, and | 
| + // extract information about published attributes. | 
| + new _HtmlExtractor(generator, publishedAttributes).visit(document); | 
| + | 
| + // Process all classes and top-level functions to include initializers, | 
| + // register custom elements, and include special fields and methods in | 
| + // custom element classes. | 
| + for (var id in entryLibraries) { | 
| + var lib = resolver.getLibrary(id); | 
| + for (var cls in _visibleClassesOf(lib)) { | 
| + _processClass(cls, id); | 
| + } | 
| - // Inject @CustomTag and @initMethod initializations for each library | 
| - // that is sourced in a script tag. | 
| - for (int i = 0; i < entryLibraries.length; i++) { | 
| - for (var init in initializers[entryLibraries[i]]) { | 
| - var initCode = init.asCode('i$i'); | 
| - code.write(" $initCode,\n"); | 
| + for (var fun in _visibleTopLevelMethodsOf(lib)) { | 
| + _processFunction(fun, id); | 
| } | 
| } | 
| - code..writeln(' ]);') | 
| - ..writeln(' i${entryLibraries.length - 1}.main();') | 
| - ..writeln('}'); | 
| - return code.toString(); | 
| - } | 
| - /// Computes initializers needed for each library in [entryLibraries]. Results | 
| - /// are available afterwards in [initializers]. | 
| - Future _computeInitializers() => Future.forEach(entryLibraries, (lib) { | 
| - return _initializersOf(lib).then((res) { | 
| - initializers[lib] = res; | 
| - }); | 
| + // Warn about tagNames with no corresponding Dart class | 
| + // TODO(sigmund): is there a way to exclude polymer.js elements? should we | 
| + // 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.
 | 
| + publishedAttributes.forEach((tagName, attrs) { | 
| + if (tagsWithClasses.contains(tagName)) return; | 
| + logger.warning('Class for custom-element "$tagName" not found. ' | 
| + 'Code-generation might be incomplete.'); | 
| + // We include accessors for the missing attributes because they help get | 
| + // better warning messages at runtime. | 
| + for (var attr in attrs) { | 
| + generator.addGetter(attr); | 
| + generator.addSetter(attr); | 
| + generator.addSymbol(attr); | 
| + } | 
| }); | 
| + } | 
| - /// Computes the initializers of [dartLibrary]. That is, a closure that calls | 
| - /// Polymer.register for each @CustomTag, and any public top-level methods | 
| - /// labeled with @initMethod. | 
| - Future<List<_Initializer>> _initializersOf(AssetId dartLibrary) { | 
| + /// Retrieves all classses that are visible if you were to import [lib]. This | 
| + /// includes exported classes from other libraries. | 
| + List<ClassElement> _visibleClassesOf(LibraryElement lib) { | 
| var result = []; | 
| - return transform.readInputAsString(dartLibrary).then((code) { | 
| - var file = new SourceFile.text(_simpleUriForSource(dartLibrary), code); | 
| - var unit = parseCompilationUnit(code); | 
| - | 
| - return Future.forEach(unit.directives, (directive) { | 
| - // Include anything from parts. | 
| - if (directive is PartDirective) { | 
| - var targetId = uriToAssetId(dartLibrary, directive.uri.stringValue, | 
| - logger, _getSpan(file, directive)); | 
| - return _initializersOf(targetId).then(result.addAll); | 
| - } | 
| - | 
| - // Similarly, include anything from exports except what's filtered by | 
| - // the show/hide combinators. | 
| - if (directive is ExportDirective) { | 
| - var targetId = uriToAssetId(dartLibrary, directive.uri.stringValue, | 
| - logger, _getSpan(file, directive)); | 
| - return _initializersOf(targetId).then( | 
| - (r) => _processExportDirective(directive, r, result)); | 
| - } | 
| - }).then((_) { | 
| - // Scan the code for classes and top-level functions. | 
| - for (var node in unit.declarations) { | 
| - if (node is ClassDeclaration) { | 
| - _processClassDeclaration(node, result, file, logger); | 
| - } else if (node is FunctionDeclaration && | 
| - node.metadata.any(_isInitMethodAnnotation)) { | 
| - _processFunctionDeclaration(node, result, file, logger); | 
| - } | 
| - } | 
| - return result; | 
| - }); | 
| - }); | 
| + result.addAll(lib.units.expand((u) => u.types)); | 
| + for (var e in lib.exports) { | 
| + var exported = e.exportedLibrary.units.expand((u) => u.types).toList(); | 
| + _filter(exported, e.combinators); | 
| + result.addAll(exported); | 
| + } | 
| + return result; | 
| } | 
| - static String _simpleUriForSource(AssetId source) => | 
| - source.path.startsWith('lib/') | 
| - ? 'package:${source.package}/${source.path.substring(4)}' : source.path; | 
| - | 
| - /// Filter [exportedInitializers] according to [directive]'s show/hide | 
| - /// combinators and add the result to [result]. | 
| - // TODO(sigmund): call the analyzer's resolver instead? | 
| - static _processExportDirective(ExportDirective directive, | 
| - List<_Initializer> exportedInitializers, | 
| - List<_Initializer> result) { | 
| - for (var combinator in directive.combinators) { | 
| - if (combinator is ShowCombinator) { | 
| - var show = combinator.shownNames.map((n) => n.name).toSet(); | 
| - exportedInitializers.retainWhere((e) => show.contains(e.symbolName)); | 
| - } else if (combinator is HideCombinator) { | 
| - var hide = combinator.hiddenNames.map((n) => n.name).toSet(); | 
| - exportedInitializers.removeWhere((e) => hide.contains(e.symbolName)); | 
| - } | 
| + /// Retrieves all top-level methods that are visible if you were to import | 
| + /// [lib]. This includes exported methods from other libraries too. | 
| + List<ClassElement> _visibleTopLevelMethodsOf(LibraryElement lib) { | 
| + var result = []; | 
| + result.addAll(lib.units.expand((u) => u.functions)); | 
| + for (var e in lib.exports) { | 
| + var exported = e.exportedLibrary.units | 
| + .expand((u) => u.functions).toList(); | 
| + _filter(exported, e.combinators); | 
| + result.addAll(exported); | 
| } | 
| - result.addAll(exportedInitializers); | 
| + return result; | 
| } | 
| - /// Add an initializer to register [node] as a polymer element if it contains | 
| - /// an appropriate [CustomTag] annotation. | 
| - static _processClassDeclaration(ClassDeclaration node, | 
| - List<_Initializer> result, SourceFile file, | 
| - TransformLogger logger) { | 
| - for (var meta in node.metadata) { | 
| - if (!_isCustomTagAnnotation(meta)) continue; | 
| - var args = meta.arguments.arguments; | 
| - if (args == null || args.length == 0) { | 
| - logger.error('Missing argument in @CustomTag annotation', | 
| - span: _getSpan(file, meta)); | 
| - continue; | 
| + /// Filters [elements] that come from an export, according to its show/hide | 
| + /// combinators. This modifies [elements] in place. | 
| + void _filter(List<analyzer.Element> elements, | 
| + List<NamespaceCombinator> combinators) { | 
| + for (var c in combinators) { | 
| + if (c is ShowElementCombinator) { | 
| + var show = c.shownNames.toSet(); | 
| + elements.retainWhere((e) => show.contains(e.displayName)); | 
| + } else if (c is HideElementCombinator) { | 
| + var hide = c.hiddenNames.toSet(); | 
| + elements.removeWhere((e) => hide.contains(e.displayName)); | 
| } | 
| + } | 
| + } | 
| - var tagName = args[0].stringValue; | 
| - var typeName = node.name.name; | 
| - if (typeName.startsWith('_')) { | 
| - logger.error('@CustomTag is no longer supported on private ' | 
| - 'classes: $tagName', span: _getSpan(file, node.name)); | 
| - continue; | 
| + /// 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
 | 
| + /// annotation, we include an initializer to register this class, and make | 
| + /// sure to include everything that might be accessed or queried from them | 
| + /// using the smoke package. In particular, polymer uses smoke for the | 
| + /// following: | 
| + /// * invoke #registerCallback on custom elements classes, if present. | 
| + /// * query for methods ending in `*Changed`. | 
| + /// * query for methods with the `@ObserveProperty` annotation. | 
| + /// * query for non-final properties labeled with `@published`. | 
| + /// * read declarations of properties named in the `attributes` attribute. | 
| + /// * read/write the value of published properties . | 
| + /// * invoke methods in event handlers. | 
| + _processClass(ClassElement cls, AssetId id) { | 
| + // Check whether the class has a @CustomTag annotation. Typically we expect | 
| + // a single @CustomTag, but it's possible to have several. | 
| + var tagNames = []; | 
| + for (var meta in cls.node.metadata) { | 
| + var tagName = _extractTagName(meta, cls); | 
| + if (tagName != null) tagNames.add(tagName); | 
| + } | 
| + if (tagNames.isEmpty) return; | 
| + | 
| + if (cls.isPrivate) { | 
| + 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
 | 
| + ' ${tagNames.first}', span: _spanForNode(cls, cls.node.name)); | 
| + return; | 
| + } | 
| + | 
| + // Include #registerCallback if it exists. Note that by default lookupMember | 
| + // and query will also add the corresponding getters and setters. | 
| + 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
 | 
| + | 
| + // Include methods that end with *Changed. | 
| + recorder.runQuery(cls, new QueryOptions( | 
| + includeFields: false, includeProperties: false, | 
| + includeInherited: true, includeMethods: true, | 
| + includeUpTo: _htmlElementElement, | 
| + matches: (n) => n.endsWith('Changed') && n != 'attributeChanged')); | 
| + | 
| + // Include methods marked with @ObserveProperty. | 
| + recorder.runQuery(cls, new QueryOptions( | 
| + includeFields: false, includeProperties: false, | 
| + includeInherited: true, includeMethods: true, | 
| + includeUpTo: _htmlElementElement, | 
| + withAnnotations: [_observePropertyElement])); | 
| + | 
| + // Include @published and @observable properties. | 
| + // Symbols in @published are used when resolving bindings on published | 
| + // attributes, symbols for @observable are used via path observers when | 
| + // implementing *Changed an @ObserveProperty. | 
| + // TODO(sigmund): consider including only those symbols mentioned in | 
| + // *Changed and @ObserveProperty instead. | 
| + recorder.runQuery(cls, new QueryOptions(includeUpTo: _htmlElementElement, | 
| + withAnnotations: [_publishedElement, _observableElement])); | 
| + | 
| + for (var tagName in tagNames) { | 
| + tagsWithClasses.add(tagName); | 
| + // Include an initializer that will call Polymer.register | 
| + initializers.add(new _CustomTagInitializer(id, tagName, cls.displayName)); | 
| + | 
| + // Include also properties published via the `attributes` attribute. | 
| + var attrs = publishedAttributes[tagName]; | 
| + if (attrs == null) continue; | 
| + for (var attr in attrs) { | 
| + recorder.lookupMember(cls, attr, recursive: true); | 
| } | 
| - result.add(new _CustomTagInitializer(tagName, typeName)); | 
| } | 
| } | 
| - /// Add a method initializer for [function]. | 
| - static _processFunctionDeclaration(FunctionDeclaration function, | 
| - List<_Initializer> result, SourceFile file, | 
| - TransformLogger logger) { | 
| - var name = function.name.name; | 
| - if (name.startsWith('_')) { | 
| + /// If [meta] is [CustomTag], extract the name associated with the tag. | 
| + String _extractTagName(Annotation meta, ClassElement cls) { | 
| + if (meta.element != _customTagConstructor) return null; | 
| + | 
| + // Read argument from the AST | 
| + var args = meta.arguments.arguments; | 
| + if (args == null || args.length == 0) { | 
| + logger.error('Missing argument in @CustomTag annotation', | 
| + span: _spanForNode(cls, meta)); | 
| + return null; | 
| + } | 
| + | 
| + if (args[0] is! StringLiteral) { | 
| + 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
 | 
| + ' transformers. See dartbug.com/17739.', | 
| + span: _spanForNode(cls, meta)); | 
| + return null; | 
| + } | 
| + return args[0].stringValue; | 
| + } | 
| + | 
| + /// Adds the top-level [function] as an initalizer if it's marked with | 
| + /// `@initMethod`. | 
| + _processFunction(FunctionElement function, AssetId id) { | 
| + bool initMethodFound = false; | 
| + for (var meta in function.metadata) { | 
| + var e = meta.element; | 
| + if (e is PropertyAccessorElement && e.variable == _initMethodElement) { | 
| + initMethodFound = true; | 
| + break; | 
| + } | 
| + } | 
| + if (!initMethodFound) return; | 
| + if (function.isPrivate) { | 
| logger.error('@initMethod is no longer supported on private ' | 
| - 'functions: $name', span: _getSpan(file, function.name)); | 
| + 'functions: ${function.displayName}', | 
| + span: _spanForNode(function, function.node.name)); | 
| return; | 
| } | 
| - result.add(new _InitMethodInitializer(name)); | 
| + initializers.add(new _InitMethodInitializer(id, function.displayName)); | 
| } | 
| -} | 
| -// TODO(sigmund): consider support for importing annotations with prefixes. | 
| -bool _isInitMethodAnnotation(Annotation node) => | 
| - node.name.name == 'initMethod' && node.constructorName == null && | 
| - node.arguments == null; | 
| -bool _isCustomTagAnnotation(Annotation node) => node.name.name == 'CustomTag'; | 
| + /// Writes the final output for the bootstrap Dart file and entrypoint HTML | 
| + /// file. | 
| + void _emitFiles(_) { | 
| + StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER); | 
| + Map<AssetId, String> prefixes = {}; | 
| + int i = 0; | 
| + for (var id in entryLibraries) { | 
| + var url = assetUrlFor(id, bootstrapId, logger); | 
| + if (url == null) continue; | 
| + code.writeln("import '$url' as i$i;"); | 
| + prefixes[id] = 'i$i'; | 
| + i++; | 
| + } | 
| + | 
| + // Include smoke initialization. | 
| + generator.writeImports(code); | 
| + generator.writeTopLevelDeclarations(code); | 
| + code.writeln('\nvoid main() {'); | 
| + generator.writeInitCall(code); | 
| + code.writeln(' configureForDeployment(['); | 
| + | 
| + // Include initializers to switch from mirrors_loader to static_loader. | 
| + // TODO(sigmund): do we need to sort out initializers to ensure that parent | 
| + // 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,
 | 
| + for (var init in initializers) { | 
| + var initCode = init.asCode(prefixes[init.assetId]); | 
| + code.write(" $initCode,\n"); | 
| + } | 
| + code..writeln(' ]);') | 
| + ..writeln(' i${entryLibraries.length - 1}.main();') | 
| + ..writeln('}'); | 
| + transform.addOutput(new Asset.fromString(bootstrapId, code.toString())); | 
| + transform.addOutput(new Asset.fromString(docId, document.outerHtml)); | 
| + } | 
| + | 
| + _spanForNode(analyzer.Element context, AstNode node) { | 
| + var file = resolver.getSourceFile(context); | 
| + return file.span(node.offset, node.end); | 
| + } | 
| +} | 
| abstract class _Initializer { | 
| + AssetId get assetId; | 
| String get symbolName; | 
| String asCode(String prefix); | 
| } | 
| class _InitMethodInitializer implements _Initializer { | 
| - String methodName; | 
| + final AssetId assetId; | 
| + final String methodName; | 
| String get symbolName => methodName; | 
| - _InitMethodInitializer(this.methodName); | 
| + _InitMethodInitializer(this.assetId, this.methodName); | 
| String asCode(String prefix) => "$prefix.$methodName"; | 
| } | 
| class _CustomTagInitializer implements _Initializer { | 
| - String tagName; | 
| - String typeName; | 
| + final AssetId assetId; | 
| + final String tagName; | 
| + final String typeName; | 
| String get symbolName => typeName; | 
| - _CustomTagInitializer(this.tagName, this.typeName); | 
| + _CustomTagInitializer(this.assetId, this.tagName, this.typeName); | 
| String asCode(String prefix) => | 
| "() => Polymer.register('$tagName', $prefix.$typeName)"; | 
| @@ -296,5 +504,194 @@ const MAIN_HEADER = """ | 
| library app_bootstrap; | 
| import 'package:polymer/polymer.dart'; | 
| -import 'package:smoke/static.dart' as smoke; | 
| """; | 
| + | 
| +/// An html visitor that: | 
| +/// * finds all polymer expressions and records the getters and setters that | 
| +/// will be needed to evaluate them at runtime. | 
| +/// * extracts all attributes declared in the `attribute` attributes of | 
| +/// polymer elements. | 
| +class _HtmlExtractor extends TreeVisitor { | 
| + final Map<String, List<String>> publishedAttributes; | 
| + final SmokeCodeGenerator generator; | 
| + final _SubExpressionVisitor visitor; | 
| + bool _inTemplate = false; | 
| + | 
| + _HtmlExtractor(SmokeCodeGenerator generator, this.publishedAttributes) | 
| + : generator = generator, | 
| + visitor = new _SubExpressionVisitor(generator); | 
| + | 
| + void visitElement(Element node) { | 
| + if (_inTemplate) _processNormalElement(node); | 
| + if (node.localName == 'polymer-element') { | 
| + _processPolymerElement(node); | 
| + _processNormalElement(node); | 
| + } | 
| + | 
| + if (node.localName == 'template') { | 
| + var last = _inTemplate; | 
| + _inTemplate = true; | 
| + super.visitElement(node); | 
| + _inTemplate = last; | 
| + } else { | 
| + super.visitElement(node); | 
| + } | 
| + } | 
| + | 
| + void visitText(Text node) { | 
| + if (!_inTemplate) return; | 
| + var bindings = _parseMustaches(node.data); | 
| + if (bindings == null) return; | 
| + for (var e in bindings.expressions) { | 
| + _addExpression(e, false, false); | 
| + } | 
| + } | 
| + | 
| + /// Regex to split names in the attributes attribute, which supports 'a b c', | 
| + /// 'a,b,c', or even 'a b,c'. | 
| + static final _ATTRIBUTES_REGEX = new RegExp(r'\s|,'); | 
| + | 
| + /// Registers getters and setters for all published attributes. | 
| + void _processPolymerElement(Element node) { | 
| + var tagName = node.attributes['name']; | 
| + var value = node.attributes['attributes']; | 
| + if (value != null) { | 
| + publishedAttributes[tagName] = | 
| + value.split(_ATTRIBUTES_REGEX).map((a) => a.trim()).toList(); | 
| + } | 
| + } | 
| + | 
| + /// Produces warnings for misuses of on-foo event handlers, and for instanting | 
| + /// custom tags incorrectly. | 
| + void _processNormalElement(Element node) { | 
| + var tag = node.localName; | 
| + var isCustomTag = isCustomTagName(tag) || node.attributes['is'] != null; | 
| + | 
| + // Event handlers only allowed inside polymer-elements | 
| + node.attributes.forEach((name, value) { | 
| + var bindings = _parseMustaches(value); | 
| + if (bindings == null) return; | 
| + var isEvent = false; | 
| + var isTwoWay = false; | 
| + if (name is String) { | 
| + name = name.toLowerCase(); | 
| + isEvent = name.startsWith('on-'); | 
| + 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.
 | 
| + tag == 'input' && (name == 'value' || name =='checked') || | 
| + tag == 'select' && (name == 'selectedindex' || name == 'value') || | 
| + tag == 'textarea' && name == 'value'); | 
| + } | 
| + for (var exp in bindings.expressions) { | 
| + _addExpression(exp, isEvent, isTwoWay); | 
| + } | 
| + }); | 
| + } | 
| + | 
| + void _addExpression(String stringExpression, bool inEvent, bool isTwoWay) { | 
| + if (inEvent) { | 
| + if (!stringExpression.startsWith("@")) { | 
| + generator.addGetter(stringExpression); | 
| + generator.addSymbol(stringExpression); | 
| + return; | 
| + } | 
| + stringExpression = stringExpression.substring(1); | 
| + } | 
| + visitor.run(pe.parse(stringExpression), isTwoWay); | 
| + } | 
| +} | 
| + | 
| +/// A polymer-expression visitor that records every getter and setter that will | 
| +/// be needed to evaluate a single expression at runtime. | 
| +class _SubExpressionVisitor extends pe.RecursiveVisitor { | 
| + final SmokeCodeGenerator generator; | 
| + bool _includeSetter; | 
| + | 
| + _SubExpressionVisitor(this.generator); | 
| + | 
| + /// Visit [exp], and record getters and setters that are needed in order to | 
| + /// evaluate it at runtime. [includeSetter] is only true if this expression | 
| + /// occured in a context where it could be updated, for example in two-way | 
| + /// bindings such as `<input value={{exp}}>`. | 
| + void run(pe.Expression exp, bool includeSetter) { | 
| + _includeSetter = includeSetter; | 
| + visit(exp); | 
| + } | 
| + | 
| + /// Adds a getter and symbol for [name], and optionally a setter. | 
| + _add(String name) { | 
| + generator.addGetter(name); | 
| + generator.addSymbol(name); | 
| + if (_includeSetter) generator.addSetter(name); | 
| + } | 
| + | 
| + void preVisitExpression(e) { | 
| + // For two-way bindings the outermost expression may be updated, so we need | 
| + // both the getter and the setter, but subexpressions only need the getter. | 
| + // So we exclude setters as soon as we go deeper in the tree. | 
| + _includeSetter = false; | 
| + } | 
| + | 
| + visitIdentifier(pe.Identifier e) { | 
| + if (e.value != 'this') _add(e.value); | 
| + super.visitIdentifier(e); | 
| + } | 
| + | 
| + visitGetter(pe.Getter e) { | 
| + _add(e.name); | 
| + super.visitGetter(e); | 
| + } | 
| + | 
| + visitInvoke(pe.Invoke e) { | 
| + _includeSetter = false; // Invoke is only valid as an r-value. | 
| + _add(e.method); | 
| + super.visitInvoke(e); | 
| + } | 
| +} | 
| + | 
| +class _Mustaches { | 
| + /// Each expression that appears within `{{...}}` and `[[...]]`. | 
| + List<String> expressions = []; | 
| + | 
| + /// Whether the whole text returned by [_parseMustaches] was a single mustache | 
| + /// expression. | 
| + bool isWhole; | 
| + | 
| + _Mustaches(this.isWhole); | 
| +} | 
| + | 
| +// TODO(sigmund): this is a simplification of what template-binding does. Could | 
| +// 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'
 | 
| +_Mustaches _parseMustaches(String text) { | 
| + if (text == null || text.isEmpty) return null; | 
| + var bindings = null; | 
| + var length = text.length; | 
| + var lastIndex = 0; | 
| + while (lastIndex < length) { | 
| + var startIndex = text.indexOf('{{', lastIndex); | 
| + var oneTimeStart = text.indexOf('[[', lastIndex); | 
| + var oneTime = false; | 
| + var terminator = '}}'; | 
| + | 
| + if (oneTimeStart >= 0 && | 
| + (startIndex < 0 || oneTimeStart < startIndex)) { | 
| + startIndex = oneTimeStart; | 
| + oneTime = true; | 
| + terminator = ']]'; | 
| + } | 
| + | 
| + var endIndex = -1; | 
| + if (startIndex >= 0) { | 
| + endIndex = text.indexOf(terminator, startIndex + 2); | 
| + } | 
| + | 
| + if (endIndex < 0) return bindings; | 
| + | 
| + if (bindings == null) { | 
| + bindings = new _Mustaches(startIndex == 0 && endIndex == length - 2); | 
| + } | 
| + var pathString = text.substring(startIndex + 2, endIndex).trim(); | 
| + bindings.expressions.add(pathString); | 
| + lastIndex = endIndex + 2; | 
| + } | 
| + return bindings; | 
| +} |