Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(75)

Side by Side Diff: pkg/polymer/lib/src/validator.dart

Issue 23452010: Add a polymer validator: a linter/analysis that will replace the old (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698