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