| 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 |
| (...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 88 for (var tag in document.queryAll('link')) { | 88 for (var tag in document.queryAll('link')) { |
| 89 if (tag.attributes['rel'] != 'import') continue; | 89 if (tag.attributes['rel'] != 'import') continue; |
| 90 var href = tag.attributes['href']; | 90 var href = tag.attributes['href']; |
| 91 var span = tag.sourceSpan; | 91 var span = tag.sourceSpan; |
| 92 var id = resolve(sourceId, href, logger, span); | 92 var id = resolve(sourceId, href, logger, span); |
| 93 if (id == null) continue; | 93 if (id == null) continue; |
| 94 importIds.add(assetExists(id, transform).then((exists) { | 94 importIds.add(assetExists(id, transform).then((exists) { |
| 95 if (exists) return id; | 95 if (exists) return id; |
| 96 if (sourceId == transform.primaryInput.id) { | 96 if (sourceId == transform.primaryInput.id) { |
| 97 logger.error('couldn\'t find imported asset "${id.path}" in package ' | 97 logger.error('couldn\'t find imported asset "${id.path}" in package ' |
| 98 '"${id.package}".', span); | 98 '"${id.package}".', span: span); |
| 99 } | 99 } |
| 100 })); | 100 })); |
| 101 } | 101 } |
| 102 return Future.wait(importIds); | 102 return Future.wait(importIds); |
| 103 } | 103 } |
| 104 | 104 |
| 105 void _addElements(Document document, TransformLogger logger, | 105 void _addElements(Document document, TransformLogger logger, |
| 106 Map<String, _ElementSummary> elements) { | 106 Map<String, _ElementSummary> elements) { |
| 107 for (var tag in document.queryAll('polymer-element')) { | 107 for (var tag in document.queryAll('polymer-element')) { |
| 108 var name = tag.attributes['name']; | 108 var name = tag.attributes['name']; |
| 109 if (name == null) continue; | 109 if (name == null) continue; |
| 110 var extendsTag = tag.attributes['extends']; | 110 var extendsTag = tag.attributes['extends']; |
| 111 var span = tag.sourceSpan; | 111 var span = tag.sourceSpan; |
| 112 var existing = elements[name]; | 112 var existing = elements[name]; |
| 113 if (existing != null) { | 113 if (existing != null) { |
| 114 | 114 |
| 115 // Report warning only once. | 115 // Report warning only once. |
| 116 if (existing.hasConflict) continue; | 116 if (existing.hasConflict) continue; |
| 117 existing.hasConflict = true; | 117 existing.hasConflict = true; |
| 118 logger.warning('duplicate definition for custom tag "$name".', | 118 logger.warning('duplicate definition for custom tag "$name".', |
| 119 existing.span); | 119 span: existing.span); |
| 120 logger.warning('duplicate definition for custom tag "$name" ' | 120 logger.warning('duplicate definition for custom tag "$name" ' |
| 121 ' (second definition).', span); | 121 ' (second definition).', span: span); |
| 122 continue; | 122 continue; |
| 123 } | 123 } |
| 124 | 124 |
| 125 elements[name] = new _ElementSummary(name, extendsTag, tag.sourceSpan); | 125 elements[name] = new _ElementSummary(name, extendsTag, tag.sourceSpan); |
| 126 } | 126 } |
| 127 } | 127 } |
| 128 } | 128 } |
| 129 | 129 |
| 130 /** A proxy of [Transform] that returns a different logger. */ | 130 /** A proxy of [Transform] that returns a different logger. */ |
| 131 // TODO(sigmund): get rid of this when barback supports a better way to log | 131 // TODO(sigmund): get rid of this when barback supports a better way to log |
| 132 // messages without printing them. | 132 // messages without printing them. |
| 133 class _LoggerInterceptor implements Transform, TransformLogger { | 133 class _LoggerInterceptor implements Transform, TransformLogger { |
| 134 final Transform _original; | 134 final Transform _original; |
| 135 final List<String> _messages = []; | 135 final List<String> _messages = []; |
| 136 final MessageFormatter _formatter; | 136 final MessageFormatter _formatter; |
| 137 | 137 |
| 138 _LoggerInterceptor(this._original, MessageFormatter formatter) | 138 _LoggerInterceptor(this._original, MessageFormatter formatter) |
| 139 : _formatter = formatter == null ? consoleFormatter : formatter; | 139 : _formatter = formatter == null ? consoleFormatter : formatter; |
| 140 | 140 |
| 141 TransformLogger get logger => this; | 141 TransformLogger get logger => this; |
| 142 | 142 |
| 143 noSuchMethod(Invocation m) => reflect(_original).delegate(m); | 143 noSuchMethod(Invocation m) => reflect(_original).delegate(m); |
| 144 | 144 |
| 145 // form TransformLogger: | 145 // form TransformLogger: |
| 146 void warning(String message, [Span span]) => _write('warning', message, span); | 146 void warning(String message, {AssetId asset, Span span}) |
| 147 => _write('warning', message, span); |
| 147 | 148 |
| 148 void error(String message, [Span span]) => _write('error', message, span); | 149 void error(String message, {AssetId asset, Span span}) |
| 150 => _write('error', message, span); |
| 149 | 151 |
| 150 void _write(String kind, String message, Span span) { | 152 void _write(String kind, String message, Span span) { |
| 151 _messages.add(_formatter(kind, message, span)); | 153 _messages.add(_formatter(kind, message, span)); |
| 152 } | 154 } |
| 153 } | 155 } |
| 154 | 156 |
| 155 /** | 157 /** |
| 156 * Formatter that generates messages using a format that can be parsed | 158 * Formatter that generates messages using a format that can be parsed |
| 157 * by tools, such as the Dart Editor, for reporting error messages. | 159 * by tools, such as the Dart Editor, for reporting error messages. |
| 158 */ | 160 */ |
| (...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 243 | 245 |
| 244 /** Produce warnings for invalid link-rel tags. */ | 246 /** Produce warnings for invalid link-rel tags. */ |
| 245 void _validateLinkElement(Element node) { | 247 void _validateLinkElement(Element node) { |
| 246 var rel = node.attributes['rel']; | 248 var rel = node.attributes['rel']; |
| 247 if (rel != 'import' && rel != 'stylesheet') return; | 249 if (rel != 'import' && rel != 'stylesheet') return; |
| 248 | 250 |
| 249 var href = node.attributes['href']; | 251 var href = node.attributes['href']; |
| 250 if (href != null && href != '') return; | 252 if (href != null && href != '') return; |
| 251 | 253 |
| 252 // TODO(sigmund): warn also if href can't be resolved. | 254 // TODO(sigmund): warn also if href can't be resolved. |
| 253 _logger.warning('link rel="$rel" missing href.', node.sourceSpan); | 255 _logger.warning('link rel="$rel" missing href.', span: node.sourceSpan); |
| 254 } | 256 } |
| 255 | 257 |
| 256 /** Produce warnings if using `<element>` instead of `<polymer-element>`. */ | 258 /** Produce warnings if using `<element>` instead of `<polymer-element>`. */ |
| 257 void _validateElementElement(Element node) { | 259 void _validateElementElement(Element node) { |
| 258 _logger.warning('<element> elements are not supported, use' | 260 _logger.warning('<element> elements are not supported, use' |
| 259 ' <polymer-element> instead', node.sourceSpan); | 261 ' <polymer-element> instead', span: node.sourceSpan); |
| 260 } | 262 } |
| 261 | 263 |
| 262 /** | 264 /** |
| 263 * Produce warnings if using `<polymer-element>` in the wrong place or if the | 265 * Produce warnings if using `<polymer-element>` in the wrong place or if the |
| 264 * definition is not complete. | 266 * definition is not complete. |
| 265 */ | 267 */ |
| 266 void _validatePolymerElement(Element node) { | 268 void _validatePolymerElement(Element node) { |
| 267 if (_inPolymerElement) { | 269 if (_inPolymerElement) { |
| 268 _logger.error('Nested polymer element definitions are not allowed.', | 270 _logger.error('Nested polymer element definitions are not allowed.', |
| 269 node.sourceSpan); | 271 span: node.sourceSpan); |
| 270 return; | 272 return; |
| 271 } | 273 } |
| 272 | 274 |
| 273 var tagName = node.attributes['name']; | 275 var tagName = node.attributes['name']; |
| 274 var extendsTag = node.attributes['extends']; | 276 var extendsTag = node.attributes['extends']; |
| 275 | 277 |
| 276 if (tagName == null) { | 278 if (tagName == null) { |
| 277 _logger.error('Missing tag name of the custom element. Please include an ' | 279 _logger.error('Missing tag name of the custom element. Please include an ' |
| 278 'attribute like \'name="your-tag-name"\'.', | 280 'attribute like \'name="your-tag-name"\'.', |
| 279 node.sourceSpan); | 281 span: node.sourceSpan); |
| 280 } else if (!_isCustomTag(tagName)) { | 282 } else if (!_isCustomTag(tagName)) { |
| 281 _logger.error('Invalid name "$tagName". Custom element names must have ' | 283 _logger.error('Invalid name "$tagName". Custom element names must have ' |
| 282 'at least one dash and can\'t be any of the following names: ' | 284 'at least one dash and can\'t be any of the following names: ' |
| 283 '${_invalidTagNames.keys.join(", ")}.', | 285 '${_invalidTagNames.keys.join(", ")}.', |
| 284 node.sourceSpan); | 286 span: node.sourceSpan); |
| 285 } | 287 } |
| 286 | 288 |
| 287 if (_elements[extendsTag] == null && _isCustomTag(extendsTag)) { | 289 if (_elements[extendsTag] == null && _isCustomTag(extendsTag)) { |
| 288 _logger.warning('custom element with name "$extendsTag" not found.', | 290 _logger.warning('custom element with name "$extendsTag" not found.', |
| 289 node.sourceSpan); | 291 span: node.sourceSpan); |
| 290 } | 292 } |
| 291 | 293 |
| 292 var attrs = node.attributes['attributes']; | 294 var attrs = node.attributes['attributes']; |
| 293 if (attrs != null) { | 295 if (attrs != null) { |
| 294 var attrsSpan = node.attributeSpans['attributes']; | 296 var attrsSpan = node.attributeSpans['attributes']; |
| 295 | 297 |
| 296 // names='a b c' or names='a,b,c' | 298 // names='a b c' or names='a,b,c' |
| 297 // record each name for publishing | 299 // record each name for publishing |
| 298 for (var attr in attrs.split(attrs.contains(',') ? ',' : ' ')) { | 300 for (var attr in attrs.split(attrs.contains(',') ? ',' : ' ')) { |
| 299 // remove excess ws | 301 // remove excess ws |
| (...skipping 24 matching lines...) Expand all Loading... |
| 324 var src = node.attributes['src']; | 326 var src = node.attributes['src']; |
| 325 | 327 |
| 326 if (scriptType == null) { | 328 if (scriptType == null) { |
| 327 if (src == null && _inPolymerElement) { | 329 if (src == null && _inPolymerElement) { |
| 328 // TODO(sigmund): revisit this check once we start interop with polymer | 330 // TODO(sigmund): revisit this check once we start interop with polymer |
| 329 // elements written in JS. Maybe we need to inspect the contents of the | 331 // elements written in JS. Maybe we need to inspect the contents of the |
| 330 // script to find whether there is an import or something that indicates | 332 // script to find whether there is an import or something that indicates |
| 331 // that the code is indeed using Dart. | 333 // that the code is indeed using Dart. |
| 332 _logger.warning('script tag in polymer element with no type will ' | 334 _logger.warning('script tag in polymer element with no type will ' |
| 333 'be treated as JavaScript. Did you forget type="application/dart"?', | 335 'be treated as JavaScript. Did you forget type="application/dart"?', |
| 334 node.sourceSpan); | 336 span: node.sourceSpan); |
| 335 } | 337 } |
| 336 if (src != null && src.endsWith('.dart')) { | 338 if (src != null && src.endsWith('.dart')) { |
| 337 _logger.warning('script tag with .dart source file but no type will ' | 339 _logger.warning('script tag with .dart source file but no type will ' |
| 338 'be treated as JavaScript. Did you forget type="application/dart"?', | 340 'be treated as JavaScript. Did you forget type="application/dart"?', |
| 339 node.sourceSpan); | 341 span: node.sourceSpan); |
| 340 } | 342 } |
| 341 return; | 343 return; |
| 342 } | 344 } |
| 343 | 345 |
| 344 if (scriptType != 'application/dart') return; | 346 if (scriptType != 'application/dart') return; |
| 345 | 347 |
| 346 if (src != null) { | 348 if (src != null) { |
| 347 if (!src.endsWith('.dart')) { | 349 if (!src.endsWith('.dart')) { |
| 348 _logger.warning('"application/dart" scripts should ' | 350 _logger.warning('"application/dart" scripts should ' |
| 349 'use the .dart file extension.', | 351 'use the .dart file extension.', |
| 350 node.sourceSpan); | 352 span: node.sourceSpan); |
| 351 } | 353 } |
| 352 | 354 |
| 353 if (node.innerHtml.trim() != '') { | 355 if (node.innerHtml.trim() != '') { |
| 354 _logger.warning('script tag has "src" attribute and also has script ' | 356 _logger.warning('script tag has "src" attribute and also has script ' |
| 355 'text.', node.sourceSpan); | 357 'text.', span: node.sourceSpan); |
| 356 } | 358 } |
| 357 } | 359 } |
| 358 } | 360 } |
| 359 | 361 |
| 360 /** | 362 /** |
| 361 * Produces warnings for misuses of on-foo event handlers, and for instanting | 363 * Produces warnings for misuses of on-foo event handlers, and for instanting |
| 362 * custom tags incorrectly. | 364 * custom tags incorrectly. |
| 363 */ | 365 */ |
| 364 void _validateNormalElement(Element node) { | 366 void _validateNormalElement(Element node) { |
| 365 // Event handlers only allowed inside polymer-elements | 367 // Event handlers only allowed inside polymer-elements |
| (...skipping 18 matching lines...) Expand all Loading... |
| 384 } | 386 } |
| 385 | 387 |
| 386 if (customTagName == null || customTagName == 'polymer-element') return; | 388 if (customTagName == null || customTagName == 'polymer-element') return; |
| 387 | 389 |
| 388 var info = _elements[customTagName]; | 390 var info = _elements[customTagName]; |
| 389 if (info == null) { | 391 if (info == null) { |
| 390 // TODO(jmesserly): this warning is wrong if someone is using raw custom | 392 // 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 | 393 // elements. Is there another way we can handle this warning that won't |
| 392 // generate false positives? | 394 // generate false positives? |
| 393 _logger.warning('definition for Polymer element with tag name ' | 395 _logger.warning('definition for Polymer element with tag name ' |
| 394 '"$customTagName" not found.', node.sourceSpan); | 396 '"$customTagName" not found.', span: node.sourceSpan); |
| 395 return; | 397 return; |
| 396 } | 398 } |
| 397 | 399 |
| 398 var baseTag = info.baseExtendsTag; | 400 var baseTag = info.baseExtendsTag; |
| 399 if (baseTag != null && !hasIsAttribute) { | 401 if (baseTag != null && !hasIsAttribute) { |
| 400 _logger.warning( | 402 _logger.warning( |
| 401 'custom element "$customTagName" extends from "$baseTag", but ' | 403 'custom element "$customTagName" extends from "$baseTag", but ' |
| 402 'this tag will not include the default properties of "$baseTag". ' | 404 'this tag will not include the default properties of "$baseTag". ' |
| 403 'To fix this, either write this tag as <$baseTag ' | 405 'To fix this, either write this tag as <$baseTag ' |
| 404 'is="$customTagName"> or remove the "extends" attribute from ' | 406 'is="$customTagName"> or remove the "extends" attribute from ' |
| 405 'the custom element declaration.', node.sourceSpan); | 407 'the custom element declaration.', span: node.sourceSpan); |
| 406 return; | 408 return; |
| 407 } | 409 } |
| 408 | 410 |
| 409 if (hasIsAttribute && baseTag == null) { | 411 if (hasIsAttribute && baseTag == null) { |
| 410 _logger.warning( | 412 _logger.warning( |
| 411 'custom element "$customTagName" doesn\'t declare any type ' | 413 'custom element "$customTagName" doesn\'t declare any type ' |
| 412 'extensions. To fix this, either rewrite this tag as ' | 414 'extensions. To fix this, either rewrite this tag as ' |
| 413 '<$customTagName> or add \'extends="$nodeTag"\' to ' | 415 '<$customTagName> or add \'extends="$nodeTag"\' to ' |
| 414 'the custom element declaration.', node.sourceSpan); | 416 'the custom element declaration.', span: node.sourceSpan); |
| 415 return; | 417 return; |
| 416 } | 418 } |
| 417 | 419 |
| 418 if (hasIsAttribute && baseTag != nodeTag) { | 420 if (hasIsAttribute && baseTag != nodeTag) { |
| 419 _logger.warning( | 421 _logger.warning( |
| 420 'custom element "$customTagName" extends from "$baseTag". ' | 422 'custom element "$customTagName" extends from "$baseTag". ' |
| 421 'Did you mean to write <$baseTag is="$customTagName">?', | 423 'Did you mean to write <$baseTag is="$customTagName">?', |
| 422 node.sourceSpan); | 424 span: node.sourceSpan); |
| 423 } | 425 } |
| 424 } | 426 } |
| 425 | 427 |
| 426 /** | 428 /** |
| 427 * Validate an attribute on a custom-element. Returns true if valid. | 429 * Validate an attribute on a custom-element. Returns true if valid. |
| 428 */ | 430 */ |
| 429 bool _validateCustomAttributeName(String name, FileSpan span) { | 431 bool _validateCustomAttributeName(String name, FileSpan span) { |
| 430 if (name.contains('-')) { | 432 if (name.contains('-')) { |
| 431 var newName = toCamelCase(name); | 433 var newName = toCamelCase(name); |
| 432 _logger.warning('PolymerElement no longer recognizes attribute names with
' | 434 _logger.warning('PolymerElement no longer recognizes attribute names with
' |
| 433 'dashes such as "$name". Use "$newName" or "${newName.toLowerCase()}"
' | 435 'dashes such as "$name". Use "$newName" or "${newName.toLowerCase()}"
' |
| 434 'instead (both forms are equivalent in HTML).', span); | 436 'instead (both forms are equivalent in HTML).', span: span); |
| 435 return false; | 437 return false; |
| 436 } | 438 } |
| 437 return true; | 439 return true; |
| 438 } | 440 } |
| 439 | 441 |
| 440 /** Validate event handlers are used correctly. */ | 442 /** Validate event handlers are used correctly. */ |
| 441 void _validateEventHandler(Element node, String name, String value) { | 443 void _validateEventHandler(Element node, String name, String value) { |
| 442 if (!name.startsWith('on-')) { | 444 if (!name.startsWith('on-')) { |
| 443 _logger.warning('Event handler "$name" will be interpreted as an inline' | 445 _logger.warning('Event handler "$name" will be interpreted as an inline' |
| 444 ' JavaScript event handler. Use the form ' | 446 ' JavaScript event handler. Use the form ' |
| 445 'on-event-name="handlerName" if you want a Dart handler ' | 447 'on-event-name="handlerName" if you want a Dart handler ' |
| 446 'that will automatically update the UI based on model changes.', | 448 'that will automatically update the UI based on model changes.', |
| 447 node.attributeSpans[name]); | 449 span: node.attributeSpans[name]); |
| 448 return; | 450 return; |
| 449 } | 451 } |
| 450 | 452 |
| 451 if (!_inPolymerElement) { | 453 if (!_inPolymerElement) { |
| 452 _logger.warning('Inline event handlers are only supported inside ' | 454 _logger.warning('Inline event handlers are only supported inside ' |
| 453 'declarations of <polymer-element>.', node.attributeSpans[name]); | 455 'declarations of <polymer-element>.', |
| 456 span: node.attributeSpans[name]); |
| 454 } | 457 } |
| 455 | 458 |
| 456 var eventName = name.substring('on-'.length); | 459 var eventName = name.substring('on-'.length); |
| 457 if (eventName.contains('-')) { | 460 if (eventName.contains('-')) { |
| 458 var newEvent = toCamelCase(eventName); | 461 var newEvent = toCamelCase(eventName); |
| 459 _logger.warning('Invalid event name "$name". After the "on-" the event ' | 462 _logger.warning('Invalid event name "$name". After the "on-" the event ' |
| 460 'name should not use dashes. For example use "on-$newEvent" or ' | 463 'name should not use dashes. For example use "on-$newEvent" or ' |
| 461 '"on-${newEvent.toLowerCase()}" (both forms are equivalent in HTML).', | 464 '"on-${newEvent.toLowerCase()}" (both forms are equivalent in HTML).', |
| 462 node.attributeSpans[name]); | 465 span: node.attributeSpans[name]); |
| 463 } | 466 } |
| 464 | 467 |
| 465 if (value.contains('.') || value.contains('(')) { | 468 if (value.contains('.') || value.contains('(')) { |
| 466 _logger.warning('Invalid event handler body "$value". Declare a method ' | 469 _logger.warning('Invalid event handler body "$value". Declare a method ' |
| 467 'in your custom element "void handlerName(event, detail, target)" ' | 470 'in your custom element "void handlerName(event, detail, target)" ' |
| 468 'and use the form $name="handlerName".', | 471 'and use the form $name="handlerName".', |
| 469 node.attributeSpans[name]); | 472 span: node.attributeSpans[name]); |
| 470 } | 473 } |
| 471 } | 474 } |
| 472 } | 475 } |
| 473 | 476 |
| 474 | 477 |
| 475 // These names have meaning in SVG or MathML, so they aren't allowed as custom | 478 // These names have meaning in SVG or MathML, so they aren't allowed as custom |
| 476 // tags. | 479 // tags. |
| 477 var _invalidTagNames = const { | 480 var _invalidTagNames = const { |
| 478 'annotation-xml': '', | 481 'annotation-xml': '', |
| 479 'color-profile': '', | 482 'color-profile': '', |
| (...skipping 10 matching lines...) Expand all Loading... |
| 490 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html#dfn
-custom-element-name> | 493 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html#dfn
-custom-element-name> |
| 491 */ | 494 */ |
| 492 bool _isCustomTag(String name) { | 495 bool _isCustomTag(String name) { |
| 493 if (name == null || !name.contains('-')) return false; | 496 if (name == null || !name.contains('-')) return false; |
| 494 return !_invalidTagNames.containsKey(name); | 497 return !_invalidTagNames.containsKey(name); |
| 495 } | 498 } |
| 496 | 499 |
| 497 final String _RED_COLOR = '\u001b[31m'; | 500 final String _RED_COLOR = '\u001b[31m'; |
| 498 final String _MAGENTA_COLOR = '\u001b[35m'; | 501 final String _MAGENTA_COLOR = '\u001b[35m'; |
| 499 final String _NO_COLOR = '\u001b[0m'; | 502 final String _NO_COLOR = '\u001b[0m'; |
| OLD | NEW |