| 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 /** |
| 6 * Barback transformer that doesn't actually mutate the HTML, but validates that |
| 7 * it correctly uses Polymer constructs. This is mainly used to produce warnings |
| 8 * for feedback in the editor. |
| 9 */ |
| 10 library polymer.src.validator; |
| 11 |
| 12 import 'dart:async'; |
| 13 import 'dart:mirrors'; |
| 14 import 'dart:convert' show JSON; |
| 15 |
| 16 import 'package:barback/barback.dart'; |
| 17 import 'package:html5lib/dom.dart'; |
| 18 import 'package:html5lib/dom_parsing.dart'; |
| 19 |
| 20 import 'transform/common.dart'; |
| 21 |
| 22 typedef String MessageFormatter(String kind, String message, Span span); |
| 23 |
| 24 /** |
| 25 * A validator that checks for common Polymer errors and produces warnings to |
| 26 * show on the editor or the command line. Leaves sources unchanged, but creates |
| 27 * a new asset containing all the warnings. |
| 28 */ |
| 29 // TODO(sigmund): maybe barback should have the concept of a validator too? |
| 30 class Validator extends Transformer { |
| 31 /** Only run on .html files. */ |
| 32 final String allowedExtensions = '.html'; |
| 33 |
| 34 final MessageFormatter _formatter; |
| 35 |
| 36 Validator([this._formatter]); |
| 37 |
| 38 Future apply(Transform transform) { |
| 39 var wrapper = new _LoggerInterceptor(transform, _formatter); |
| 40 var seen = new Set<AssetId>(); |
| 41 var primary = transform.primaryInput; |
| 42 var id = primary.id; |
| 43 wrapper.addOutput(primary); // this phase is analysis only |
| 44 seen.add(id); |
| 45 return readPrimaryAsHtml(wrapper).then((document) { |
| 46 return _collectElements(document, id, wrapper, seen).then((elements) { |
| 47 new _ValidatorVisitor(wrapper, elements).visit(document); |
| 48 var messagesId = id.addExtension('.messages'); |
| 49 wrapper.addOutput(new Asset.fromString(messagesId, |
| 50 wrapper._messages.join('\n'))); |
| 51 }); |
| 52 }); |
| 53 } |
| 54 |
| 55 /** |
| 56 * Collect into [elements] any data about each polymer-element defined in |
| 57 * [document] or any of it's imports, unless they have already been [seen]. |
| 58 * Elements are added in the order they appear, transitive imports are added |
| 59 * first. |
| 60 */ |
| 61 Future<Map<String, _ElementSummary>> _collectElements( |
| 62 Document document, AssetId sourceId, Transform transform, |
| 63 Set<AssetId> seen, [Map<String, _ElementSummary> elements]) { |
| 64 if (elements == null) elements = <String, _ElementSummary>{}; |
| 65 var logger = transform.logger; |
| 66 // Note: the import order is relevant, so we visit in that order. |
| 67 return Future.forEach(_getImportedIds(document, sourceId, logger), (id) { |
| 68 if (seen.contains(id)) return new Future.value(null); |
| 69 seen.add(id); |
| 70 return readAsHtml(id, transform) |
| 71 .then((doc) => _collectElements(doc, id, transform, seen, elements)); |
| 72 }).then((_) { |
| 73 _addElements(document, logger, elements); |
| 74 return elements; |
| 75 }); |
| 76 } |
| 77 |
| 78 List<AssetId> _getImportedIds( |
| 79 Document document, AssetId sourceId, TranformLogger logger) { |
| 80 var importIds = []; |
| 81 for (var tag in document.queryAll('link')) { |
| 82 if (tag.attributes['rel'] != 'import') continue; |
| 83 var href = tag.attributes['href']; |
| 84 var id = resolve(sourceId, href, logger, tag.sourceSpan); |
| 85 if (id == null) continue; |
| 86 importIds.add(id); |
| 87 } |
| 88 return importIds; |
| 89 } |
| 90 |
| 91 void _addElements(Document document, TransformLogger logger, |
| 92 Map<String, _ElementSummary> elements) { |
| 93 for (var tag in document.queryAll('polymer-element')) { |
| 94 var name = tag.attributes['name']; |
| 95 if (name == null) continue; |
| 96 var extendsTag = tag.attributes['extends']; |
| 97 var span = tag.sourceSpan; |
| 98 var existing = elements[name]; |
| 99 if (existing != null) { |
| 100 |
| 101 // Report warning only once. |
| 102 if (existing.hasConflict) continue; |
| 103 existing.hasConflict = true; |
| 104 logger.warning('duplicate definition for custom tag "$name".', |
| 105 existing.span); |
| 106 logger.warning('duplicate definition for custom tag "$name" ' |
| 107 ' (second definition).', span); |
| 108 continue; |
| 109 } |
| 110 |
| 111 elements[name] = new _ElementSummary(name, extendsTag, tag.sourceSpan); |
| 112 } |
| 113 } |
| 114 } |
| 115 |
| 116 /** A proxy of [Transform] that returns a different logger. */ |
| 117 // TODO(sigmund): get rid of this when barback supports a better way to log |
| 118 // messages without printing them. |
| 119 class _LoggerInterceptor implements Transform, TransformLogger { |
| 120 final Transform _original; |
| 121 final List<String> _messages = []; |
| 122 final MessageFormatter _formatter; |
| 123 |
| 124 _LoggerInterceptor(this._original, MessageFormatter formatter) |
| 125 : _formatter = formatter == null ? _defaultFormatter : formatter; |
| 126 |
| 127 TransformLogger get logger => this; |
| 128 |
| 129 noSuchMethod(Invocation m) => reflect(_original).delegate(m); |
| 130 |
| 131 // form TransformLogger: |
| 132 void warning(String message, [Span span]) => _write('warning', message, span); |
| 133 |
| 134 void error(String message, [Span span]) => _write('error', message, span); |
| 135 |
| 136 void _write(String kind, String message, Span span) { |
| 137 _messages.add(_formatter(kind, message, span)); |
| 138 } |
| 139 } |
| 140 |
| 141 /** |
| 142 * Default formatter that generates messages using a format that can be parsed |
| 143 * by tools, such as the Dart Editor, for reporting error messages. |
| 144 */ |
| 145 String _defaultFormatter(String kind, String message, Span span) { |
| 146 return JSON.encode((span == null) |
| 147 ? [{'method': 'warning', 'params': {'message': message}}] |
| 148 : [{'method': kind, |
| 149 'params': { |
| 150 'file': span.sourceUrl, |
| 151 'message': message, |
| 152 'line': span.start.line + 1, |
| 153 'charStart': span.start.offset, |
| 154 'charEnd': span.end.offset, |
| 155 }}]); |
| 156 } |
| 157 |
| 158 |
| 159 /** |
| 160 * Information needed about other polymer-element tags in order to validate |
| 161 * how they are used and extended. |
| 162 */ |
| 163 class _ElementSummary { |
| 164 final String tagName; |
| 165 final String extendsTag; |
| 166 final Span span; |
| 167 |
| 168 _ElementSummary extendsType; |
| 169 bool hasConflict = false; |
| 170 |
| 171 String get baseExtendsTag => extendsType == null |
| 172 ? extendsTag : extendsType.baseExtendsTag; |
| 173 |
| 174 _ElementSummary(this.tagName, this.extendsTag, this.span); |
| 175 |
| 176 String toString() => "($tagName <: $extendsTag)"; |
| 177 } |
| 178 |
| 179 class _ValidatorVisitor extends TreeVisitor { |
| 180 TransformLogger _logger; |
| 181 bool _inPolymerElement = false; |
| 182 Map<String, _ElementSummary> _elements; |
| 183 |
| 184 _ValidatorVisitor(this._logger, this._elements) { |
| 185 // We normalize the map, so each element has a direct reference to any |
| 186 // element it extends from. |
| 187 for (var tag in _elements.values) { |
| 188 var extendsTag = tag.extendsTag; |
| 189 if (extendsTag == null) continue; |
| 190 tag.extendsType = _elements[extendsTag]; |
| 191 } |
| 192 } |
| 193 |
| 194 void visitElement(Element node) { |
| 195 switch (node.tagName) { |
| 196 case 'link': _validateLinkElement(node); break; |
| 197 case 'element': _validateElementElement(node); break; |
| 198 case 'polymer-element': _validatePolymerElement(node); break; |
| 199 case 'script': _validateScriptElement(node); break; |
| 200 default: |
| 201 _validateNormalElement(node); |
| 202 super.visitElement(node); |
| 203 break; |
| 204 } |
| 205 } |
| 206 |
| 207 /** Produce warnings for invalid link-rel tags. */ |
| 208 void _validateLinkElement(Element node) { |
| 209 var rel = node.attributes['rel']; |
| 210 if (rel != 'import' && rel != 'stylesheet') return; |
| 211 |
| 212 var href = node.attributes['href']; |
| 213 if (href != null && href != '') return; |
| 214 |
| 215 // TODO(sigmund): warn also if href can't be resolved. |
| 216 _logger.warning('link rel="$rel" missing href.', node.sourceSpan); |
| 217 } |
| 218 |
| 219 /** Produce warnings if using `<element>` instead of `<polymer-element>`. */ |
| 220 void _validateElementElement(Element node) { |
| 221 _logger.warning('<element> elements are not supported, use' |
| 222 ' <polymer-element> instead', node.sourceSpan); |
| 223 } |
| 224 |
| 225 /** |
| 226 * Produce warnings if using `<polymer-element>` in the wrong place or if the |
| 227 * definition is not complete. |
| 228 */ |
| 229 void _validatePolymerElement(Element node) { |
| 230 if (_inPolymerElement) { |
| 231 _logger.error('Nested polymer element definitions are not allowed.', |
| 232 node.sourceSpan); |
| 233 return; |
| 234 } |
| 235 |
| 236 var tagName = node.attributes['name']; |
| 237 var extendsTag = node.attributes['extends']; |
| 238 |
| 239 if (tagName == null) { |
| 240 _logger.error('Missing tag name of the custom element. Please include an ' |
| 241 'attribute like \'name="your-tag-name"\'.', |
| 242 node.sourceSpan); |
| 243 } else if (!_isCustomTag(tagName)) { |
| 244 _logger.error('Invalid name "$tagName". Custom element names must have ' |
| 245 'at least one dash and can\'t be any of the following names: ' |
| 246 '${_invalidTagNames.keys.join(", ")}.', |
| 247 node.sourceSpan); |
| 248 } |
| 249 |
| 250 if (_elements[extendsTag] == null && _isCustomTag(extendsTag)) { |
| 251 _logger.warning('custom element with name "$extendsTag" not found.', |
| 252 node.sourceSpan); |
| 253 } |
| 254 |
| 255 var oldValue = _inPolymerElement; |
| 256 _inPolymerElement = true; |
| 257 super.visitElement(node); |
| 258 _inPolymerElement = oldValue; |
| 259 } |
| 260 |
| 261 /** |
| 262 * Produces warnings for malformed script tags. In html5 leaving off type= is |
| 263 * fine, but it defaults to text/javascript. Because this might be a common |
| 264 * error, we warn about it when src file ends in .dart, but the type is |
| 265 * incorrect, or when users write code in an inline script tag of a custom |
| 266 * element. |
| 267 * |
| 268 * The hope is that these cases shouldn't break existing valid code, but that |
| 269 * they'll help Polymer authors avoid having their Dart code accidentally |
| 270 * interpreted as JavaScript by the browser. |
| 271 */ |
| 272 void _validateScriptElement(Element node) { |
| 273 var scriptType = node.attributes['type']; |
| 274 var src = node.attributes['src']; |
| 275 |
| 276 if (scriptType == null) { |
| 277 if (src == null && _inPolymerElement) { |
| 278 // TODO(sigmund): revisit this check once we start interop with polymer |
| 279 // elements written in JS. Maybe we need to inspect the contents of the |
| 280 // script to find whether there is an import or something that indicates |
| 281 // that the code is indeed using Dart. |
| 282 _logger.warning('script tag in polymer element with no type will ' |
| 283 'be treated as JavaScript. Did you forget type="application/dart"?', |
| 284 node.sourceSpan); |
| 285 } |
| 286 if (src != null && src.endsWith('.dart')) { |
| 287 _logger.warning('script tag with .dart source file but no type will ' |
| 288 'be treated as JavaScript. Did you forget type="application/dart"?', |
| 289 node.sourceSpan); |
| 290 } |
| 291 return; |
| 292 } |
| 293 |
| 294 if (scriptType != 'application/dart') return; |
| 295 |
| 296 if (src != null) { |
| 297 if (!src.endsWith('.dart')) { |
| 298 _logger.warning('"application/dart" scripts should ' |
| 299 'use the .dart file extension.', |
| 300 node.sourceSpan); |
| 301 } |
| 302 |
| 303 if (node.innerHtml.trim() != '') { |
| 304 _logger.warning('script tag has "src" attribute and also has script ' |
| 305 'text.', node.sourceSpan); |
| 306 } |
| 307 } |
| 308 } |
| 309 |
| 310 /** |
| 311 * Produces warnings for misuses of on-foo event handlers, and for instanting |
| 312 * custom tags incorrectly. |
| 313 */ |
| 314 void _validateNormalElement(Element node) { |
| 315 // Event handlers only allowed inside polymer-elements |
| 316 node.attributes.forEach((name, value) { |
| 317 if (name.startsWith('on')) { |
| 318 _validateEventHandler(node, name, value); |
| 319 } |
| 320 }); |
| 321 |
| 322 // Validate uses of custom-tags |
| 323 var nodeTag = node.tagName; |
| 324 var hasIsAttribute; |
| 325 var customTagName; |
| 326 if (_isCustomTag(nodeTag)) { |
| 327 // <fancy-button> |
| 328 customTagName = nodeTag; |
| 329 hasIsAttribute = false; |
| 330 } else { |
| 331 // <button is="fancy-button"> |
| 332 customTagName = node.attributes['is']; |
| 333 hasIsAttribute = true; |
| 334 } |
| 335 |
| 336 if (customTagName == null || customTagName == 'polymer-element') return; |
| 337 |
| 338 var info = _elements[customTagName]; |
| 339 if (info == null) { |
| 340 _logger.warning('definition for custom element with tag name ' |
| 341 '"$customTagName" not found.', node.sourceSpan); |
| 342 return; |
| 343 } |
| 344 |
| 345 var baseTag = info.baseExtendsTag; |
| 346 if (baseTag != null && !hasIsAttribute) { |
| 347 _logger.warning( |
| 348 'custom element "$customTagName" extends from "$baseTag", but ' |
| 349 'this tag will not include the default properties of "$baseTag". ' |
| 350 'To fix this, either write this tag as <$baseTag ' |
| 351 'is="$customTagName"> or remove the "extends" attribute from ' |
| 352 'the custom element declaration.', node.sourceSpan); |
| 353 return; |
| 354 } |
| 355 |
| 356 if (hasIsAttribute && baseTag == null) { |
| 357 _logger.warning( |
| 358 'custom element "$customTagName" doesn\'t declare any type ' |
| 359 'extensions. To fix this, either rewrite this tag as ' |
| 360 '<$customTagName> or add \'extends="$nodeTag"\' to ' |
| 361 'the custom element declaration.', node.sourceSpan); |
| 362 return; |
| 363 } |
| 364 |
| 365 if (hasIsAttribute && baseTag != nodeTag) { |
| 366 _logger.warning( |
| 367 'custom element "$customTagName" extends from "$baseTag". ' |
| 368 'Did you mean to write <$baseTag is="$customTagName">?', |
| 369 node.sourceSpan); |
| 370 } |
| 371 } |
| 372 |
| 373 /** Validate event handlers are used correctly. */ |
| 374 void _validateEventHandler(Element node, String name, String value) { |
| 375 if (!name.startsWith('on-')) { |
| 376 _logger.warning('Event handler "$name" will be interpreted as an inline' |
| 377 ' JavaScript event handler. Use the form ' |
| 378 'on-event-name="handlerName" if you want a Dart handler ' |
| 379 'that will automatically update the UI based on model changes.', |
| 380 node.sourceSpan); |
| 381 return; |
| 382 } |
| 383 |
| 384 if (!_inPolymerElement) { |
| 385 _logger.warning('Inline event handlers are only supported inside ' |
| 386 'declarations of <polymer-element>.', node.sourceSpan); |
| 387 } |
| 388 |
| 389 if (value.contains('.') || value.contains('(')) { |
| 390 _logger.warning('Invalid event handler body "$value". Declare a method ' |
| 391 'in your custom element "void handlerName(event, detail, target)" ' |
| 392 'and use the form $name="handlerName".', |
| 393 node.sourceSpan); |
| 394 } |
| 395 } |
| 396 } |
| 397 |
| 398 |
| 399 // These names have meaning in SVG or MathML, so they aren't allowed as custom |
| 400 // tags. |
| 401 var _invalidTagNames = const { |
| 402 'annotation-xml': '', |
| 403 'color-profile': '', |
| 404 'font-face': '', |
| 405 'font-face-src': '', |
| 406 'font-face-uri': '', |
| 407 'font-face-format': '', |
| 408 'font-face-name': '', |
| 409 'missing-glyph': '', |
| 410 }; |
| 411 |
| 412 /** |
| 413 * Returns true if this is a valid custom element name. See: |
| 414 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html#dfn
-custom-element-name> |
| 415 */ |
| 416 bool _isCustomTag(String name) { |
| 417 if (name == null || !name.contains('-')) return false; |
| 418 return !_invalidTagNames.containsKey(name); |
| 419 } |
| OLD | NEW |