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 |