| OLD | NEW |
| (Empty) |
| 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 | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /// Transfomer that combines multiple dart script tags into a single one. | |
| 6 library polymer.src.build.script_compactor; | |
| 7 | |
| 8 import 'dart:async'; | |
| 9 import 'dart:convert'; | |
| 10 | |
| 11 import 'package:html5lib/dom.dart' show Document, Element, Text; | |
| 12 import 'package:html5lib/dom_parsing.dart'; | |
| 13 import 'package:html5lib/parser.dart' show parseFragment; | |
| 14 import 'package:analyzer/src/generated/ast.dart'; | |
| 15 import 'package:analyzer/src/generated/element.dart' hide Element; | |
| 16 import 'package:analyzer/src/generated/element.dart' as analyzer show Element; | |
| 17 import 'package:barback/barback.dart'; | |
| 18 import 'package:code_transformers/messages/build_logger.dart'; | |
| 19 import 'package:path/path.dart' as path; | |
| 20 import 'package:source_span/source_span.dart'; | |
| 21 import 'package:smoke/codegen/generator.dart'; | |
| 22 import 'package:smoke/codegen/recorder.dart'; | |
| 23 import 'package:code_transformers/resolver.dart'; | |
| 24 import 'package:code_transformers/src/dart_sdk.dart'; | |
| 25 import 'package:template_binding/src/mustache_tokens.dart' show MustacheTokens; | |
| 26 | |
| 27 import 'package:polymer_expressions/expression.dart' as pe; | |
| 28 import 'package:polymer_expressions/parser.dart' as pe; | |
| 29 import 'package:polymer_expressions/visitor.dart' as pe; | |
| 30 | |
| 31 import 'common.dart'; | |
| 32 import 'import_inliner.dart' show ImportInliner; // just for docs. | |
| 33 import 'messages.dart'; | |
| 34 | |
| 35 /// Combines Dart script tags into a single script tag, and creates a new Dart | |
| 36 /// file that calls the main function of each of the original script tags. | |
| 37 /// | |
| 38 /// This transformer assumes that all script tags point to external files. To | |
| 39 /// support script tags with inlined code, use this transformer after running | |
| 40 /// [ImportInliner] on an earlier phase. | |
| 41 /// | |
| 42 /// Internally, this transformer will convert each script tag into an import | |
| 43 /// statement to a library, and then uses `initPolymer` (see polymer.dart) to | |
| 44 /// process `@initMethod` and `@CustomTag` annotations in those libraries. | |
| 45 class ScriptCompactor extends Transformer { | |
| 46 final Resolvers resolvers; | |
| 47 final TransformOptions options; | |
| 48 | |
| 49 ScriptCompactor(this.options, {String sdkDir}) | |
| 50 // TODO(sigmund): consider restoring here a resolver that uses the real | |
| 51 // SDK once the analyzer is lazy and only an resolves what it needs: | |
| 52 //: resolvers = new Resolvers(sdkDir != null ? sdkDir : dartSdkDirectory); | |
| 53 : resolvers = new Resolvers.fromMock({ | |
| 54 // The list of types below is derived from: | |
| 55 // * types we use via our smoke queries, including HtmlElement and | |
| 56 // types from `_typeHandlers` (deserialize.dart) | |
| 57 // * types that are used internally by the resolver (see | |
| 58 // _initializeFrom in resolver.dart). | |
| 59 'dart:core': ''' | |
| 60 library dart.core; | |
| 61 class Object {} | |
| 62 class Function {} | |
| 63 class StackTrace {} | |
| 64 class Symbol {} | |
| 65 class Type {} | |
| 66 | |
| 67 class String extends Object {} | |
| 68 class bool extends Object {} | |
| 69 class num extends Object {} | |
| 70 class int extends num {} | |
| 71 class double extends num {} | |
| 72 class DateTime extends Object {} | |
| 73 class Null extends Object {} | |
| 74 | |
| 75 class Deprecated extends Object { | |
| 76 final String expires; | |
| 77 const Deprecated(this.expires); | |
| 78 } | |
| 79 const Object deprecated = const Deprecated("next release"); | |
| 80 class _Override { const _Override(); } | |
| 81 const Object override = const _Override(); | |
| 82 class _Proxy { const _Proxy(); } | |
| 83 const Object proxy = const _Proxy(); | |
| 84 | |
| 85 class List<V> extends Object {} | |
| 86 class Map<K, V> extends Object {} | |
| 87 ''', | |
| 88 'dart:html': ''' | |
| 89 library dart.html; | |
| 90 class HtmlElement {} | |
| 91 ''', | |
| 92 }); | |
| 93 | |
| 94 | |
| 95 | |
| 96 /// Only run on entry point .html files. | |
| 97 // TODO(nweiz): This should just take an AssetId when barback <0.13.0 support | |
| 98 // is dropped. | |
| 99 Future<bool> isPrimary(idOrAsset) { | |
| 100 var id = idOrAsset is AssetId ? idOrAsset : idOrAsset.id; | |
| 101 return new Future.value(options.isHtmlEntryPoint(id)); | |
| 102 } | |
| 103 | |
| 104 Future apply(Transform transform) => | |
| 105 new _ScriptCompactor(transform, options, resolvers).apply(); | |
| 106 } | |
| 107 | |
| 108 /// Helper class mainly use to flatten the async code. | |
| 109 class _ScriptCompactor extends PolymerTransformer { | |
| 110 final TransformOptions options; | |
| 111 final Transform transform; | |
| 112 final BuildLogger logger; | |
| 113 final AssetId docId; | |
| 114 final AssetId bootstrapId; | |
| 115 | |
| 116 /// HTML document parsed from [docId]. | |
| 117 Document document; | |
| 118 | |
| 119 /// List of ids for each Dart entry script tag (the main tag and any tag | |
| 120 /// included on each custom element definition). | |
| 121 List<AssetId> entryLibraries; | |
| 122 | |
| 123 /// Whether we are using the experimental bootstrap logic. | |
| 124 bool experimentalBootstrap; | |
| 125 | |
| 126 /// Initializers that will register custom tags or invoke `initMethod`s. | |
| 127 final List<_Initializer> initializers = []; | |
| 128 | |
| 129 /// Attributes published on a custom-tag. We make these available via | |
| 130 /// reflection even if @published was not used. | |
| 131 final Map<String, List<String>> publishedAttributes = {}; | |
| 132 | |
| 133 /// Hook needed to access the analyzer within barback transformers. | |
| 134 final Resolvers resolvers; | |
| 135 | |
| 136 /// Resolved types used for analyzing the user's sources and generating code. | |
| 137 _ResolvedTypes types; | |
| 138 | |
| 139 /// The resolver instance associated with a single run of this transformer. | |
| 140 Resolver resolver; | |
| 141 | |
| 142 /// Code generator used to create the static initialization for smoke. | |
| 143 final generator = new SmokeCodeGenerator(); | |
| 144 | |
| 145 _SubExpressionVisitor expressionVisitor; | |
| 146 | |
| 147 _ScriptCompactor(Transform transform, options, this.resolvers) | |
| 148 : transform = transform, | |
| 149 options = options, | |
| 150 logger = new BuildLogger( | |
| 151 transform, convertErrorsToWarnings: !options.releaseMode, | |
| 152 detailsUri: 'http://goo.gl/5HPeuP'), | |
| 153 docId = transform.primaryInput.id, | |
| 154 bootstrapId = transform.primaryInput.id.addExtension('_bootstrap.dart'); | |
| 155 | |
| 156 Future apply() => | |
| 157 _loadDocument() | |
| 158 .then(_loadEntryLibraries) | |
| 159 .then(_processHtml) | |
| 160 .then(_emitNewEntrypoint) | |
| 161 .then((_) { | |
| 162 // Write out the logs collected by our [BuildLogger]. | |
| 163 if (options.injectBuildLogsInOutput) return logger.writeOutput(); | |
| 164 }); | |
| 165 | |
| 166 /// Loads the primary input as an html document. | |
| 167 Future _loadDocument() => | |
| 168 readPrimaryAsHtml(transform, logger).then((doc) { document = doc; }); | |
| 169 | |
| 170 /// Populates [entryLibraries] as a list containing the asset ids of each | |
| 171 /// library loaded on a script tag. The actual work of computing this is done | |
| 172 /// in an earlier phase and emited in the `entrypoint._data` asset. | |
| 173 Future _loadEntryLibraries(_) => | |
| 174 transform.readInputAsString(docId.addExtension('._data')).then((data) { | |
| 175 var map = JSON.decode(data); | |
| 176 experimentalBootstrap = map['experimental_bootstrap']; | |
| 177 entryLibraries = map['script_ids'] | |
| 178 .map((id) => new AssetId.deserialize(id)) | |
| 179 .toList(); | |
| 180 return Future.forEach(entryLibraries, logger.addLogFilesFromAsset); | |
| 181 }); | |
| 182 | |
| 183 /// Removes unnecessary script tags, and identifies the main entry point Dart | |
| 184 /// script tag (if any). | |
| 185 void _processHtml(_) { | |
| 186 for (var tag in document.querySelectorAll('script')) { | |
| 187 var src = tag.attributes['src']; | |
| 188 if (src == 'packages/polymer/boot.js') { | |
| 189 tag.remove(); | |
| 190 continue; | |
| 191 } | |
| 192 if (tag.attributes['type'] == 'application/dart') { | |
| 193 logger.warning(INTERNAL_ERROR_UNEXPECTED_SCRIPT, span: tag.sourceSpan); | |
| 194 } | |
| 195 } | |
| 196 } | |
| 197 | |
| 198 /// Emits the main HTML and Dart bootstrap code for the application. If there | |
| 199 /// were not Dart entry point files, then this simply emits the original HTML. | |
| 200 Future _emitNewEntrypoint(_) { | |
| 201 // If we don't find code, there is nothing to do. | |
| 202 if (entryLibraries.isEmpty) return null; | |
| 203 return _initResolver() | |
| 204 .then(_extractUsesOfMirrors) | |
| 205 .then(_emitFiles) | |
| 206 .whenComplete(() { | |
| 207 if (resolver != null) resolver.release(); | |
| 208 }); | |
| 209 } | |
| 210 | |
| 211 /// Load a resolver that computes information for every library in | |
| 212 /// [entryLibraries], then use it to initialize the [recorder] (for import | |
| 213 /// resolution) and to resolve specific elements (for analyzing the user's | |
| 214 /// code). | |
| 215 Future _initResolver() { | |
| 216 // We include 'polymer.dart' to simplify how we do resolution below. This | |
| 217 // way we can assume polymer is there, even if the user didn't include an | |
| 218 // import to it. If not, the polymer build will fail with an error when | |
| 219 // trying to create _ResolvedTypes below. | |
| 220 var libsToLoad = [new AssetId('polymer', 'lib/polymer.dart')] | |
| 221 ..addAll(entryLibraries); | |
| 222 return resolvers.get(transform, libsToLoad).then((r) { | |
| 223 resolver = r; | |
| 224 types = new _ResolvedTypes(resolver); | |
| 225 }); | |
| 226 } | |
| 227 | |
| 228 /// Inspects the entire program to find out anything that polymer accesses | |
| 229 /// using mirrors and produces static information that can be used to replace | |
| 230 /// the mirror-based loader and the uses of mirrors through the `smoke` | |
| 231 /// package. This includes: | |
| 232 /// | |
| 233 /// * visiting entry-libraries to extract initializers, | |
| 234 /// * visiting polymer-expressions to extract getters and setters, | |
| 235 /// * looking for published fields of custom elements, and | |
| 236 /// * looking for event handlers and callbacks of change notifications. | |
| 237 /// | |
| 238 void _extractUsesOfMirrors(_) { | |
| 239 // Generate getters and setters needed to evaluate polymer expressions, and | |
| 240 // extract information about published attributes. | |
| 241 expressionVisitor = new _SubExpressionVisitor(generator, logger); | |
| 242 new _HtmlExtractor(logger, generator, publishedAttributes, | |
| 243 expressionVisitor).visit(document); | |
| 244 | |
| 245 // Create a recorder that uses analyzer data to feed data to [generator]. | |
| 246 var recorder = new Recorder(generator, | |
| 247 (lib) => resolver.getImportUri(lib, from: bootstrapId).toString()); | |
| 248 | |
| 249 // Process all classes and top-level functions to include initializers, | |
| 250 // register custom elements, and include special fields and methods in | |
| 251 // custom element classes. | |
| 252 var functionsSeen = new Set<FunctionElement>(); | |
| 253 var classesSeen = new Set<ClassElement>(); | |
| 254 for (var id in entryLibraries) { | |
| 255 var lib = resolver.getLibrary(id); | |
| 256 for (var fun in _visibleTopLevelMethodsOf(lib)) { | |
| 257 if (functionsSeen.contains(fun)) continue; | |
| 258 functionsSeen.add(fun); | |
| 259 _processFunction(fun, id); | |
| 260 } | |
| 261 | |
| 262 for (var cls in _visibleClassesOf(lib)) { | |
| 263 if (classesSeen.contains(cls)) continue; | |
| 264 classesSeen.add(cls); | |
| 265 _processClass(cls, id, recorder); | |
| 266 } | |
| 267 } | |
| 268 } | |
| 269 | |
| 270 /// Process a class ([cls]). If it contains an appropriate [CustomTag] | |
| 271 /// annotation, we include an initializer to register this class, and make | |
| 272 /// sure to include everything that might be accessed or queried from them | |
| 273 /// using the smoke package. In particular, polymer uses smoke for the | |
| 274 /// following: | |
| 275 /// * invoke #registerCallback on custom elements classes, if present. | |
| 276 /// * query for methods ending in `*Changed`. | |
| 277 /// * query for methods with the `@ObserveProperty` annotation. | |
| 278 /// * query for non-final properties labeled with `@published`. | |
| 279 /// * read declarations of properties named in the `attributes` attribute. | |
| 280 /// * read/write the value of published properties . | |
| 281 /// * invoke methods in event handlers. | |
| 282 _processClass(ClassElement cls, AssetId id, Recorder recorder) { | |
| 283 if (!_hasPolymerMixin(cls)) return; | |
| 284 | |
| 285 // Check whether the class has a @CustomTag annotation. Typically we expect | |
| 286 // a single @CustomTag, but it's possible to have several. | |
| 287 var tagNames = []; | |
| 288 for (var meta in cls.node.metadata) { | |
| 289 var tagName = _extractTagName(meta, cls); | |
| 290 if (tagName != null) tagNames.add(tagName); | |
| 291 } | |
| 292 | |
| 293 if (cls.isPrivate && tagNames.isNotEmpty) { | |
| 294 var name = tagNames.first; | |
| 295 logger.error(PRIVATE_CUSTOM_TAG.create( | |
| 296 {'name': name, 'class': cls.name}), | |
| 297 span: _spanForNode(cls, cls.node.name)); | |
| 298 return; | |
| 299 } | |
| 300 | |
| 301 // Include #registerCallback if it exists. Note that by default lookupMember | |
| 302 // and query will also add the corresponding getters and setters. | |
| 303 recorder.lookupMember(cls, 'registerCallback'); | |
| 304 | |
| 305 // Include methods that end with *Changed. | |
| 306 recorder.runQuery(cls, new QueryOptions( | |
| 307 includeFields: false, includeProperties: false, | |
| 308 includeInherited: true, includeMethods: true, | |
| 309 includeUpTo: types.htmlElementElement, | |
| 310 matches: (n) => n.endsWith('Changed') && n != 'attributeChanged')); | |
| 311 | |
| 312 // Include methods marked with @ObserveProperty. | |
| 313 recorder.runQuery(cls, new QueryOptions( | |
| 314 includeFields: false, includeProperties: false, | |
| 315 includeInherited: true, includeMethods: true, | |
| 316 includeUpTo: types.htmlElementElement, | |
| 317 withAnnotations: [types.observePropertyElement])); | |
| 318 | |
| 319 // Include @published and @observable properties. | |
| 320 // Symbols in @published are used when resolving bindings on published | |
| 321 // attributes, symbols for @observable are used via path observers when | |
| 322 // implementing *Changed an @ObserveProperty. | |
| 323 // TODO(sigmund): consider including only those symbols mentioned in | |
| 324 // *Changed and @ObserveProperty instead. | |
| 325 recorder.runQuery(cls, new QueryOptions( | |
| 326 includeUpTo: types.htmlElementElement, | |
| 327 withAnnotations: [types.publishedElement, types.observableElement, | |
| 328 types.computedPropertyElement])); | |
| 329 | |
| 330 // Include @ComputedProperty and process their expressions | |
| 331 var computed = []; | |
| 332 recorder.runQuery(cls, new QueryOptions( | |
| 333 includeUpTo: types.htmlElementElement, | |
| 334 withAnnotations: [types.computedPropertyElement]), | |
| 335 results: computed); | |
| 336 _processComputedExpressions(computed); | |
| 337 | |
| 338 for (var tagName in tagNames) { | |
| 339 // Include an initializer that will call Polymer.register | |
| 340 initializers.add(new _CustomTagInitializer(id, tagName, cls.displayName)); | |
| 341 | |
| 342 // Include also properties published via the `attributes` attribute. | |
| 343 var attrs = publishedAttributes[tagName]; | |
| 344 if (attrs == null) continue; | |
| 345 for (var attr in attrs) { | |
| 346 recorder.lookupMember(cls, attr, recursive: true, | |
| 347 includeUpTo: types.htmlElementElement); | |
| 348 } | |
| 349 } | |
| 350 } | |
| 351 | |
| 352 /// Determines if [cls] or a supertype has a mixin of the Polymer class. | |
| 353 bool _hasPolymerMixin(ClassElement cls) { | |
| 354 while (cls != types.htmlElementElement) { | |
| 355 for (var m in cls.mixins) { | |
| 356 if (m.element == types.polymerClassElement) return true; | |
| 357 } | |
| 358 if (cls.supertype == null) return false; | |
| 359 cls = cls.supertype.element; | |
| 360 } | |
| 361 return false; | |
| 362 } | |
| 363 | |
| 364 /// If [meta] is [CustomTag], extract the name associated with the tag. | |
| 365 String _extractTagName(Annotation meta, ClassElement cls) { | |
| 366 if (meta.element != types.customTagConstructor) return null; | |
| 367 return _extractFirstAnnotationArgument(meta, 'CustomTag', cls); | |
| 368 } | |
| 369 | |
| 370 /// Extract the first argument of an annotation and validate that it's type is | |
| 371 /// String. For instance, return "bar" from `@Foo("bar")`. | |
| 372 String _extractFirstAnnotationArgument(Annotation meta, String name, | |
| 373 analyzer.Element context) { | |
| 374 | |
| 375 // Read argument from the AST | |
| 376 var args = meta.arguments.arguments; | |
| 377 if (args == null || args.length == 0) { | |
| 378 logger.warning(MISSING_ANNOTATION_ARGUMENT.create({'name': name}), | |
| 379 span: _spanForNode(context, meta)); | |
| 380 return null; | |
| 381 } | |
| 382 | |
| 383 var lib = context; | |
| 384 while (lib is! LibraryElement) lib = lib.enclosingElement; | |
| 385 var res = resolver.evaluateConstant(lib, args[0]); | |
| 386 if (!res.isValid || res.value.type != types.stringType) { | |
| 387 logger.warning(INVALID_ANNOTATION_ARGUMENT.create({'name': name}), | |
| 388 span: _spanForNode(context, args[0])); | |
| 389 return null; | |
| 390 } | |
| 391 return res.value.stringValue; | |
| 392 } | |
| 393 | |
| 394 /// Adds the top-level [function] as an initalizer if it's marked with | |
| 395 /// `@initMethod`. | |
| 396 _processFunction(FunctionElement function, AssetId id) { | |
| 397 bool initMethodFound = false; | |
| 398 for (var meta in function.metadata) { | |
| 399 var e = meta.element; | |
| 400 if (e is PropertyAccessorElement && | |
| 401 e.variable == types.initMethodElement) { | |
| 402 initMethodFound = true; | |
| 403 break; | |
| 404 } | |
| 405 } | |
| 406 if (!initMethodFound) return; | |
| 407 if (function.isPrivate) { | |
| 408 logger.error(PRIVATE_INIT_METHOD.create({'name': function.displayName}), | |
| 409 span: _spanForNode(function, function.node.name)); | |
| 410 return; | |
| 411 } | |
| 412 initializers.add(new _InitMethodInitializer(id, function.displayName)); | |
| 413 } | |
| 414 | |
| 415 /// Process members that are annotated with `@ComputedProperty` and records | |
| 416 /// the accessors of their expressions. | |
| 417 _processComputedExpressions(List<analyzer.Element> computed) { | |
| 418 var constructor = types.computedPropertyElement.constructors.first; | |
| 419 for (var member in computed) { | |
| 420 for (var meta in member.node.metadata) { | |
| 421 if (meta.element != constructor) continue; | |
| 422 var expr = _extractFirstAnnotationArgument( | |
| 423 meta, 'ComputedProperty', member); | |
| 424 if (expr == null) continue; | |
| 425 expressionVisitor.run(pe.parse(expr), true, | |
| 426 _spanForNode(member.enclosingElement, meta.arguments.arguments[0])); | |
| 427 } | |
| 428 } | |
| 429 } | |
| 430 | |
| 431 /// Writes the final output for the bootstrap Dart file and entrypoint HTML | |
| 432 /// file. | |
| 433 void _emitFiles(_) { | |
| 434 StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER); | |
| 435 Map<AssetId, String> prefixes = {}; | |
| 436 int i = 0; | |
| 437 for (var id in entryLibraries) { | |
| 438 var url = assetUrlFor(id, bootstrapId, logger); | |
| 439 if (url == null) continue; | |
| 440 code.writeln("import '$url' as i$i;"); | |
| 441 if (options.injectBuildLogsInOutput) { | |
| 442 code.writeln("import 'package:polymer/src/build/log_injector.dart';"); | |
| 443 } | |
| 444 prefixes[id] = 'i$i'; | |
| 445 i++; | |
| 446 } | |
| 447 | |
| 448 // Include smoke initialization. | |
| 449 generator.writeImports(code); | |
| 450 generator.writeTopLevelDeclarations(code); | |
| 451 code.writeln('\nvoid main() {'); | |
| 452 code.write(' useGeneratedCode('); | |
| 453 generator.writeStaticConfiguration(code); | |
| 454 code.writeln(');'); | |
| 455 | |
| 456 if (options.injectBuildLogsInOutput) { | |
| 457 var buildUrl = "${path.basename(docId.path)}$LOG_EXTENSION"; | |
| 458 code.writeln(" new LogInjector().injectLogsFromUrl('$buildUrl');"); | |
| 459 } | |
| 460 | |
| 461 if (experimentalBootstrap) { | |
| 462 code.write(' startPolymer(['); | |
| 463 } else { | |
| 464 code.write(' configureForDeployment(['); | |
| 465 } | |
| 466 | |
| 467 // Include initializers to switch from mirrors_loader to static_loader. | |
| 468 if (!initializers.isEmpty) { | |
| 469 code.writeln(); | |
| 470 for (var init in initializers) { | |
| 471 var initCode = init.asCode(prefixes[init.assetId]); | |
| 472 code.write(" $initCode,\n"); | |
| 473 } | |
| 474 code.writeln(' ]);'); | |
| 475 } else { | |
| 476 if (experimentalBootstrap) logger.warning(NO_INITIALIZATION); | |
| 477 code.writeln(']);'); | |
| 478 } | |
| 479 if (!experimentalBootstrap) { | |
| 480 code.writeln(' i${entryLibraries.length - 1}.main();'); | |
| 481 } | |
| 482 | |
| 483 // End of main(). | |
| 484 code.writeln('}'); | |
| 485 transform.addOutput(new Asset.fromString(bootstrapId, code.toString())); | |
| 486 | |
| 487 | |
| 488 // Emit the bootstrap .dart file | |
| 489 var srcUrl = path.url.basename(bootstrapId.path); | |
| 490 document.body.nodes.add(parseFragment( | |
| 491 '<script type="application/dart" src="$srcUrl"></script>')); | |
| 492 | |
| 493 // Add the styles for the logger widget. | |
| 494 if (options.injectBuildLogsInOutput) { | |
| 495 document.head.append(parseFragment( | |
| 496 '<link rel="stylesheet" type="text/css"' | |
| 497 ' href="packages/polymer/src/build/log_injector.css">')); | |
| 498 } | |
| 499 | |
| 500 transform.addOutput(new Asset.fromString(docId, document.outerHtml)); | |
| 501 } | |
| 502 | |
| 503 _spanForNode(analyzer.Element context, AstNode node) { | |
| 504 var file = resolver.getSourceFile(context); | |
| 505 return file.span(node.offset, node.end); | |
| 506 } | |
| 507 } | |
| 508 | |
| 509 abstract class _Initializer { | |
| 510 AssetId get assetId; | |
| 511 String get symbolName; | |
| 512 String asCode(String prefix); | |
| 513 } | |
| 514 | |
| 515 class _InitMethodInitializer implements _Initializer { | |
| 516 final AssetId assetId; | |
| 517 final String methodName; | |
| 518 String get symbolName => methodName; | |
| 519 _InitMethodInitializer(this.assetId, this.methodName); | |
| 520 | |
| 521 String asCode(String prefix) => "$prefix.$methodName"; | |
| 522 } | |
| 523 | |
| 524 class _CustomTagInitializer implements _Initializer { | |
| 525 final AssetId assetId; | |
| 526 final String tagName; | |
| 527 final String typeName; | |
| 528 String get symbolName => typeName; | |
| 529 _CustomTagInitializer(this.assetId, this.tagName, this.typeName); | |
| 530 | |
| 531 String asCode(String prefix) => | |
| 532 "() => Polymer.register('$tagName', $prefix.$typeName)"; | |
| 533 } | |
| 534 | |
| 535 const MAIN_HEADER = """ | |
| 536 library app_bootstrap; | |
| 537 | |
| 538 import 'package:polymer/polymer.dart'; | |
| 539 """; | |
| 540 | |
| 541 | |
| 542 /// An html visitor that: | |
| 543 /// * finds all polymer expressions and records the getters and setters that | |
| 544 /// will be needed to evaluate them at runtime. | |
| 545 /// * extracts all attributes declared in the `attribute` attributes of | |
| 546 /// polymer elements. | |
| 547 class _HtmlExtractor extends TreeVisitor { | |
| 548 final Map<String, List<String>> publishedAttributes; | |
| 549 final SmokeCodeGenerator generator; | |
| 550 final _SubExpressionVisitor expressionVisitor; | |
| 551 final BuildLogger logger; | |
| 552 bool _inTemplate = false; | |
| 553 | |
| 554 _HtmlExtractor(this.logger, this.generator, this.publishedAttributes, | |
| 555 this.expressionVisitor); | |
| 556 | |
| 557 void visitElement(Element node) { | |
| 558 if (_inTemplate) _processNormalElement(node); | |
| 559 if (node.localName == 'polymer-element') { | |
| 560 _processPolymerElement(node); | |
| 561 _processNormalElement(node); | |
| 562 } | |
| 563 | |
| 564 if (node.localName == 'template') { | |
| 565 var last = _inTemplate; | |
| 566 _inTemplate = true; | |
| 567 super.visitElement(node); | |
| 568 _inTemplate = last; | |
| 569 } else { | |
| 570 super.visitElement(node); | |
| 571 } | |
| 572 } | |
| 573 | |
| 574 void visitText(Text node) { | |
| 575 if (!_inTemplate) return; | |
| 576 var bindings = _Mustaches.parse(node.data); | |
| 577 if (bindings == null) return; | |
| 578 for (var e in bindings.expressions) { | |
| 579 _addExpression(e, false, false, node.sourceSpan); | |
| 580 } | |
| 581 } | |
| 582 | |
| 583 /// Registers getters and setters for all published attributes. | |
| 584 void _processPolymerElement(Element node) { | |
| 585 var tagName = node.attributes['name']; | |
| 586 var value = node.attributes['attributes']; | |
| 587 if (value != null) { | |
| 588 publishedAttributes[tagName] = | |
| 589 value.split(ATTRIBUTES_REGEX).map((a) => a.trim()).toList(); | |
| 590 } | |
| 591 } | |
| 592 | |
| 593 /// Produces warnings for misuses of on-foo event handlers, and for instanting | |
| 594 /// custom tags incorrectly. | |
| 595 void _processNormalElement(Element node) { | |
| 596 var tag = node.localName; | |
| 597 var isCustomTag = isCustomTagName(tag) || node.attributes['is'] != null; | |
| 598 | |
| 599 // Event handlers only allowed inside polymer-elements | |
| 600 node.attributes.forEach((name, value) { | |
| 601 var bindings = _Mustaches.parse(value); | |
| 602 if (bindings == null) return; | |
| 603 var isEvent = false; | |
| 604 var isTwoWay = false; | |
| 605 if (name is String) { | |
| 606 name = name.toLowerCase(); | |
| 607 isEvent = name.startsWith('on-'); | |
| 608 isTwoWay = !isEvent && bindings.isWhole && (isCustomTag || | |
| 609 tag == 'input' && (name == 'value' || name =='checked') || | |
| 610 tag == 'select' && (name == 'selectedindex' || name == 'value') || | |
| 611 tag == 'textarea' && name == 'value'); | |
| 612 } | |
| 613 for (var exp in bindings.expressions) { | |
| 614 _addExpression(exp, isEvent, isTwoWay, node.sourceSpan); | |
| 615 } | |
| 616 }); | |
| 617 } | |
| 618 | |
| 619 void _addExpression(String stringExpression, bool inEvent, bool isTwoWay, | |
| 620 SourceSpan span) { | |
| 621 | |
| 622 if (inEvent) { | |
| 623 if (stringExpression.startsWith('@')) { | |
| 624 logger.warning(AT_EXPRESSION_REMOVED, span: span); | |
| 625 return; | |
| 626 } | |
| 627 | |
| 628 if (stringExpression == '') return; | |
| 629 if (stringExpression.startsWith('_')) { | |
| 630 logger.warning(NO_PRIVATE_EVENT_HANDLERS, span: span); | |
| 631 return; | |
| 632 } | |
| 633 generator.addGetter(stringExpression); | |
| 634 generator.addSymbol(stringExpression); | |
| 635 } | |
| 636 expressionVisitor.run(pe.parse(stringExpression), isTwoWay, span); | |
| 637 } | |
| 638 } | |
| 639 | |
| 640 /// A polymer-expression visitor that records every getter and setter that will | |
| 641 /// be needed to evaluate a single expression at runtime. | |
| 642 class _SubExpressionVisitor extends pe.RecursiveVisitor { | |
| 643 final SmokeCodeGenerator generator; | |
| 644 final BuildLogger logger; | |
| 645 bool _includeSetter; | |
| 646 SourceSpan _currentSpan; | |
| 647 | |
| 648 _SubExpressionVisitor(this.generator, this.logger); | |
| 649 | |
| 650 /// Visit [exp], and record getters and setters that are needed in order to | |
| 651 /// evaluate it at runtime. [includeSetter] is only true if this expression | |
| 652 /// occured in a context where it could be updated, for example in two-way | |
| 653 /// bindings such as `<input value={{exp}}>`. | |
| 654 void run(pe.Expression exp, bool includeSetter, span) { | |
| 655 _currentSpan = span; | |
| 656 _includeSetter = includeSetter; | |
| 657 visit(exp); | |
| 658 } | |
| 659 | |
| 660 /// Adds a getter and symbol for [name], and optionally a setter. | |
| 661 _add(String name) { | |
| 662 if (name.startsWith('_')) { | |
| 663 logger.warning(NO_PRIVATE_SYMBOLS_IN_BINDINGS, span: _currentSpan); | |
| 664 return; | |
| 665 } | |
| 666 generator.addGetter(name); | |
| 667 generator.addSymbol(name); | |
| 668 if (_includeSetter) generator.addSetter(name); | |
| 669 } | |
| 670 | |
| 671 void preVisitExpression(e) { | |
| 672 // For two-way bindings the outermost expression may be updated, so we need | |
| 673 // both the getter and the setter, but we only need the getter for | |
| 674 // subexpressions. We exclude setters as soon as we go deeper in the tree, | |
| 675 // except when we see a filter (that can potentially be a two-way | |
| 676 // transformer). | |
| 677 if (e is pe.BinaryOperator && e.operator == '|') return; | |
| 678 _includeSetter = false; | |
| 679 } | |
| 680 | |
| 681 visitIdentifier(pe.Identifier e) { | |
| 682 if (e.value != 'this') _add(e.value); | |
| 683 super.visitIdentifier(e); | |
| 684 } | |
| 685 | |
| 686 visitGetter(pe.Getter e) { | |
| 687 _add(e.name); | |
| 688 super.visitGetter(e); | |
| 689 } | |
| 690 | |
| 691 visitInvoke(pe.Invoke e) { | |
| 692 _includeSetter = false; // Invoke is only valid as an r-value. | |
| 693 if (e.method != null) _add(e.method); | |
| 694 super.visitInvoke(e); | |
| 695 } | |
| 696 } | |
| 697 | |
| 698 /// Parses and collects information about bindings found in polymer templates. | |
| 699 class _Mustaches { | |
| 700 /// Each expression that appears within `{{...}}` and `[[...]]`. | |
| 701 final List<String> expressions; | |
| 702 | |
| 703 /// Whether the whole text returned by [parse] was a single expression. | |
| 704 final bool isWhole; | |
| 705 | |
| 706 _Mustaches(this.isWhole, this.expressions); | |
| 707 | |
| 708 static _Mustaches parse(String text) { | |
| 709 if (text == null || text.isEmpty) return null; | |
| 710 // Use template-binding's parser, but provide a delegate function factory to | |
| 711 // save the expressions without parsing them as [PropertyPath]s. | |
| 712 var tokens = MustacheTokens.parse(text, (s) => () => s); | |
| 713 if (tokens == null) return null; | |
| 714 var length = tokens.length; | |
| 715 bool isWhole = length == 1 && tokens.getText(length) == '' && | |
| 716 tokens.getText(0) == ''; | |
| 717 var expressions = new List(length); | |
| 718 for (int i = 0; i < length; i++) { | |
| 719 expressions[i] = tokens.getPrepareBinding(i)(); | |
| 720 } | |
| 721 return new _Mustaches(isWhole, expressions); | |
| 722 } | |
| 723 } | |
| 724 | |
| 725 /// Holds types that are used in queries | |
| 726 class _ResolvedTypes { | |
| 727 /// Element representing `HtmlElement`. | |
| 728 final ClassElement htmlElementElement; | |
| 729 | |
| 730 /// Element representing `String`. | |
| 731 final InterfaceType stringType; | |
| 732 | |
| 733 /// Element representing `Polymer`. | |
| 734 final ClassElement polymerClassElement; | |
| 735 | |
| 736 /// Element representing the constructor of `@CustomTag`. | |
| 737 final ConstructorElement customTagConstructor; | |
| 738 | |
| 739 /// Element representing the type of `@published`. | |
| 740 final ClassElement publishedElement; | |
| 741 | |
| 742 /// Element representing the type of `@observable`. | |
| 743 final ClassElement observableElement; | |
| 744 | |
| 745 /// Element representing the type of `@ObserveProperty`. | |
| 746 final ClassElement observePropertyElement; | |
| 747 | |
| 748 /// Element representing the type of `@ComputedProperty`. | |
| 749 final ClassElement computedPropertyElement; | |
| 750 | |
| 751 /// Element representing the `@initMethod` annotation. | |
| 752 final TopLevelVariableElement initMethodElement; | |
| 753 | |
| 754 | |
| 755 factory _ResolvedTypes(Resolver resolver) { | |
| 756 // Load class elements that are used in queries for codegen. | |
| 757 var polymerLib = resolver.getLibrary( | |
| 758 new AssetId('polymer', 'lib/polymer.dart')); | |
| 759 if (polymerLib == null) _definitionError('the polymer library'); | |
| 760 | |
| 761 var htmlLib = resolver.getLibraryByUri(Uri.parse('dart:html')); | |
| 762 if (htmlLib == null) _definitionError('the "dart:html" library'); | |
| 763 | |
| 764 var coreLib = resolver.getLibraryByUri(Uri.parse('dart:core')); | |
| 765 if (coreLib == null) _definitionError('the "dart:core" library'); | |
| 766 | |
| 767 var observeLib = resolver.getLibrary( | |
| 768 new AssetId('observe', 'lib/src/metadata.dart')); | |
| 769 if (observeLib == null) _definitionError('the observe library'); | |
| 770 | |
| 771 var initMethodElement = null; | |
| 772 for (var unit in polymerLib.parts) { | |
| 773 if (unit.uri == 'src/loader.dart') { | |
| 774 initMethodElement = unit.topLevelVariables.firstWhere( | |
| 775 (t) => t.displayName == 'initMethod'); | |
| 776 break; | |
| 777 } | |
| 778 } | |
| 779 var customTagConstructor = | |
| 780 _lookupType(polymerLib, 'CustomTag').constructors.first; | |
| 781 var publishedElement = _lookupType(polymerLib, 'PublishedProperty'); | |
| 782 var observableElement = _lookupType(observeLib, 'ObservableProperty'); | |
| 783 var observePropertyElement = _lookupType(polymerLib, 'ObserveProperty'); | |
| 784 var computedPropertyElement = _lookupType(polymerLib, 'ComputedProperty'); | |
| 785 var polymerClassElement = _lookupType(polymerLib, 'Polymer'); | |
| 786 var htmlElementElement = _lookupType(htmlLib, 'HtmlElement'); | |
| 787 var stringType = _lookupType(coreLib, 'String').type; | |
| 788 if (initMethodElement == null) _definitionError('@initMethod'); | |
| 789 | |
| 790 return new _ResolvedTypes.internal(htmlElementElement, stringType, | |
| 791 polymerClassElement, customTagConstructor, publishedElement, | |
| 792 observableElement, observePropertyElement, computedPropertyElement, | |
| 793 initMethodElement); | |
| 794 } | |
| 795 | |
| 796 _ResolvedTypes.internal(this.htmlElementElement, this.stringType, | |
| 797 this.polymerClassElement, this.customTagConstructor, | |
| 798 this.publishedElement, this.observableElement, | |
| 799 this.observePropertyElement, this.computedPropertyElement, | |
| 800 this.initMethodElement); | |
| 801 | |
| 802 static _lookupType(LibraryElement lib, String typeName) { | |
| 803 var result = lib.getType(typeName); | |
| 804 if (result == null) _definitionError(typeName); | |
| 805 return result; | |
| 806 } | |
| 807 | |
| 808 static _definitionError(name) { | |
| 809 throw new StateError("Internal error in polymer-builder: couldn't find " | |
| 810 "definition of $name."); | |
| 811 } | |
| 812 } | |
| 813 | |
| 814 /// Retrieves all classses that are visible if you were to import [lib]. This | |
| 815 /// includes exported classes from other libraries. | |
| 816 List<ClassElement> _visibleClassesOf(LibraryElement lib) { | |
| 817 var result = []; | |
| 818 result.addAll(lib.units.expand((u) => u.types)); | |
| 819 for (var e in lib.exports) { | |
| 820 var exported = e.exportedLibrary.units.expand((u) => u.types).toList(); | |
| 821 _filter(exported, e.combinators); | |
| 822 result.addAll(exported); | |
| 823 } | |
| 824 return result; | |
| 825 } | |
| 826 | |
| 827 /// Retrieves all top-level methods that are visible if you were to import | |
| 828 /// [lib]. This includes exported methods from other libraries too. | |
| 829 List<FunctionElement> _visibleTopLevelMethodsOf(LibraryElement lib) { | |
| 830 var result = []; | |
| 831 result.addAll(lib.units.expand((u) => u.functions)); | |
| 832 for (var e in lib.exports) { | |
| 833 var exported = e.exportedLibrary.units.expand((u) => u.functions).toList(); | |
| 834 _filter(exported, e.combinators); | |
| 835 result.addAll(exported); | |
| 836 } | |
| 837 return result; | |
| 838 } | |
| 839 | |
| 840 /// Filters [elements] that come from an export, according to its show/hide | |
| 841 /// combinators. This modifies [elements] in place. | |
| 842 void _filter(List<analyzer.Element> elements, | |
| 843 List<NamespaceCombinator> combinators) { | |
| 844 for (var c in combinators) { | |
| 845 if (c is ShowElementCombinator) { | |
| 846 var show = c.shownNames.toSet(); | |
| 847 elements.retainWhere((e) => show.contains(e.displayName)); | |
| 848 } else if (c is HideElementCombinator) { | |
| 849 var hide = c.hiddenNames.toSet(); | |
| 850 elements.removeWhere((e) => hide.contains(e.displayName)); | |
| 851 } | |
| 852 } | |
| 853 } | |
| OLD | NEW |