| 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.polymer_smoke_generator; | |
| 7 | |
| 8 import 'dart:async'; | |
| 9 | |
| 10 import 'package:html/dom.dart' show Document, Element, Text; | |
| 11 import 'package:html/dom_parsing.dart'; | |
| 12 import 'package:html/parser.dart' show parseFragment; | |
| 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; | |
| 16 import 'package:barback/barback.dart'; | |
| 17 import 'package:code_transformers/messages/build_logger.dart'; | |
| 18 import 'package:code_transformers/assets.dart'; | |
| 19 import 'package:code_transformers/src/dart_sdk.dart' as dart_sdk; | |
| 20 import 'package:path/path.dart' as path; | |
| 21 import 'package:source_span/source_span.dart'; | |
| 22 import 'package:smoke/codegen/generator.dart'; | |
| 23 import 'package:smoke/codegen/recorder.dart'; | |
| 24 import 'package:code_transformers/resolver.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 'package:web_components/build/import_crawler.dart'; | |
| 32 | |
| 33 import 'common.dart'; | |
| 34 import 'messages.dart'; | |
| 35 | |
| 36 /// Method to generate a bootstrap file for Polymer given a [Transform] and a | |
| 37 /// [Resolver]. This can be used inside any transformer to share the [Resolver] | |
| 38 /// with other steps. | |
| 39 Future<Asset> generatePolymerBootstrap(Transform transform, Resolver resolver, | |
| 40 AssetId entryPointId, AssetId bootstrapId, Document document, | |
| 41 TransformOptions options, {AssetId resolveFromId}) { | |
| 42 return new PolymerSmokeGenerator( | |
| 43 transform, resolver, entryPointId, bootstrapId, document, options, | |
| 44 resolveFromId: resolveFromId).apply(); | |
| 45 } | |
| 46 | |
| 47 class PolymerSmokeGeneratorTransformer extends Transformer | |
| 48 with PolymerTransformer { | |
| 49 final Resolvers resolvers; | |
| 50 final TransformOptions options; | |
| 51 | |
| 52 PolymerSmokeGeneratorTransformer(this.options, {String sdkDir}) | |
| 53 // TODO(sigmund): consider restoring here a resolver that uses the real | |
| 54 // SDK once the analyzer is lazy and only an resolves what it needs: | |
| 55 //: resolvers = new Resolvers(sdkDir != null ? sdkDir : dartSdkDirectory); | |
| 56 : resolvers = new Resolvers.fromMock(dart_sdk.mockSdkSources); | |
| 57 | |
| 58 /// Only run on entry point .html files. | |
| 59 bool isPrimary(AssetId id) => options.isHtmlEntryPoint(id); | |
| 60 | |
| 61 Future apply(Transform transform) { | |
| 62 var logger = new BuildLogger(transform, | |
| 63 convertErrorsToWarnings: !options.releaseMode, | |
| 64 detailsUri: 'http://goo.gl/5HPeuP'); | |
| 65 var primaryId = transform.primaryInput.id; | |
| 66 return readPrimaryAsHtml(transform, logger).then((document) { | |
| 67 var script = document.querySelector('script[type="application/dart"]'); | |
| 68 if (script == null) return null; | |
| 69 var entryScriptId = uriToAssetId( | |
| 70 primaryId, script.attributes['src'], logger, script.sourceSpan); | |
| 71 var bootstrapId = primaryId.addExtension('_bootstrap.dart'); | |
| 72 script.attributes['src'] = path.basename(bootstrapId.path); | |
| 73 | |
| 74 return resolvers.get(transform, [entryScriptId]).then((resolver) { | |
| 75 return generatePolymerBootstrap(transform, resolver, entryScriptId, | |
| 76 bootstrapId, document, options).then((bootstrapAsset) { | |
| 77 transform.addOutput(bootstrapAsset); | |
| 78 transform | |
| 79 .addOutput(new Asset.fromString(primaryId, document.outerHtml)); | |
| 80 resolver.release(); | |
| 81 }); | |
| 82 }); | |
| 83 }); | |
| 84 } | |
| 85 } | |
| 86 | |
| 87 /// Class which generates the static smoke configuration for polymer. | |
| 88 // TODO(jakemac): Investigate further turning this into an [InitializerPlugin]. | |
| 89 // The main difficulty is this actually recognizes any class which extends the | |
| 90 // [PolymerElement] class, not just things annotated with [CustomTag]. | |
| 91 class PolymerSmokeGenerator { | |
| 92 final TransformOptions options; | |
| 93 final Transform transform; | |
| 94 final BuildLogger logger; | |
| 95 final AssetId docId; | |
| 96 final AssetId bootstrapId; | |
| 97 | |
| 98 /// Id of the Dart script found in the document (can only be one). | |
| 99 AssetId entryScriptId; | |
| 100 | |
| 101 /// Id of the Dart script to start resolution from. | |
| 102 AssetId resolveFromId; | |
| 103 | |
| 104 /// HTML document parsed from [docId]. | |
| 105 Document document; | |
| 106 | |
| 107 /// Attributes published on a custom-tag. We make these available via | |
| 108 /// reflection even if @published was not used. | |
| 109 final Map<String, List<String>> publishedAttributes = {}; | |
| 110 | |
| 111 /// Resolved types used for analyzing the user's sources and generating code. | |
| 112 _ResolvedTypes types; | |
| 113 | |
| 114 /// The resolver instance associated with a single run of this transformer. | |
| 115 Resolver resolver; | |
| 116 | |
| 117 /// Code generator used to create the static initialization for smoke. | |
| 118 final generator = new SmokeCodeGenerator(); | |
| 119 | |
| 120 _SubExpressionVisitor expressionVisitor; | |
| 121 | |
| 122 PolymerSmokeGenerator(Transform transform, Resolver resolver, | |
| 123 this.entryScriptId, this.bootstrapId, this.document, options, | |
| 124 {this.resolveFromId}) | |
| 125 : transform = transform, | |
| 126 options = options, | |
| 127 logger = new BuildLogger(transform, | |
| 128 convertErrorsToWarnings: !options.releaseMode, | |
| 129 detailsUri: 'http://goo.gl/5HPeuP'), | |
| 130 docId = transform.primaryInput.id, | |
| 131 resolver = resolver { | |
| 132 _ResolvedTypes.logger = logger; | |
| 133 types = new _ResolvedTypes(resolver); | |
| 134 if (resolveFromId == null) resolveFromId = entryScriptId; | |
| 135 } | |
| 136 | |
| 137 Future<Asset> apply() { | |
| 138 return _extractUsesOfMirrors().then((_) { | |
| 139 var bootstrapAsset = _buildBootstrap(); | |
| 140 _modifyDocument(); | |
| 141 | |
| 142 // Write out the logs collected by our [BuildLogger]. | |
| 143 if (options.injectBuildLogsInOutput) { | |
| 144 return logger.writeOutput().then((_) => bootstrapAsset); | |
| 145 } | |
| 146 return bootstrapAsset; | |
| 147 }); | |
| 148 } | |
| 149 | |
| 150 /// Inspects the entire program to find out anything that polymer accesses | |
| 151 /// using mirrors and produces static information that can be used to replace | |
| 152 /// the mirror-based loader and the uses of mirrors through the `smoke` | |
| 153 /// package. This includes: | |
| 154 /// | |
| 155 /// * visiting polymer-expressions to extract getters and setters, | |
| 156 /// * looking for published fields of custom elements, and | |
| 157 /// * looking for event handlers and callbacks of change notifications. | |
| 158 /// | |
| 159 Future _extractUsesOfMirrors() { | |
| 160 // Generate getters and setters needed to evaluate polymer expressions, and | |
| 161 // extract information about published attributes. | |
| 162 expressionVisitor = new _SubExpressionVisitor(generator, logger); | |
| 163 | |
| 164 return new ImportCrawler(transform, transform.primaryInput.id, logger, | |
| 165 primaryDocument: document).crawlImports().then((documentData) { | |
| 166 for (var data in documentData.values) { | |
| 167 new _HtmlExtractor( | |
| 168 logger, generator, publishedAttributes, expressionVisitor) | |
| 169 .visit(data.document); | |
| 170 } | |
| 171 | |
| 172 // Create a recorder that uses analyzer data to feed data to [generator]. | |
| 173 var recorder = new Recorder(generator, | |
| 174 (lib) => resolver.getImportUri(lib, from: bootstrapId).toString()); | |
| 175 | |
| 176 // Process all classes to include special fields and methods in custom | |
| 177 // element classes. | |
| 178 _visitLibraries(resolver.getLibrary(resolveFromId), recorder); | |
| 179 }); | |
| 180 } | |
| 181 | |
| 182 _visitLibraries(LibraryElement library, Recorder recorder, | |
| 183 [Set<LibraryElement> librariesSeen, Set<ClassElement> classesSeen]) { | |
| 184 if (librariesSeen == null) librariesSeen = new Set<LibraryElement>(); | |
| 185 librariesSeen.add(library); | |
| 186 | |
| 187 // Visit all our dependencies. | |
| 188 for (var importedLibrary in _libraryDependencies(library)) { | |
| 189 // Don't include anything from the sdk. | |
| 190 if (importedLibrary.isInSdk) continue; | |
| 191 if (librariesSeen.contains(importedLibrary)) continue; | |
| 192 _visitLibraries(importedLibrary, recorder, librariesSeen, classesSeen); | |
| 193 } | |
| 194 | |
| 195 // After visiting dependencies, then visit classes in this library. | |
| 196 if (classesSeen == null) classesSeen = new Set<ClassElement>(); | |
| 197 var classes = _visibleClassesOf(library); | |
| 198 for (var clazz in classes) { | |
| 199 _processClass(clazz, recorder); | |
| 200 } | |
| 201 } | |
| 202 | |
| 203 Iterable<LibraryElement> _libraryDependencies(LibraryElement library) { | |
| 204 getLibrary(UriReferencedElement element) { | |
| 205 if (element is ImportElement) return element.importedLibrary; | |
| 206 if (element is ExportElement) return element.exportedLibrary; | |
| 207 } | |
| 208 | |
| 209 return (new List.from(library.imports)..addAll(library.exports)) | |
| 210 .map(getLibrary); | |
| 211 } | |
| 212 | |
| 213 /// Process a class ([cls]). If it contains an appropriate [CustomTag] | |
| 214 /// annotation, we make sure to include everything that might be accessed or | |
| 215 /// queried from them using the smoke package. In particular, polymer uses | |
| 216 /// smoke for the following: | |
| 217 /// * invoke #registerCallback on custom elements classes, if present. | |
| 218 /// * query for methods ending in `*Changed`. | |
| 219 /// * query for methods with the `@ObserveProperty` annotation. | |
| 220 /// * query for non-final properties labeled with `@published`. | |
| 221 /// * read declarations of properties named in the `attributes` attribute. | |
| 222 /// * read/write the value of published properties . | |
| 223 /// * invoke methods in event handlers. | |
| 224 _processClass(ClassElement cls, Recorder recorder) { | |
| 225 if (!_hasPolymerMixin(cls)) return; | |
| 226 if (cls.node is! ClassDeclaration) return; | |
| 227 var node = cls.node as ClassDeclaration; | |
| 228 | |
| 229 // Check whether the class has a @CustomTag annotation. Typically we expect | |
| 230 // a single @CustomTag, but it's possible to have several. | |
| 231 var tagNames = []; | |
| 232 for (var meta in node.metadata) { | |
| 233 var tagName = _extractTagName(meta, cls); | |
| 234 if (tagName != null) tagNames.add(tagName); | |
| 235 } | |
| 236 | |
| 237 if (cls.isPrivate && tagNames.isNotEmpty) { | |
| 238 var name = tagNames.first; | |
| 239 logger.error(PRIVATE_CUSTOM_TAG.create({'name': name, 'class': cls.name}), | |
| 240 span: _spanForNode(cls, node.name)); | |
| 241 return; | |
| 242 } | |
| 243 | |
| 244 // Include #registerCallback if it exists. Note that by default lookupMember | |
| 245 // and query will also add the corresponding getters and setters. | |
| 246 recorder.lookupMember(cls, 'registerCallback'); | |
| 247 | |
| 248 // Include methods that end with *Changed. | |
| 249 recorder.runQuery(cls, new QueryOptions( | |
| 250 includeFields: false, | |
| 251 includeProperties: false, | |
| 252 includeInherited: true, | |
| 253 includeMethods: true, | |
| 254 includeUpTo: types.htmlElementElement, | |
| 255 matches: (n) => n.endsWith('Changed') && n != 'attributeChanged')); | |
| 256 | |
| 257 // Include methods marked with @ObserveProperty. | |
| 258 recorder.runQuery(cls, new QueryOptions( | |
| 259 includeFields: false, | |
| 260 includeProperties: false, | |
| 261 includeInherited: true, | |
| 262 includeMethods: true, | |
| 263 includeUpTo: types.htmlElementElement, | |
| 264 withAnnotations: [types.observePropertyElement])); | |
| 265 | |
| 266 // Include @published and @observable properties. | |
| 267 // Symbols in @published are used when resolving bindings on published | |
| 268 // attributes, symbols for @observable are used via path observers when | |
| 269 // implementing *Changed an @ObserveProperty. | |
| 270 // TODO(sigmund): consider including only those symbols mentioned in | |
| 271 // *Changed and @ObserveProperty instead. | |
| 272 recorder.runQuery(cls, new QueryOptions( | |
| 273 includeUpTo: types.htmlElementElement, | |
| 274 withAnnotations: [ | |
| 275 types.publishedElement, | |
| 276 types.observableElement, | |
| 277 types.computedPropertyElement | |
| 278 ])); | |
| 279 | |
| 280 // Include @ComputedProperty and process their expressions | |
| 281 var computed = []; | |
| 282 recorder.runQuery(cls, new QueryOptions( | |
| 283 includeUpTo: types.htmlElementElement, | |
| 284 withAnnotations: [types.computedPropertyElement]), results: computed); | |
| 285 _processComputedExpressions(computed); | |
| 286 | |
| 287 for (var tagName in tagNames) { | |
| 288 // Include also properties published via the `attributes` attribute. | |
| 289 var attrs = publishedAttributes[tagName]; | |
| 290 if (attrs == null) continue; | |
| 291 for (var attr in attrs) { | |
| 292 recorder.lookupMember(cls, attr, | |
| 293 recursive: true, includeUpTo: types.htmlElementElement); | |
| 294 } | |
| 295 } | |
| 296 } | |
| 297 | |
| 298 /// Determines if [cls] or a supertype has a mixin of the Polymer class. | |
| 299 bool _hasPolymerMixin(ClassElement cls) { | |
| 300 while (cls != types.htmlElementElement) { | |
| 301 for (var m in cls.mixins) { | |
| 302 if (m.element == types.polymerClassElement) return true; | |
| 303 } | |
| 304 if (cls.supertype == null) return false; | |
| 305 cls = cls.supertype.element; | |
| 306 } | |
| 307 return false; | |
| 308 } | |
| 309 | |
| 310 /// If [meta] is [CustomTag], extract the name associated with the tag. | |
| 311 String _extractTagName(Annotation meta, ClassElement cls) { | |
| 312 if (meta.element != types.customTagConstructor) return null; | |
| 313 return _extractFirstAnnotationArgument(meta, 'CustomTag', cls); | |
| 314 } | |
| 315 | |
| 316 /// Extract the first argument of an annotation and validate that it's type is | |
| 317 /// String. For instance, return "bar" from `@Foo("bar")`. | |
| 318 String _extractFirstAnnotationArgument( | |
| 319 Annotation meta, String name, analyzer.Element context) { | |
| 320 | |
| 321 // Read argument from the AST | |
| 322 var args = meta.arguments.arguments; | |
| 323 if (args == null || args.length == 0) { | |
| 324 logger.warning(MISSING_ANNOTATION_ARGUMENT.create({'name': name}), | |
| 325 span: _spanForNode(context, meta)); | |
| 326 return null; | |
| 327 } | |
| 328 | |
| 329 var lib = context; | |
| 330 while (lib is! LibraryElement) lib = lib.enclosingElement; | |
| 331 var res = resolver.evaluateConstant(lib, args[0]); | |
| 332 if (!res.isValid || res.value.type != types.stringType) { | |
| 333 logger.warning(INVALID_ANNOTATION_ARGUMENT.create({'name': name}), | |
| 334 span: _spanForNode(context, args[0])); | |
| 335 return null; | |
| 336 } | |
| 337 return res.value.stringValue; | |
| 338 } | |
| 339 | |
| 340 /// Process members that are annotated with `@ComputedProperty` and records | |
| 341 /// the accessors of their expressions. | |
| 342 _processComputedExpressions(List<analyzer.Element> computed) { | |
| 343 var constructor = types.computedPropertyElement.constructors.first; | |
| 344 for (var member in computed) { | |
| 345 for (var meta in member.node.metadata) { | |
| 346 if (meta.element != constructor) continue; | |
| 347 var expr = | |
| 348 _extractFirstAnnotationArgument(meta, 'ComputedProperty', member); | |
| 349 if (expr == null) continue; | |
| 350 expressionVisitor.run(pe.parse(expr), true, | |
| 351 _spanForNode(member.enclosingElement, meta.arguments.arguments[0])); | |
| 352 } | |
| 353 } | |
| 354 } | |
| 355 | |
| 356 // Builds the bootstrap Dart file asset. | |
| 357 Asset _buildBootstrap() { | |
| 358 StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER); | |
| 359 | |
| 360 // TODO(jakemac): Inject this at some other stage. | |
| 361 // https://github.com/dart-lang/polymer-dart/issues/22 | |
| 362 if (options.injectBuildLogsInOutput) { | |
| 363 code.writeln("import 'package:polymer/src/build/log_injector.dart';"); | |
| 364 } | |
| 365 | |
| 366 var entryScriptUrl = assetUrlFor(entryScriptId, bootstrapId, logger); | |
| 367 code.writeln("import '$entryScriptUrl' as i0;"); | |
| 368 | |
| 369 // Include smoke initialization. | |
| 370 generator.writeImports(code); | |
| 371 generator.writeTopLevelDeclarations(code); | |
| 372 code.writeln('\nmain() {'); | |
| 373 code.write(' useGeneratedCode('); | |
| 374 generator.writeStaticConfiguration(code); | |
| 375 code.writeln(');'); | |
| 376 | |
| 377 // TODO(jakemac): Inject this at some other stage. | |
| 378 // https://github.com/dart-lang/polymer-dart/issues/22 | |
| 379 if (options.injectBuildLogsInOutput) { | |
| 380 var buildUrl = "${path.basename(docId.path)}$LOG_EXTENSION"; | |
| 381 code.writeln(" new LogInjector().injectLogsFromUrl('$buildUrl');"); | |
| 382 } | |
| 383 | |
| 384 code.writeln(' configureForDeployment();'); | |
| 385 code.writeln(' return i0.main();'); | |
| 386 | |
| 387 // End of main(). | |
| 388 code.writeln('}'); | |
| 389 return new Asset.fromString(bootstrapId, code.toString()); | |
| 390 } | |
| 391 | |
| 392 // Add the styles for the logger widget. | |
| 393 // TODO(jakemac): Inject this at some other stage. | |
| 394 // https://github.com/dart-lang/polymer-dart/issues/22 | |
| 395 void _modifyDocument() { | |
| 396 if (options.injectBuildLogsInOutput) { | |
| 397 document.head.append(parseFragment( | |
| 398 '<link rel="stylesheet" type="text/css"' | |
| 399 ' href="packages/polymer/src/build/log_injector.css">')); | |
| 400 } | |
| 401 } | |
| 402 | |
| 403 _spanForNode(analyzer.Element context, AstNode node) { | |
| 404 var file = resolver.getSourceFile(context); | |
| 405 return file.span(node.offset, node.end); | |
| 406 } | |
| 407 } | |
| 408 | |
| 409 const MAIN_HEADER = """ | |
| 410 library app_bootstrap; | |
| 411 | |
| 412 import 'package:polymer/polymer.dart'; | |
| 413 """; | |
| 414 | |
| 415 /// An html visitor that: | |
| 416 /// * finds all polymer expressions and records the getters and setters that | |
| 417 /// will be needed to evaluate them at runtime. | |
| 418 /// * extracts all attributes declared in the `attribute` attributes of | |
| 419 /// polymer elements. | |
| 420 class _HtmlExtractor extends TreeVisitor { | |
| 421 final Map<String, List<String>> publishedAttributes; | |
| 422 final SmokeCodeGenerator generator; | |
| 423 final _SubExpressionVisitor expressionVisitor; | |
| 424 final BuildLogger logger; | |
| 425 bool _inTemplate = false; | |
| 426 bool _inPolymerJs = false; | |
| 427 | |
| 428 _HtmlExtractor(this.logger, this.generator, this.publishedAttributes, | |
| 429 this.expressionVisitor); | |
| 430 | |
| 431 void visitElement(Element node) { | |
| 432 if (_inTemplate) _processNormalElement(node); | |
| 433 var lastInPolymerJs = _inPolymerJs; | |
| 434 if (node.localName == 'polymer-element') { | |
| 435 // Detect Polymer JS elements, the current logic is any element with only | |
| 436 // non-Dart script tags. | |
| 437 var scripts = node.querySelectorAll('script'); | |
| 438 _inPolymerJs = scripts.isNotEmpty && | |
| 439 scripts.every((s) => s.attributes['type'] != 'application/dart'); | |
| 440 _processPolymerElement(node); | |
| 441 _processNormalElement(node); | |
| 442 } | |
| 443 | |
| 444 if (node.localName == 'template') { | |
| 445 var last = _inTemplate; | |
| 446 _inTemplate = true; | |
| 447 super.visitElement(node); | |
| 448 _inTemplate = last; | |
| 449 } else { | |
| 450 super.visitElement(node); | |
| 451 } | |
| 452 _inPolymerJs = lastInPolymerJs; | |
| 453 } | |
| 454 | |
| 455 void visitText(Text node) { | |
| 456 // Nothing here applies if inside a polymer js element | |
| 457 if (!_inTemplate || _inPolymerJs) return; | |
| 458 var bindings = _Mustaches.parse(node.data); | |
| 459 if (bindings == null) return; | |
| 460 for (var e in bindings.expressions) { | |
| 461 _addExpression(e, false, false, node.sourceSpan); | |
| 462 } | |
| 463 } | |
| 464 | |
| 465 /// Registers getters and setters for all published attributes. | |
| 466 void _processPolymerElement(Element node) { | |
| 467 // Nothing here applies if inside a polymer js element | |
| 468 if (_inPolymerJs) return; | |
| 469 | |
| 470 var tagName = node.attributes['name']; | |
| 471 var value = node.attributes['attributes']; | |
| 472 if (value != null) { | |
| 473 publishedAttributes[tagName] = | |
| 474 value.split(ATTRIBUTES_REGEX).map((a) => a.trim()).toList(); | |
| 475 } | |
| 476 } | |
| 477 | |
| 478 /// Produces warnings for misuses of on-foo event handlers, and for instanting | |
| 479 /// custom tags incorrectly. | |
| 480 void _processNormalElement(Element node) { | |
| 481 // Nothing here applies if inside a polymer js element | |
| 482 if (_inPolymerJs) return; | |
| 483 | |
| 484 var tag = node.localName; | |
| 485 var isCustomTag = isCustomTagName(tag) || node.attributes['is'] != null; | |
| 486 | |
| 487 // Event handlers only allowed inside polymer-elements | |
| 488 node.attributes.forEach((name, value) { | |
| 489 var bindings = _Mustaches.parse(value); | |
| 490 if (bindings == null) return; | |
| 491 var isEvent = false; | |
| 492 var isTwoWay = false; | |
| 493 if (name is String) { | |
| 494 name = name.toLowerCase(); | |
| 495 isEvent = name.startsWith('on-'); | |
| 496 isTwoWay = !isEvent && | |
| 497 bindings.isWhole && | |
| 498 (isCustomTag || | |
| 499 tag == 'input' && (name == 'value' || name == 'checked') || | |
| 500 tag == 'select' && | |
| 501 (name == 'selectedindex' || name == 'value') || | |
| 502 tag == 'textarea' && name == 'value'); | |
| 503 } | |
| 504 for (var exp in bindings.expressions) { | |
| 505 _addExpression(exp, isEvent, isTwoWay, node.sourceSpan); | |
| 506 } | |
| 507 }); | |
| 508 } | |
| 509 | |
| 510 void _addExpression( | |
| 511 String stringExpression, bool inEvent, bool isTwoWay, SourceSpan span) { | |
| 512 if (inEvent) { | |
| 513 if (stringExpression.startsWith('@')) { | |
| 514 logger.warning(AT_EXPRESSION_REMOVED, span: span); | |
| 515 return; | |
| 516 } | |
| 517 | |
| 518 if (stringExpression == '') return; | |
| 519 if (stringExpression.startsWith('_')) { | |
| 520 logger.warning(NO_PRIVATE_EVENT_HANDLERS, span: span); | |
| 521 return; | |
| 522 } | |
| 523 generator.addGetter(stringExpression); | |
| 524 generator.addSymbol(stringExpression); | |
| 525 } | |
| 526 expressionVisitor.run(pe.parse(stringExpression), isTwoWay, span); | |
| 527 } | |
| 528 } | |
| 529 | |
| 530 /// A polymer-expression visitor that records every getter and setter that will | |
| 531 /// be needed to evaluate a single expression at runtime. | |
| 532 class _SubExpressionVisitor extends pe.RecursiveVisitor { | |
| 533 final SmokeCodeGenerator generator; | |
| 534 final BuildLogger logger; | |
| 535 bool _includeSetter; | |
| 536 SourceSpan _currentSpan; | |
| 537 | |
| 538 _SubExpressionVisitor(this.generator, this.logger); | |
| 539 | |
| 540 /// Visit [exp], and record getters and setters that are needed in order to | |
| 541 /// evaluate it at runtime. [includeSetter] is only true if this expression | |
| 542 /// occured in a context where it could be updated, for example in two-way | |
| 543 /// bindings such as `<input value={{exp}}>`. | |
| 544 void run(pe.Expression exp, bool includeSetter, span) { | |
| 545 _currentSpan = span; | |
| 546 _includeSetter = includeSetter; | |
| 547 visit(exp); | |
| 548 } | |
| 549 | |
| 550 /// Adds a getter and symbol for [name], and optionally a setter. | |
| 551 _add(String name) { | |
| 552 if (name.startsWith('_')) { | |
| 553 logger.warning(NO_PRIVATE_SYMBOLS_IN_BINDINGS, span: _currentSpan); | |
| 554 return; | |
| 555 } | |
| 556 generator.addGetter(name); | |
| 557 generator.addSymbol(name); | |
| 558 if (_includeSetter) generator.addSetter(name); | |
| 559 } | |
| 560 | |
| 561 void preVisitExpression(e) { | |
| 562 // For two-way bindings the outermost expression may be updated, so we need | |
| 563 // both the getter and the setter, but we only need the getter for | |
| 564 // subexpressions. We exclude setters as soon as we go deeper in the tree, | |
| 565 // except when we see a filter (that can potentially be a two-way | |
| 566 // transformer). | |
| 567 if (e is pe.BinaryOperator && e.operator == '|') return; | |
| 568 _includeSetter = false; | |
| 569 } | |
| 570 | |
| 571 visitIdentifier(pe.Identifier e) { | |
| 572 if (e.value != 'this') _add(e.value); | |
| 573 super.visitIdentifier(e); | |
| 574 } | |
| 575 | |
| 576 visitGetter(pe.Getter e) { | |
| 577 _add(e.name); | |
| 578 super.visitGetter(e); | |
| 579 } | |
| 580 | |
| 581 visitInvoke(pe.Invoke e) { | |
| 582 _includeSetter = false; // Invoke is only valid as an r-value. | |
| 583 if (e.method != null) _add(e.method); | |
| 584 super.visitInvoke(e); | |
| 585 } | |
| 586 } | |
| 587 | |
| 588 /// Parses and collects information about bindings found in polymer templates. | |
| 589 class _Mustaches { | |
| 590 /// Each expression that appears within `{{...}}` and `[[...]]`. | |
| 591 final List<String> expressions; | |
| 592 | |
| 593 /// Whether the whole text returned by [parse] was a single expression. | |
| 594 final bool isWhole; | |
| 595 | |
| 596 _Mustaches(this.isWhole, this.expressions); | |
| 597 | |
| 598 static _Mustaches parse(String text) { | |
| 599 if (text == null || text.isEmpty) return null; | |
| 600 // Use template-binding's parser, but provide a delegate function factory to | |
| 601 // save the expressions without parsing them as [PropertyPath]s. | |
| 602 var tokens = MustacheTokens.parse(text, (s) => () => s); | |
| 603 if (tokens == null) return null; | |
| 604 var length = tokens.length; | |
| 605 bool isWhole = | |
| 606 length == 1 && tokens.getText(length) == '' && tokens.getText(0) == ''; | |
| 607 var expressions = new List(length); | |
| 608 for (int i = 0; i < length; i++) { | |
| 609 expressions[i] = tokens.getPrepareBinding(i)(); | |
| 610 } | |
| 611 return new _Mustaches(isWhole, expressions); | |
| 612 } | |
| 613 } | |
| 614 | |
| 615 /// Holds types that are used in queries | |
| 616 class _ResolvedTypes { | |
| 617 /// Element representing `HtmlElement`. | |
| 618 final ClassElement htmlElementElement; | |
| 619 | |
| 620 /// Element representing `String`. | |
| 621 final InterfaceType stringType; | |
| 622 | |
| 623 /// Element representing `Polymer`. | |
| 624 final ClassElement polymerClassElement; | |
| 625 | |
| 626 /// Element representing the constructor of `@CustomTag`. | |
| 627 final ConstructorElement customTagConstructor; | |
| 628 | |
| 629 /// Element representing the type of `@published`. | |
| 630 final ClassElement publishedElement; | |
| 631 | |
| 632 /// Element representing the type of `@observable`. | |
| 633 final ClassElement observableElement; | |
| 634 | |
| 635 /// Element representing the type of `@ObserveProperty`. | |
| 636 final ClassElement observePropertyElement; | |
| 637 | |
| 638 /// Element representing the type of `@ComputedProperty`. | |
| 639 final ClassElement computedPropertyElement; | |
| 640 | |
| 641 /// Logger for reporting errors. | |
| 642 static BuildLogger logger; | |
| 643 | |
| 644 factory _ResolvedTypes(Resolver resolver) { | |
| 645 var coreLib = resolver.getLibraryByUri(Uri.parse('dart:core')); | |
| 646 // coreLib should never be null, its ok to throw if this fails. | |
| 647 var stringType = _lookupType(coreLib, 'String').type; | |
| 648 | |
| 649 // Load class elements that are used in queries for codegen. | |
| 650 var polymerLib = | |
| 651 resolver.getLibrary(new AssetId('polymer', 'lib/polymer.dart')); | |
| 652 if (polymerLib == null) { | |
| 653 _definitionError('polymer'); | |
| 654 return new _ResolvedTypes.internal( | |
| 655 null, stringType, null, null, null, null, null, null); | |
| 656 } | |
| 657 | |
| 658 var htmlLib = resolver.getLibraryByUri(Uri.parse('dart:html')); | |
| 659 var observeLib = | |
| 660 resolver.getLibrary(new AssetId('observe', 'lib/src/metadata.dart')); | |
| 661 | |
| 662 var customTagConstructor = | |
| 663 _lookupType(polymerLib, 'CustomTag').constructors.first; | |
| 664 var publishedElement = _lookupType(polymerLib, 'PublishedProperty'); | |
| 665 var observePropertyElement = _lookupType(polymerLib, 'ObserveProperty'); | |
| 666 var computedPropertyElement = _lookupType(polymerLib, 'ComputedProperty'); | |
| 667 var polymerClassElement = _lookupType(polymerLib, 'Polymer'); | |
| 668 var observableElement = _lookupType(observeLib, 'ObservableProperty'); | |
| 669 var htmlElementElement = _lookupType(htmlLib, 'HtmlElement'); | |
| 670 | |
| 671 return new _ResolvedTypes.internal(htmlElementElement, stringType, | |
| 672 polymerClassElement, customTagConstructor, publishedElement, | |
| 673 observableElement, observePropertyElement, computedPropertyElement); | |
| 674 } | |
| 675 | |
| 676 _ResolvedTypes.internal(this.htmlElementElement, this.stringType, | |
| 677 this.polymerClassElement, this.customTagConstructor, | |
| 678 this.publishedElement, this.observableElement, | |
| 679 this.observePropertyElement, this.computedPropertyElement); | |
| 680 | |
| 681 static _lookupType(LibraryElement lib, String typeName) { | |
| 682 var result = lib.getType(typeName); | |
| 683 if (result == null) _definitionError(typeName); | |
| 684 return result; | |
| 685 } | |
| 686 | |
| 687 static _definitionError(name) { | |
| 688 var message = MISSING_POLYMER_DART; | |
| 689 if (logger != null) { | |
| 690 logger.warning(message); | |
| 691 } else { | |
| 692 throw new StateError(message.snippet); | |
| 693 } | |
| 694 } | |
| 695 } | |
| 696 | |
| 697 /// Retrieves all classes that are visible if you were to import [lib]. This | |
| 698 /// includes exported classes from other libraries. | |
| 699 List<ClassElement> _visibleClassesOf(LibraryElement lib) { | |
| 700 var result = []; | |
| 701 result.addAll(lib.units.expand((u) => u.types)); | |
| 702 for (var e in lib.exports) { | |
| 703 var exported = e.exportedLibrary.units.expand((u) => u.types).toList(); | |
| 704 _filter(exported, e.combinators); | |
| 705 result.addAll(exported); | |
| 706 } | |
| 707 return result; | |
| 708 } | |
| 709 | |
| 710 /// Retrieves all top-level methods that are visible if you were to import | |
| 711 /// [lib]. This includes exported methods from other libraries too. | |
| 712 List<FunctionElement> _visibleTopLevelMethodsOf(LibraryElement lib) { | |
| 713 var result = []; | |
| 714 result.addAll(lib.units.expand((u) => u.functions)); | |
| 715 for (var e in lib.exports) { | |
| 716 var exported = e.exportedLibrary.units.expand((u) => u.functions).toList(); | |
| 717 _filter(exported, e.combinators); | |
| 718 result.addAll(exported); | |
| 719 } | |
| 720 return result; | |
| 721 } | |
| 722 | |
| 723 /// Filters [elements] that come from an export, according to its show/hide | |
| 724 /// combinators. This modifies [elements] in place. | |
| 725 void _filter( | |
| 726 List<analyzer.Element> elements, List<NamespaceCombinator> combinators) { | |
| 727 for (var c in combinators) { | |
| 728 if (c is ShowElementCombinator) { | |
| 729 var show = c.shownNames.toSet(); | |
| 730 elements.retainWhere((e) => show.contains(e.displayName)); | |
| 731 } else if (c is HideElementCombinator) { | |
| 732 var hide = c.hiddenNames.toSet(); | |
| 733 elements.removeWhere((e) => hide.contains(e.displayName)); | |
| 734 } | |
| 735 } | |
| 736 } | |
| OLD | NEW |