| 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 /** |
| 6 * Logic to validate that developers are correctly using Polymer constructs. | 6 * Logic to validate that developers are correctly using Polymer constructs. |
| 7 * 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 */ | 8 */ |
| 9 library polymer.src.build.linter; | 9 library polymer.src.build.linter; |
| 10 | 10 |
| 11 import 'dart:io'; | 11 import 'dart:io'; |
| 12 import 'dart:async'; | 12 import 'dart:async'; |
| 13 import 'dart:mirrors'; | 13 import 'dart:mirrors'; |
| 14 import 'dart:convert' show JSON; | 14 import 'dart:convert' show JSON; |
| 15 | 15 |
| 16 import 'package:barback/barback.dart'; | 16 import 'package:barback/barback.dart'; |
| 17 import 'package:html5lib/dom.dart'; | 17 import 'package:html5lib/dom.dart'; |
| 18 import 'package:html5lib/dom_parsing.dart'; | 18 import 'package:html5lib/dom_parsing.dart'; |
| 19 import 'package:source_maps/span.dart'; |
| 19 | 20 |
| 20 import 'common.dart'; | 21 import 'common.dart'; |
| 22 import 'utils.dart'; |
| 21 | 23 |
| 22 typedef String MessageFormatter(String kind, String message, Span span); | 24 typedef String MessageFormatter(String kind, String message, Span span); |
| 23 | 25 |
| 24 /** | 26 /** |
| 25 * A linter that checks for common Polymer errors and produces warnings to | 27 * A linter that checks for common Polymer errors and produces warnings to |
| 26 * show on the editor or the command line. Leaves sources unchanged, but creates | 28 * show on the editor or the command line. Leaves sources unchanged, but creates |
| 27 * a new asset containing all the warnings. | 29 * a new asset containing all the warnings. |
| 28 */ | 30 */ |
| 29 class Linter extends Transformer with PolymerTransformer { | 31 class Linter extends Transformer with PolymerTransformer { |
| 30 final TransformOptions options; | 32 final TransformOptions options; |
| (...skipping 153 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 184 output.write(span.getLocationMessage(message, | 186 output.write(span.getLocationMessage(message, |
| 185 useColors: useColors, | 187 useColors: useColors, |
| 186 color: levelColor)); | 188 color: levelColor)); |
| 187 } | 189 } |
| 188 return output.toString(); | 190 return output.toString(); |
| 189 } | 191 } |
| 190 | 192 |
| 191 /** | 193 /** |
| 192 * Information needed about other polymer-element tags in order to validate | 194 * Information needed about other polymer-element tags in order to validate |
| 193 * how they are used and extended. | 195 * how they are used and extended. |
| 196 * |
| 197 * Note: these are only created for polymer-element, because pure custom |
| 198 * elements don't have a declarative form. |
| 194 */ | 199 */ |
| 195 class _ElementSummary { | 200 class _ElementSummary { |
| 196 final String tagName; | 201 final String tagName; |
| 197 final String extendsTag; | 202 final String extendsTag; |
| 198 final Span span; | 203 final Span span; |
| 199 | 204 |
| 200 _ElementSummary extendsType; | 205 _ElementSummary extendsType; |
| 201 bool hasConflict = false; | 206 bool hasConflict = false; |
| 202 | 207 |
| 203 String get baseExtendsTag => extendsType == null | 208 String get baseExtendsTag => extendsType == null |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 277 'at least one dash and can\'t be any of the following names: ' | 282 'at least one dash and can\'t be any of the following names: ' |
| 278 '${_invalidTagNames.keys.join(", ")}.', | 283 '${_invalidTagNames.keys.join(", ")}.', |
| 279 node.sourceSpan); | 284 node.sourceSpan); |
| 280 } | 285 } |
| 281 | 286 |
| 282 if (_elements[extendsTag] == null && _isCustomTag(extendsTag)) { | 287 if (_elements[extendsTag] == null && _isCustomTag(extendsTag)) { |
| 283 _logger.warning('custom element with name "$extendsTag" not found.', | 288 _logger.warning('custom element with name "$extendsTag" not found.', |
| 284 node.sourceSpan); | 289 node.sourceSpan); |
| 285 } | 290 } |
| 286 | 291 |
| 292 var attrs = node.attributes['attributes']; |
| 293 if (attrs != null) { |
| 294 var attrsSpan = node.attributeSpans['attributes']; |
| 295 |
| 296 // names='a b c' or names='a,b,c' |
| 297 // record each name for publishing |
| 298 for (var attr in attrs.split(attrs.contains(',') ? ',' : ' ')) { |
| 299 // remove excess ws |
| 300 attr = attr.trim(); |
| 301 if (!_validateCustomAttributeName(attr, attrsSpan)) break; |
| 302 } |
| 303 } |
| 304 |
| 287 var oldValue = _inPolymerElement; | 305 var oldValue = _inPolymerElement; |
| 288 _inPolymerElement = true; | 306 _inPolymerElement = true; |
| 289 super.visitElement(node); | 307 super.visitElement(node); |
| 290 _inPolymerElement = oldValue; | 308 _inPolymerElement = oldValue; |
| 291 } | 309 } |
| 292 | 310 |
| 293 /** | 311 /** |
| 294 * Produces warnings for malformed script tags. In html5 leaving off type= is | 312 * Produces warnings for malformed script tags. In html5 leaving off type= is |
| 295 * fine, but it defaults to text/javascript. Because this might be a common | 313 * fine, but it defaults to text/javascript. Because this might be a common |
| 296 * error, we warn about it when src file ends in .dart, but the type is | 314 * error, we warn about it when src file ends in .dart, but the type is |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 362 } else { | 380 } else { |
| 363 // <button is="fancy-button"> | 381 // <button is="fancy-button"> |
| 364 customTagName = node.attributes['is']; | 382 customTagName = node.attributes['is']; |
| 365 hasIsAttribute = true; | 383 hasIsAttribute = true; |
| 366 } | 384 } |
| 367 | 385 |
| 368 if (customTagName == null || customTagName == 'polymer-element') return; | 386 if (customTagName == null || customTagName == 'polymer-element') return; |
| 369 | 387 |
| 370 var info = _elements[customTagName]; | 388 var info = _elements[customTagName]; |
| 371 if (info == null) { | 389 if (info == null) { |
| 372 _logger.warning('definition for custom element with tag name ' | 390 // TODO(jmesserly): this warning is wrong if someone is using raw custom |
| 391 // elements. Is there another way we can handle this warning that won't |
| 392 // generate false positives? |
| 393 _logger.warning('definition for Polymer element with tag name ' |
| 373 '"$customTagName" not found.', node.sourceSpan); | 394 '"$customTagName" not found.', node.sourceSpan); |
| 374 return; | 395 return; |
| 375 } | 396 } |
| 376 | 397 |
| 377 var baseTag = info.baseExtendsTag; | 398 var baseTag = info.baseExtendsTag; |
| 378 if (baseTag != null && !hasIsAttribute) { | 399 if (baseTag != null && !hasIsAttribute) { |
| 379 _logger.warning( | 400 _logger.warning( |
| 380 'custom element "$customTagName" extends from "$baseTag", but ' | 401 'custom element "$customTagName" extends from "$baseTag", but ' |
| 381 'this tag will not include the default properties of "$baseTag". ' | 402 'this tag will not include the default properties of "$baseTag". ' |
| 382 'To fix this, either write this tag as <$baseTag ' | 403 'To fix this, either write this tag as <$baseTag ' |
| 383 'is="$customTagName"> or remove the "extends" attribute from ' | 404 'is="$customTagName"> or remove the "extends" attribute from ' |
| 384 'the custom element declaration.', node.sourceSpan); | 405 'the custom element declaration.', node.sourceSpan); |
| 385 return; | 406 return; |
| 386 } | 407 } |
| 387 | 408 |
| 388 if (hasIsAttribute && baseTag == null) { | 409 if (hasIsAttribute && baseTag == null) { |
| 389 _logger.warning( | 410 _logger.warning( |
| 390 'custom element "$customTagName" doesn\'t declare any type ' | 411 'custom element "$customTagName" doesn\'t declare any type ' |
| 391 'extensions. To fix this, either rewrite this tag as ' | 412 'extensions. To fix this, either rewrite this tag as ' |
| 392 '<$customTagName> or add \'extends="$nodeTag"\' to ' | 413 '<$customTagName> or add \'extends="$nodeTag"\' to ' |
| 393 'the custom element declaration.', node.sourceSpan); | 414 'the custom element declaration.', node.sourceSpan); |
| 394 return; | 415 return; |
| 395 } | 416 } |
| 396 | 417 |
| 397 if (hasIsAttribute && baseTag != nodeTag) { | 418 if (hasIsAttribute && baseTag != nodeTag) { |
| 398 _logger.warning( | 419 _logger.warning( |
| 399 'custom element "$customTagName" extends from "$baseTag". ' | 420 'custom element "$customTagName" extends from "$baseTag". ' |
| 400 'Did you mean to write <$baseTag is="$customTagName">?', | 421 'Did you mean to write <$baseTag is="$customTagName">?', |
| 401 node.sourceSpan); | 422 node.sourceSpan); |
| 402 } | 423 } |
| 403 } | 424 } |
| 404 | 425 |
| 426 /** |
| 427 * Validate an attribute on a custom-element. Returns true if valid. |
| 428 */ |
| 429 bool _validateCustomAttributeName(String name, FileSpan span) { |
| 430 if (name.contains('-')) { |
| 431 var newName = toCamelCase(name); |
| 432 _logger.warning('PolymerElement no longer recognizes attribute names with
' |
| 433 'dashes such as "$name". Use "$newName" or "${newName.toLowerCase()}"
' |
| 434 'instead (both forms are equivalent in HTML).', span); |
| 435 return false; |
| 436 } |
| 437 return true; |
| 438 } |
| 439 |
| 405 /** Validate event handlers are used correctly. */ | 440 /** Validate event handlers are used correctly. */ |
| 406 void _validateEventHandler(Element node, String name, String value) { | 441 void _validateEventHandler(Element node, String name, String value) { |
| 407 if (!name.startsWith('on-')) { | 442 if (!name.startsWith('on-')) { |
| 408 _logger.warning('Event handler "$name" will be interpreted as an inline' | 443 _logger.warning('Event handler "$name" will be interpreted as an inline' |
| 409 ' JavaScript event handler. Use the form ' | 444 ' JavaScript event handler. Use the form ' |
| 410 'on-event-name="handlerName" if you want a Dart handler ' | 445 'on-event-name="handlerName" if you want a Dart handler ' |
| 411 'that will automatically update the UI based on model changes.', | 446 'that will automatically update the UI based on model changes.', |
| 412 node.sourceSpan); | 447 node.attributeSpans[name]); |
| 413 return; | 448 return; |
| 414 } | 449 } |
| 415 | 450 |
| 416 if (!_inPolymerElement) { | 451 if (!_inPolymerElement) { |
| 417 _logger.warning('Inline event handlers are only supported inside ' | 452 _logger.warning('Inline event handlers are only supported inside ' |
| 418 'declarations of <polymer-element>.', node.sourceSpan); | 453 'declarations of <polymer-element>.', node.attributeSpans[name]); |
| 454 } |
| 455 |
| 456 var eventName = name.substring('on-'.length); |
| 457 if (eventName.contains('-')) { |
| 458 var newEvent = toCamelCase(eventName); |
| 459 _logger.warning('Invalid event name "$name". After the "on-" the event ' |
| 460 'name should not use dashes. For example use "on-$newEvent" or ' |
| 461 '"on-${newEvent.toLowerCase()}" (both forms are equivalent in HTML).', |
| 462 node.attributeSpans[name]); |
| 419 } | 463 } |
| 420 | 464 |
| 421 if (value.contains('.') || value.contains('(')) { | 465 if (value.contains('.') || value.contains('(')) { |
| 422 _logger.warning('Invalid event handler body "$value". Declare a method ' | 466 _logger.warning('Invalid event handler body "$value". Declare a method ' |
| 423 'in your custom element "void handlerName(event, detail, target)" ' | 467 'in your custom element "void handlerName(event, detail, target)" ' |
| 424 'and use the form $name="handlerName".', | 468 'and use the form $name="handlerName".', |
| 425 node.sourceSpan); | 469 node.attributeSpans[name]); |
| 426 } | 470 } |
| 427 } | 471 } |
| 428 } | 472 } |
| 429 | 473 |
| 430 | 474 |
| 431 // These names have meaning in SVG or MathML, so they aren't allowed as custom | 475 // These names have meaning in SVG or MathML, so they aren't allowed as custom |
| 432 // tags. | 476 // tags. |
| 433 var _invalidTagNames = const { | 477 var _invalidTagNames = const { |
| 434 'annotation-xml': '', | 478 'annotation-xml': '', |
| 435 'color-profile': '', | 479 'color-profile': '', |
| (...skipping 10 matching lines...) Expand all Loading... |
| 446 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html#dfn
-custom-element-name> | 490 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html#dfn
-custom-element-name> |
| 447 */ | 491 */ |
| 448 bool _isCustomTag(String name) { | 492 bool _isCustomTag(String name) { |
| 449 if (name == null || !name.contains('-')) return false; | 493 if (name == null || !name.contains('-')) return false; |
| 450 return !_invalidTagNames.containsKey(name); | 494 return !_invalidTagNames.containsKey(name); |
| 451 } | 495 } |
| 452 | 496 |
| 453 final String _RED_COLOR = '\u001b[31m'; | 497 final String _RED_COLOR = '\u001b[31m'; |
| 454 final String _MAGENTA_COLOR = '\u001b[35m'; | 498 final String _MAGENTA_COLOR = '\u001b[35m'; |
| 455 final String _NO_COLOR = '\u001b[0m'; | 499 final String _NO_COLOR = '\u001b[0m'; |
| OLD | NEW |