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