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 |