| 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 /** | 5 /// Logic to validate that developers are correctly using Polymer constructs. |
| 6 * Logic to validate that developers are correctly using Polymer constructs. | 6 /// This is mainly used to produce warnings for feedback in the editor. |
| 7 * This is mainly used to produce warnings for feedback in the editor. | |
| 8 */ | |
| 9 library polymer.src.build.linter; | 7 library polymer.src.build.linter; |
| 10 | 8 |
| 11 import 'dart:async'; | 9 import 'dart:async'; |
| 12 | 10 |
| 13 import 'package:barback/barback.dart'; | 11 import 'package:barback/barback.dart'; |
| 14 import 'package:html5lib/dom.dart'; | 12 import 'package:html5lib/dom.dart'; |
| 15 import 'package:html5lib/dom_parsing.dart'; | 13 import 'package:html5lib/dom_parsing.dart'; |
| 16 import 'package:source_maps/span.dart'; | 14 import 'package:source_maps/span.dart'; |
| 17 | 15 |
| 18 import 'common.dart'; | 16 import 'common.dart'; |
| 19 import 'utils.dart'; | 17 import 'utils.dart'; |
| 20 | 18 |
| 21 /** | 19 /// A linter that checks for common Polymer errors and produces warnings to |
| 22 * A linter that checks for common Polymer errors and produces warnings to | 20 /// show on the editor or the command line. Leaves sources unchanged, but |
| 23 * show on the editor or the command line. Leaves sources unchanged, but creates | 21 /// creates a new asset containing all the warnings. |
| 24 * a new asset containing all the warnings. | |
| 25 */ | |
| 26 class Linter extends Transformer with PolymerTransformer { | 22 class Linter extends Transformer with PolymerTransformer { |
| 27 final TransformOptions options; | 23 final TransformOptions options; |
| 28 | 24 |
| 29 /** Only run on .html files. */ | 25 /// Only run on .html files. |
| 30 final String allowedExtensions = '.html'; | 26 final String allowedExtensions = '.html'; |
| 31 | 27 |
| 32 Linter(this.options); | 28 Linter(this.options); |
| 33 | 29 |
| 34 Future apply(Transform transform) { | 30 Future apply(Transform transform) { |
| 35 var seen = new Set<AssetId>(); | 31 var seen = new Set<AssetId>(); |
| 36 var primary = transform.primaryInput; | 32 var primary = transform.primaryInput; |
| 37 var id = primary.id; | 33 var id = primary.id; |
| 38 transform.addOutput(primary); // this phase is analysis only | 34 transform.addOutput(primary); // this phase is analysis only |
| 39 seen.add(id); | 35 seen.add(id); |
| 40 return readPrimaryAsHtml(transform).then((document) { | 36 return readPrimaryAsHtml(transform).then((document) { |
| 41 return _collectElements(document, id, transform, seen).then((elements) { | 37 return _collectElements(document, id, transform, seen).then((elements) { |
| 42 bool isEntrypoint = options.isHtmlEntryPoint(id); | 38 bool isEntrypoint = options.isHtmlEntryPoint(id); |
| 43 new _LinterVisitor(transform.logger, elements, isEntrypoint) | 39 new _LinterVisitor(transform.logger, elements, isEntrypoint) |
| 44 .run(document); | 40 .run(document); |
| 45 }); | 41 }); |
| 46 }); | 42 }); |
| 47 } | 43 } |
| 48 | 44 |
| 49 /** | 45 /// Collect into [elements] any data about each polymer-element defined in |
| 50 * Collect into [elements] any data about each polymer-element defined in | 46 /// [document] or any of it's imports, unless they have already been [seen]. |
| 51 * [document] or any of it's imports, unless they have already been [seen]. | 47 /// Elements are added in the order they appear, transitive imports are added |
| 52 * Elements are added in the order they appear, transitive imports are added | 48 /// first. |
| 53 * first. | |
| 54 */ | |
| 55 Future<Map<String, _ElementSummary>> _collectElements( | 49 Future<Map<String, _ElementSummary>> _collectElements( |
| 56 Document document, AssetId sourceId, Transform transform, | 50 Document document, AssetId sourceId, Transform transform, |
| 57 Set<AssetId> seen, [Map<String, _ElementSummary> elements]) { | 51 Set<AssetId> seen, [Map<String, _ElementSummary> elements]) { |
| 58 if (elements == null) elements = <String, _ElementSummary>{}; | 52 if (elements == null) elements = <String, _ElementSummary>{}; |
| 59 return _getImportedIds(document, sourceId, transform) | 53 return _getImportedIds(document, sourceId, transform) |
| 60 // Note: the import order is relevant, so we visit in that order. | 54 // Note: the import order is relevant, so we visit in that order. |
| 61 .then((ids) => Future.forEach(ids, | 55 .then((ids) => Future.forEach(ids, |
| 62 (id) => _readAndCollectElements(id, transform, seen, elements))) | 56 (id) => _readAndCollectElements(id, transform, seen, elements))) |
| 63 .then((_) => _addElements(document, transform.logger, elements)) | 57 .then((_) => _addElements(document, transform.logger, elements)) |
| 64 .then((_) => elements); | 58 .then((_) => elements); |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 113 ' (second definition).', span: span); | 107 ' (second definition).', span: span); |
| 114 continue; | 108 continue; |
| 115 } | 109 } |
| 116 | 110 |
| 117 elements[name] = new _ElementSummary(name, extendsTag, tag.sourceSpan); | 111 elements[name] = new _ElementSummary(name, extendsTag, tag.sourceSpan); |
| 118 } | 112 } |
| 119 } | 113 } |
| 120 } | 114 } |
| 121 | 115 |
| 122 | 116 |
| 123 /** | 117 /// Information needed about other polymer-element tags in order to validate |
| 124 * Information needed about other polymer-element tags in order to validate | 118 /// how they are used and extended. |
| 125 * how they are used and extended. | 119 /// |
| 126 * | 120 /// Note: these are only created for polymer-element, because pure custom |
| 127 * Note: these are only created for polymer-element, because pure custom | 121 /// elements don't have a declarative form. |
| 128 * elements don't have a declarative form. | |
| 129 */ | |
| 130 class _ElementSummary { | 122 class _ElementSummary { |
| 131 final String tagName; | 123 final String tagName; |
| 132 final String extendsTag; | 124 final String extendsTag; |
| 133 final Span span; | 125 final Span span; |
| 134 | 126 |
| 135 _ElementSummary extendsType; | 127 _ElementSummary extendsType; |
| 136 bool hasConflict = false; | 128 bool hasConflict = false; |
| 137 | 129 |
| 138 String get baseExtendsTag => extendsType == null | 130 String get baseExtendsTag => extendsType == null |
| 139 ? extendsTag : extendsType.baseExtendsTag; | 131 ? extendsTag : extendsType.baseExtendsTag; |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 174 } | 166 } |
| 175 | 167 |
| 176 void run(Document doc) { | 168 void run(Document doc) { |
| 177 visit(doc); | 169 visit(doc); |
| 178 | 170 |
| 179 if (_isEntrypoint && !_dartTagSeen) { | 171 if (_isEntrypoint && !_dartTagSeen) { |
| 180 _logger.error(USE_INIT_DART, span: doc.body.sourceSpan); | 172 _logger.error(USE_INIT_DART, span: doc.body.sourceSpan); |
| 181 } | 173 } |
| 182 } | 174 } |
| 183 | 175 |
| 184 /** Produce warnings for invalid link-rel tags. */ | 176 /// Produce warnings for invalid link-rel tags. |
| 185 void _validateLinkElement(Element node) { | 177 void _validateLinkElement(Element node) { |
| 186 var rel = node.attributes['rel']; | 178 var rel = node.attributes['rel']; |
| 187 if (rel != 'import' && rel != 'stylesheet') return; | 179 if (rel != 'import' && rel != 'stylesheet') return; |
| 188 | 180 |
| 189 if (rel == 'import' && _dartTagSeen) { | 181 if (rel == 'import' && _dartTagSeen) { |
| 190 _logger.warning( | 182 _logger.warning( |
| 191 "Move HTML imports above your Dart script tag.", | 183 "Move HTML imports above your Dart script tag.", |
| 192 span: node.sourceSpan); | 184 span: node.sourceSpan); |
| 193 } | 185 } |
| 194 | 186 |
| 195 var href = node.attributes['href']; | 187 var href = node.attributes['href']; |
| 196 if (href != null && href != '') return; | 188 if (href != null && href != '') return; |
| 197 | 189 |
| 198 // TODO(sigmund): warn also if href can't be resolved. | 190 // TODO(sigmund): warn also if href can't be resolved. |
| 199 _logger.warning('link rel="$rel" missing href.', span: node.sourceSpan); | 191 _logger.warning('link rel="$rel" missing href.', span: node.sourceSpan); |
| 200 } | 192 } |
| 201 | 193 |
| 202 /** Produce warnings if using `<element>` instead of `<polymer-element>`. */ | 194 /// Produce warnings if using `<element>` instead of `<polymer-element>`. |
| 203 void _validateElementElement(Element node) { | 195 void _validateElementElement(Element node) { |
| 204 _logger.warning('<element> elements are not supported, use' | 196 _logger.warning('<element> elements are not supported, use' |
| 205 ' <polymer-element> instead', span: node.sourceSpan); | 197 ' <polymer-element> instead', span: node.sourceSpan); |
| 206 } | 198 } |
| 207 | 199 |
| 208 /** | 200 /// Produce warnings if using `<polymer-element>` in the wrong place or if the |
| 209 * Produce warnings if using `<polymer-element>` in the wrong place or if the | 201 /// definition is not complete. |
| 210 * definition is not complete. | |
| 211 */ | |
| 212 void _validatePolymerElement(Element node) { | 202 void _validatePolymerElement(Element node) { |
| 213 if (_inPolymerElement) { | 203 if (_inPolymerElement) { |
| 214 _logger.error('Nested polymer element definitions are not allowed.', | 204 _logger.error('Nested polymer element definitions are not allowed.', |
| 215 span: node.sourceSpan); | 205 span: node.sourceSpan); |
| 216 return; | 206 return; |
| 217 } | 207 } |
| 218 | 208 |
| 219 var tagName = node.attributes['name']; | 209 var tagName = node.attributes['name']; |
| 220 var extendsTag = node.attributes['extends']; | 210 var extendsTag = node.attributes['extends']; |
| 221 | 211 |
| (...skipping 25 matching lines...) Expand all Loading... |
| 247 if (!_validateCustomAttributeName(attr, attrsSpan)) break; | 237 if (!_validateCustomAttributeName(attr, attrsSpan)) break; |
| 248 } | 238 } |
| 249 } | 239 } |
| 250 | 240 |
| 251 var oldValue = _inPolymerElement; | 241 var oldValue = _inPolymerElement; |
| 252 _inPolymerElement = true; | 242 _inPolymerElement = true; |
| 253 super.visitElement(node); | 243 super.visitElement(node); |
| 254 _inPolymerElement = oldValue; | 244 _inPolymerElement = oldValue; |
| 255 } | 245 } |
| 256 | 246 |
| 257 /** | 247 /// Produces warnings for malformed script tags. In html5 leaving off type= is |
| 258 * Produces warnings for malformed script tags. In html5 leaving off type= is | 248 /// fine, but it defaults to text/javascript. Because this might be a common |
| 259 * fine, but it defaults to text/javascript. Because this might be a common | 249 /// error, we warn about it when src file ends in .dart, but the type is |
| 260 * error, we warn about it when src file ends in .dart, but the type is | 250 /// incorrect, or when users write code in an inline script tag of a custom |
| 261 * incorrect, or when users write code in an inline script tag of a custom | 251 /// element. |
| 262 * element. | 252 /// |
| 263 * | 253 /// The hope is that these cases shouldn't break existing valid code, but that |
| 264 * The hope is that these cases shouldn't break existing valid code, but that | 254 /// they'll help Polymer authors avoid having their Dart code accidentally |
| 265 * they'll help Polymer authors avoid having their Dart code accidentally | 255 /// interpreted as JavaScript by the browser. |
| 266 * interpreted as JavaScript by the browser. | |
| 267 */ | |
| 268 void _validateScriptElement(Element node) { | 256 void _validateScriptElement(Element node) { |
| 269 var scriptType = node.attributes['type']; | 257 var scriptType = node.attributes['type']; |
| 270 var isDart = scriptType == 'application/dart'; | 258 var isDart = scriptType == 'application/dart'; |
| 271 var src = node.attributes['src']; | 259 var src = node.attributes['src']; |
| 272 | 260 |
| 273 if (scriptType == null) { | 261 if (scriptType == null) { |
| 274 if (src == null && _inPolymerElement) { | 262 if (src == null && _inPolymerElement) { |
| 275 // TODO(sigmund): revisit this check once we start interop with polymer | 263 // TODO(sigmund): revisit this check once we start interop with polymer |
| 276 // elements written in JS. Maybe we need to inspect the contents of the | 264 // elements written in JS. Maybe we need to inspect the contents of the |
| 277 // script to find whether there is an import or something that indicates | 265 // script to find whether there is an import or something that indicates |
| (...skipping 29 matching lines...) Expand all Loading... |
| 307 span: node.sourceSpan); | 295 span: node.sourceSpan); |
| 308 return; | 296 return; |
| 309 } | 297 } |
| 310 | 298 |
| 311 if (node.innerHtml.trim() != '') { | 299 if (node.innerHtml.trim() != '') { |
| 312 _logger.warning('script tag has "src" attribute and also has script ' | 300 _logger.warning('script tag has "src" attribute and also has script ' |
| 313 'text.', span: node.sourceSpan); | 301 'text.', span: node.sourceSpan); |
| 314 } | 302 } |
| 315 } | 303 } |
| 316 | 304 |
| 317 /** | 305 /// Produces warnings for misuses of on-foo event handlers, and for instanting |
| 318 * Produces warnings for misuses of on-foo event handlers, and for instanting | 306 /// custom tags incorrectly. |
| 319 * custom tags incorrectly. | |
| 320 */ | |
| 321 void _validateNormalElement(Element node) { | 307 void _validateNormalElement(Element node) { |
| 322 // Event handlers only allowed inside polymer-elements | 308 // Event handlers only allowed inside polymer-elements |
| 323 node.attributes.forEach((name, value) { | 309 node.attributes.forEach((name, value) { |
| 324 if (name is String && name.startsWith('on')) { | 310 if (name is String && name.startsWith('on')) { |
| 325 _validateEventHandler(node, name, value); | 311 _validateEventHandler(node, name, value); |
| 326 } | 312 } |
| 327 }); | 313 }); |
| 328 | 314 |
| 329 // Validate uses of custom-tags | 315 // Validate uses of custom-tags |
| 330 var nodeTag = node.localName; | 316 var nodeTag = node.localName; |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 373 } | 359 } |
| 374 | 360 |
| 375 if (hasIsAttribute && baseTag != nodeTag) { | 361 if (hasIsAttribute && baseTag != nodeTag) { |
| 376 _logger.warning( | 362 _logger.warning( |
| 377 'custom element "$customTagName" extends from "$baseTag". ' | 363 'custom element "$customTagName" extends from "$baseTag". ' |
| 378 'Did you mean to write <$baseTag is="$customTagName">?', | 364 'Did you mean to write <$baseTag is="$customTagName">?', |
| 379 span: node.sourceSpan); | 365 span: node.sourceSpan); |
| 380 } | 366 } |
| 381 } | 367 } |
| 382 | 368 |
| 383 /** | 369 /// Validate an attribute on a custom-element. Returns true if valid. |
| 384 * Validate an attribute on a custom-element. Returns true if valid. | |
| 385 */ | |
| 386 bool _validateCustomAttributeName(String name, FileSpan span) { | 370 bool _validateCustomAttributeName(String name, FileSpan span) { |
| 387 if (name.contains('-')) { | 371 if (name.contains('-')) { |
| 388 var newName = toCamelCase(name); | 372 var newName = toCamelCase(name); |
| 389 _logger.warning('PolymerElement no longer recognizes attribute names with
' | 373 _logger.warning('PolymerElement no longer recognizes attribute names with
' |
| 390 'dashes such as "$name". Use "$newName" or "${newName.toLowerCase()}"
' | 374 'dashes such as "$name". Use "$newName" or "${newName.toLowerCase()}"
' |
| 391 'instead (both forms are equivalent in HTML).', span: span); | 375 'instead (both forms are equivalent in HTML).', span: span); |
| 392 return false; | 376 return false; |
| 393 } | 377 } |
| 394 return true; | 378 return true; |
| 395 } | 379 } |
| 396 | 380 |
| 397 /** Validate event handlers are used correctly. */ | 381 /// Validate event handlers are used correctly. |
| 398 void _validateEventHandler(Element node, String name, String value) { | 382 void _validateEventHandler(Element node, String name, String value) { |
| 399 if (!name.startsWith('on-')) { | 383 if (!name.startsWith('on-')) { |
| 400 _logger.warning('Event handler "$name" will be interpreted as an inline' | 384 _logger.warning('Event handler "$name" will be interpreted as an inline' |
| 401 ' JavaScript event handler. Use the form ' | 385 ' JavaScript event handler. Use the form ' |
| 402 'on-event-name="handlerName" if you want a Dart handler ' | 386 'on-event-name="handlerName" if you want a Dart handler ' |
| 403 'that will automatically update the UI based on model changes.', | 387 'that will automatically update the UI based on model changes.', |
| 404 span: node.attributeSpans[name]); | 388 span: node.attributeSpans[name]); |
| 405 return; | 389 return; |
| 406 } | 390 } |
| 407 | 391 |
| (...skipping 19 matching lines...) Expand all Loading... |
| 427 'annotation-xml': '', | 411 'annotation-xml': '', |
| 428 'color-profile': '', | 412 'color-profile': '', |
| 429 'font-face': '', | 413 'font-face': '', |
| 430 'font-face-src': '', | 414 'font-face-src': '', |
| 431 'font-face-uri': '', | 415 'font-face-uri': '', |
| 432 'font-face-format': '', | 416 'font-face-format': '', |
| 433 'font-face-name': '', | 417 'font-face-name': '', |
| 434 'missing-glyph': '', | 418 'missing-glyph': '', |
| 435 }; | 419 }; |
| 436 | 420 |
| 437 /** | 421 /// Returns true if this is a valid custom element name. See: |
| 438 * Returns true if this is a valid custom element name. See: | 422 /// <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html#df
n-custom-element-name> |
| 439 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html#dfn
-custom-element-name> | |
| 440 */ | |
| 441 bool _isCustomTag(String name) { | 423 bool _isCustomTag(String name) { |
| 442 if (name == null || !name.contains('-')) return false; | 424 if (name == null || !name.contains('-')) return false; |
| 443 return !_invalidTagNames.containsKey(name); | 425 return !_invalidTagNames.containsKey(name); |
| 444 } | 426 } |
| 445 | 427 |
| 446 const String USE_INIT_DART = | 428 const String USE_INIT_DART = |
| 447 'To run a polymer application, you need to call "initPolymer". You can ' | 429 'To run a polymer application, you need to call "initPolymer". You can ' |
| 448 'either include a generic script tag that does this for you:' | 430 'either include a generic script tag that does this for you:' |
| 449 '\'<script type="application/dart">export "package:polymer/init.dart";' | 431 '\'<script type="application/dart">export "package:polymer/init.dart";' |
| 450 '</script>\' or add your own script tag and call that function. ' | 432 '</script>\' or add your own script tag and call that function. ' |
| 451 'Make sure the script tag is placed after all HTML imports.'; | 433 'Make sure the script tag is placed after all HTML imports.'; |
| 452 | 434 |
| 453 const String BOOT_JS_DEPRECATED = | 435 const String BOOT_JS_DEPRECATED = |
| 454 '"boot.js" is now deprecated. Instead, you can initialize your polymer ' | 436 '"boot.js" is now deprecated. Instead, you can initialize your polymer ' |
| 455 'application by calling "initPolymer()" in your main. If you don\'t have a ' | 437 'application by calling "initPolymer()" in your main. If you don\'t have a ' |
| 456 'main, then you can include our generic main by adding the following ' | 438 'main, then you can include our generic main by adding the following ' |
| 457 'script tag to your page: \'<script type="application/dart">export ' | 439 'script tag to your page: \'<script type="application/dart">export ' |
| 458 '"package:polymer/init.dart";</script>\'. Additionally you need to ' | 440 '"package:polymer/init.dart";</script>\'. Additionally you need to ' |
| 459 'include: \'<script src="packages/browser/dart.js"></script>\' in the page ' | 441 'include: \'<script src="packages/browser/dart.js"></script>\' in the page ' |
| 460 'too. Make sure these script tags come after all HTML imports.'; | 442 'too. Make sure these script tags come after all HTML imports.'; |
| OLD | NEW |