Chromium Code Reviews| 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 | |
|
Jennifer Messerly
2013/09/04 03:13:27
Apologies for any comments that are actually about
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
No problem. Even if they are from the old code, it
| |
| 5 /** | |
| 6 * Barback transformer that doesn't actually mutate the HTML, but validates that | |
|
Jennifer Messerly
2013/09/04 03:13:27
"transformer" ... you keep using that word, I do n
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
I think I'm lost :-/?
Jennifer Messerly
2013/09/04 19:21:34
Sorry that was too cute on my part :). What I mean
| |
| 7 * it correctly uses polymer constructs. This is mainly used to produce warnings | |
| 8 * for feedback in the editor. | |
| 9 */ | |
| 10 library validator; | |
|
Jennifer Messerly
2013/09/04 03:13:27
polymer.src.linter?
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done. (polymer.src.validator for now until we deci
| |
| 11 | |
| 12 import 'dart:async'; | |
| 13 import 'dart:mirrors'; | |
| 14 import 'dart:convert' show JSON; | |
| 15 | |
| 16 import 'package:barback/barback.dart'; | |
| 17 import 'package:html5lib/dom.dart'; | |
| 18 import 'package:html5lib/dom_parsing.dart'; | |
| 19 | |
| 20 import 'transform/common.dart'; | |
| 21 | |
| 22 typedef String MessageFormatter(String kind, String message, Span span); | |
| 23 | |
| 24 /** | |
| 25 * A validator that checks for common polymer errors and produces warnings to | |
|
Jennifer Messerly
2013/09/04 03:13:27
Polymer?
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done.
| |
| 26 * show on the editor or the command line. Leaves sources unchanged, but creates | |
| 27 * a new asset containing all the warnings. | |
| 28 */ | |
| 29 // TODO(sigmund): maybe barback should have the concept of a validator too? | |
| 30 class Validator extends Transformer { | |
|
Jennifer Messerly
2013/09/04 03:13:27
suggestion: "Validator" sounds to me like a Pass/F
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Funny, I was chatting with Seth about this name ye
Jennifer Messerly
2013/09/04 19:21:34
yeah, it's a tossup. this works for me
| |
| 31 /** Only run on .html files. */ | |
| 32 final String allowedExtensions = ".html"; | |
| 33 | |
| 34 final MessageFormatter _formatter; | |
|
Jennifer Messerly
2013/09/04 03:13:27
newline after?
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done.
| |
| 35 Validator([this._formatter]); | |
| 36 | |
| 37 Future apply(Transform transform) { | |
| 38 var wrapper = new _TransformLoggerInterceptor(transform, _formatter); | |
| 39 var seen = new Set<AssetId>(); | |
| 40 var primary = transform.primaryInput; | |
| 41 var id = primary.id; | |
| 42 wrapper.addOutput(primary); // this phase is analysis only | |
| 43 seen.add(id); | |
| 44 return readPrimaryAsHtml(wrapper).then((document) { | |
| 45 return _collectElements(document, id, wrapper, seen).then((elements) { | |
| 46 new _ValidatorVisitor(wrapper, elements).visit(document); | |
| 47 var messagesId = id.addExtension('.messages'); | |
| 48 wrapper.addOutput(new Asset.fromString(messagesId, | |
| 49 wrapper._messages.join('\n'))); | |
| 50 }); | |
| 51 }); | |
| 52 } | |
| 53 | |
| 54 /** | |
| 55 * Collect into [elements] any data about each polymer-element defined in | |
| 56 * [document] or any of it's imports, unless they have already been [seen]. | |
| 57 * Elements are added in the order they appear, transitive imports are added | |
| 58 * first. | |
| 59 */ | |
| 60 Future<Map<String, _ElementSummary>> _collectElements( | |
| 61 Document document, AssetId sourceId, Transform transform, | |
| 62 Set<AssetId> seen, [Map<String, _ElementSummary> elements]) { | |
| 63 if (elements == null) elements = <String, _ElementSummary>{}; | |
| 64 var logger = transform.logger; | |
| 65 // Note: the import order is relevant, so we visit in that order. | |
| 66 return Future.forEach(_getImportedIds(document, sourceId, logger), (id) { | |
| 67 if (seen.contains(id)) return new Future.value(null); | |
| 68 seen.add(id); | |
| 69 return readAsHtml(id, transform) | |
| 70 .then((doc) => _collectElements(doc, id, transform, seen, elements)); | |
| 71 }).then((_) { | |
| 72 _addElements(document, logger, elements); | |
| 73 return elements; | |
| 74 }); | |
| 75 } | |
| 76 | |
| 77 List<AssetId> _getImportedIds( | |
| 78 Document document, AssetId sourceId, TranformLogger logger) { | |
| 79 var importIds = []; | |
| 80 for (var tag in document.queryAll('link')) { | |
| 81 if (tag.attributes['rel'] != 'import') continue; | |
| 82 var href = tag.attributes['href']; | |
| 83 var id = resolve(sourceId, href, logger, tag.sourceSpan); | |
| 84 if (id == null) continue; | |
| 85 importIds.add(id); | |
| 86 } | |
| 87 return importIds; | |
| 88 } | |
| 89 | |
| 90 void _addElements(Document document, TransformLogger logger, | |
| 91 Map<String, _ElementSummary> elements) { | |
| 92 for (var tag in document.queryAll('polymer-element')) { | |
| 93 var name = tag.attributes['name']; | |
| 94 if (name == null) continue; | |
| 95 var extendsTag = tag.attributes['extends']; | |
| 96 var span = tag.sourceSpan; | |
| 97 if (elements.containsKey(name)) { | |
|
Jennifer Messerly
2013/09/04 03:13:27
you could combine these two lines into one Map loo
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done.
| |
| 98 var existing = elements[name]; | |
| 99 | |
| 100 // Report warning only once. | |
| 101 if (existing.hasConflict) continue; | |
| 102 existing.hasConflict = true; | |
| 103 logger.warning('duplicate definition for custom tag "$name".', | |
| 104 existing.span); | |
| 105 logger.warning('duplicate definition for custom tag "$name" ' | |
| 106 ' (second definition).', span); | |
| 107 continue; | |
| 108 } | |
| 109 | |
| 110 elements[name] = new _ElementSummary(name, extendsTag, tag.sourceSpan); | |
| 111 } | |
| 112 } | |
| 113 } | |
| 114 | |
| 115 // TODO(sigmund): get rid of this when barback supports a better way to log | |
| 116 // messages without printing them. | |
| 117 class _TransformLoggerInterceptor implements Transform, TransformLogger { | |
|
Jennifer Messerly
2013/09/04 03:13:27
suggestion: since this is a private something shor
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done.
| |
| 118 final Transform _original; | |
| 119 final List<String> _messages = []; | |
| 120 final MessageFormatter _formatter; | |
| 121 | |
| 122 _TransformLoggerInterceptor(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); | |
|
Jennifer Messerly
2013/09/04 03:13:27
Wow. An actual non-boilerplate-y proxy. Win!
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
:-)
| |
| 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 the format that can be parsed | |
| 141 * by the Dart Editor when reporting error messages. | |
|
Jennifer Messerly
2013/09/04 03:13:27
maybe: "by tools such as the Dart Editor"?
| |
| 142 */ | |
| 143 String _defaultFormatter(String kind, Strnig 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 { | |
|
Siggi Cherem (dart-lang)
2013/09/03 01:28:10
this is the only subset of Info that I still wante
| |
| 162 final String tagName; | |
| 163 final String extendsTag; | |
| 164 final Span span; | |
| 165 | |
| 166 _ElementSummary extendsPolymerElement; | |
|
Jennifer Messerly
2013/09/04 03:13:27
could this just be called "extends" (or "extendsTy
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done. Went with extendsType since we extends is a
| |
| 167 bool hasConflict = false; | |
| 168 | |
| 169 String get baseExtendsTag => extendsPolymerElement == null | |
| 170 ? extendsTag : extendsPolymerElement.baseExtendsTag; | |
| 171 | |
| 172 _ElementSummary(this.tagName, this.extendsTag, this.span); | |
| 173 | |
| 174 String toString() => "($tagName <: $extendsTag)"; | |
|
Jennifer Messerly
2013/09/04 03:13:27
nice use of subtype relation
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
=)
| |
| 175 } | |
| 176 | |
| 177 class _ValidatorVisitor extends TreeVisitor { | |
| 178 TransformLogger _logger; | |
| 179 bool _inPolymerElement; | |
| 180 Map<String, _ElementSummary> _elements; | |
| 181 | |
| 182 _ValidatorVisitor(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 || _elements[extendsTag] == null) continue; | |
|
Jennifer Messerly
2013/09/04 03:13:27
not a big deal, but cache _elements[extendsTag] in
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done. Since in that case is null, removing the sec
| |
| 188 tag.extendsPolymerElement = _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.', | |
|
Jennifer Messerly
2013/09/04 03:13:27
hmmm, I'm not sure nested definitions are wise, bu
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
not sure how that works with document.register tho
| |
| 230 node.sourceSpan); | |
| 231 return; | |
| 232 } | |
| 233 | |
| 234 var tagName = node.attributes['name']; | |
| 235 var extendsTag = node.attributes['extends']; | |
| 236 | |
| 237 if (tagName == null) { | |
|
Jennifer Messerly
2013/09/04 03:13:27
warn if (!_isCustomTag(tagName)) ?
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Good idea. Done.
| |
| 238 _logger.error('Missing tag name of the custom element. Please include an ' | |
| 239 'attribute like \'name="your-tag-name"\'.', | |
| 240 node.sourceSpan); | |
| 241 } | |
| 242 | |
| 243 if (_elements[extendsTag] == null && _isCustomTag(extendsTag)) { | |
| 244 _logger.warning('custom element with name "$extendsTag" not found.', | |
| 245 node.sourceSpan); | |
| 246 } | |
| 247 | |
| 248 var oldValue = _inPolymerElement; | |
| 249 _inPolymerElement = true; | |
| 250 super.visitElement(node); | |
| 251 _inPolymerElement = oldValue; | |
| 252 } | |
| 253 | |
| 254 /** | |
| 255 * Produces warnings for malformed script tags. In html5 leaving off type= is | |
| 256 * fine, but it defaults to text/javascript. Because this might be a common | |
| 257 * error, we warn about it when src file ends in .dart, but the type is | |
| 258 * incorrect, or when users write code in an inline script tag of a custom | |
| 259 * element. | |
| 260 * | |
| 261 * The hope is that these cases shouldn't break existing valid code, but that | |
| 262 * they'll help polymer authors avoid having their Dart code accidentally | |
|
Jennifer Messerly
2013/09/04 03:13:27
Polymer?
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done.
| |
| 263 * interpreted as JavaScript by the browser. | |
| 264 */ | |
| 265 void _validateScriptElement(Element node) { | |
| 266 var scriptType = node.attributes['type']; | |
| 267 var src = node.attributes["src"]; | |
|
Jennifer Messerly
2013/09/04 03:13:27
single quote?
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done.
| |
| 268 | |
| 269 if (scriptType == null) { | |
| 270 if (src == null && _inPolymerElement) { | |
| 271 _logger.warning('script tag in polymer element with no type will ' | |
|
Jennifer Messerly
2013/09/04 03:13:27
add a TODO here? this warning is a bit dubious onc
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Done.
| |
| 272 'be treated as JavaScript. Did you forget type="application/dart"?', | |
| 273 node.sourceSpan); | |
| 274 } | |
| 275 if (src != null && src.endsWith('.dart')) { | |
| 276 _logger.warning('script tag with .dart source file but no type will ' | |
| 277 'be treated as JavaScript. Did you forget type="application/dart"?', | |
| 278 node.sourceSpan); | |
| 279 } | |
| 280 return; | |
| 281 } | |
| 282 | |
| 283 if (scriptType != 'application/dart') return; | |
| 284 | |
| 285 if (src != null) { | |
| 286 if (!src.endsWith('.dart')) { | |
| 287 _logger.warning('"application/dart" scripts should ' | |
| 288 'use the .dart file extension.', | |
| 289 node.sourceSpan); | |
| 290 } | |
| 291 | |
| 292 if (node.innerHtml.trim() != '') { | |
| 293 _logger.warning('script tag has "src" attribute and also has script ' | |
| 294 'text.', node.sourceSpan); | |
| 295 } | |
| 296 } | |
| 297 } | |
| 298 | |
| 299 /** | |
| 300 * Produces warnings for misuses of on-foo event handlers, and for instanting | |
| 301 * custom tags incorrectly. | |
| 302 */ | |
| 303 void _validateNormalElement(Element node) { | |
|
Jennifer Messerly
2013/09/04 03:13:27
something for the future:
validate that {{ binding
Siggi Cherem (dart-lang)
2013/09/04 17:57:27
Good idea. Created dartbug.com/13040 to track.
| |
| 304 // Event handlers only allowed inside polymer-elements | |
| 305 node.attributes.forEach((name, value) { | |
| 306 if (name.startsWith('on')) { | |
| 307 _validateEventHandler(node, name, value); | |
| 308 } | |
| 309 }); | |
| 310 | |
| 311 // Validate uses of custom-tags | |
| 312 var nodeTag = node.tagName; | |
| 313 var hasIsAttribute; | |
| 314 var customTagName; | |
| 315 if (_isCustomTag(nodeTag)) { | |
| 316 // <fancy-button> | |
| 317 customTagName = nodeTag; | |
| 318 hasIsAttribute = false; | |
| 319 } else { | |
| 320 // <button is="fancy-button"> | |
| 321 customTagName = node.attributes['is']; | |
| 322 hasIsAttribute = true; | |
| 323 } | |
| 324 | |
| 325 if (customTagName == null || customTagName == 'polymer-element') return; | |
| 326 | |
| 327 var info = _elements[customTagName]; | |
| 328 if (info == null) { | |
| 329 _logger.warning('definition for custom element with tag name ' | |
| 330 '"$customTagName" not found.', node.sourceSpan); | |
| 331 return; | |
| 332 } | |
| 333 | |
| 334 var baseTag = info.baseExtendsTag; | |
| 335 if (baseTag != null && !hasIsAttribute) { | |
| 336 _logger.warning( | |
| 337 'custom element "$customTagName" extends from "$baseTag", but ' | |
| 338 'this tag will not include the default properties of "$baseTag". ' | |
| 339 'To fix this, either write this tag as <$baseTag ' | |
| 340 'is="$customTagName"> or remove the "extends" attribute from ' | |
| 341 'the custom element declaration.', node.sourceSpan); | |
| 342 return; | |
| 343 } | |
| 344 | |
| 345 if (hasIsAttribute && baseTag == null) { | |
| 346 _logger.warning( | |
| 347 'custom element "$customTagName" doesn\'t declare any type ' | |
| 348 'extensions. To fix this, either rewrite this tag as ' | |
| 349 '<$customTagName> or add \'extends="$nodeTag"\' to ' | |
| 350 'the custom element declaration.', node.sourceSpan); | |
| 351 return; | |
| 352 } | |
| 353 | |
| 354 if (hasIsAttribute && baseTag != nodeTag) { | |
| 355 _logger.warning( | |
| 356 'custom element "$customTagName" extends from "$baseTag". ' | |
| 357 'Did you mean to write <$baseTag is="$customTagName">?', | |
| 358 node.sourceSpan); | |
| 359 } | |
| 360 } | |
| 361 | |
| 362 /** Validate event handlers are used correctly. */ | |
| 363 void _validateEventHandler(Element node, String name, String value) { | |
| 364 if (!name.startsWith('on-')) { | |
| 365 _logger.warning('Event handler "$name" will be interpreted as an inline' | |
| 366 ' JavaScript event handler. Use the form ' | |
| 367 'on-event-name="handlerName" if you want a Dart handler ' | |
| 368 'that will automatically update the UI based on model changes.', | |
| 369 node.sourceSpan); | |
| 370 return; | |
| 371 } | |
| 372 | |
| 373 if (!_inPolymerElement) { | |
| 374 _logger.warning('Inline event handlers are only supported inside ' | |
| 375 'declarations of <polymer-element>.', node.sourceSpan); | |
| 376 } | |
| 377 | |
| 378 if (value.contains('.') || value.contains('(')) { | |
| 379 _logger.warning('Invalid event handler body "$value". Declare a method ' | |
| 380 'in your custom element "void handlerName(event, detail, target)" ' | |
| 381 'and use the form $name="handlerName".', | |
| 382 node.sourceSpan); | |
| 383 } | |
| 384 } | |
| 385 } | |
| 386 | |
| 387 /** | |
| 388 * Returns true if this is a valid custom element name. See: | |
| 389 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/custom/index.html#dfn -custom-element-name> | |
| 390 */ | |
| 391 bool _isCustomTag(String name) { | |
| 392 if (name == null || !name.contains('-')) return false; | |
| 393 | |
| 394 // These names have meaning in SVG or MathML, so they aren't allowed as custom | |
| 395 // tags. | |
| 396 var invalidNames = const { | |
| 397 'annotation-xml': '', | |
| 398 'color-profile': '', | |
| 399 'font-face': '', | |
| 400 'font-face-src': '', | |
| 401 'font-face-uri': '', | |
| 402 'font-face-format': '', | |
| 403 'font-face-name': '', | |
| 404 'missing-glyph': '', | |
| 405 }; | |
| 406 return !invalidNames.containsKey(name); | |
| 407 } | |
| OLD | NEW |