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

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

Issue 23898009: Switch polymer's build.dart to use the new linter. This CL does the following (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: addressing john comments (part 2) - renamed files 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
« no previous file with comments | « pkg/polymer/lib/polymer_element.dart ('k') | pkg/polymer/lib/src/barback_runner.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
5 /**
6 * Part of the template compilation that concerns with extracting information
7 * from the HTML parse tree.
8 */
9 library analyzer;
10
11 import 'package:html5lib/dom.dart';
12 import 'package:html5lib/dom_parsing.dart';
13
14 import 'custom_tag_name.dart';
15 import 'files.dart';
16 import 'info.dart';
17 import 'messages.dart';
18
19 /**
20 * Finds custom elements in this file and the list of referenced files with
21 * component declarations. This is the first pass of analysis on a file.
22 *
23 * Adds emitted error/warning messages to [messages], if [messages] is
24 * supplied.
25 */
26 FileInfo analyzeDefinitions(GlobalInfo global, UrlInfo inputUrl,
27 Document document, String packageRoot, Messages messages) {
28 var result = new FileInfo(inputUrl);
29 var loader = new _ElementLoader(global, result, packageRoot, messages);
30 loader.visit(document);
31 return result;
32 }
33
34 /**
35 * Extract relevant information from all files found from the root document.
36 *
37 * Adds emitted error/warning messages to [messages], if [messages] is
38 * supplied.
39 */
40 void analyzeFile(SourceFile file, Map<String, FileInfo> info,
41 Iterator<int> uniqueIds, GlobalInfo global,
42 Messages messages, emulateScopedCss) {
43 var fileInfo = info[file.path];
44 var analyzer = new _Analyzer(fileInfo, uniqueIds, global, messages,
45 emulateScopedCss);
46 analyzer._normalize(fileInfo, info);
47 analyzer.visit(file.document);
48 }
49
50
51 /** A visitor that walks the HTML to extract all the relevant information. */
52 class _Analyzer extends TreeVisitor {
53 final FileInfo _fileInfo;
54 LibraryInfo _currentInfo;
55 Iterator<int> _uniqueIds;
56 GlobalInfo _global;
57 Messages _messages;
58
59 int _generatedClassNumber = 0;
60
61 /**
62 * Whether to keep indentation spaces. Break lines and indentation spaces
63 * within templates are preserved in HTML. When users specify the attribute
64 * 'indentation="remove"' on a template tag, we'll trim those indentation
65 * spaces that occur within that tag and its decendants. If any decendant
66 * specifies 'indentation="preserve"', then we'll switch back to the normal
67 * behavior.
68 */
69 bool _keepIndentationSpaces = true;
70
71 final bool _emulateScopedCss;
72
73 _Analyzer(this._fileInfo, this._uniqueIds, this._global, this._messages,
74 this._emulateScopedCss) {
75 _currentInfo = _fileInfo;
76 }
77
78 void visitElement(Element node) {
79 if (node.tagName == 'script') {
80 // We already extracted script tags in previous phase.
81 return;
82 }
83
84 if (node.tagName == 'style') {
85 // We've already parsed the CSS.
86 // If this is a component remove the style node.
87 if (_currentInfo is ComponentInfo && _emulateScopedCss) node.remove();
88 return;
89 }
90
91 _bindCustomElement(node);
92
93 var lastInfo = _currentInfo;
94 if (node.tagName == 'polymer-element') {
95 // If element is invalid _ElementLoader already reported an error, but
96 // we skip the body of the element here.
97 var name = node.attributes['name'];
98 if (name == null) return;
99
100 ComponentInfo component = _fileInfo.components[name];
101 if (component == null) return;
102
103 _analyzeComponent(component);
104
105 _currentInfo = component;
106
107 // Remove the <element> tag from the tree
108 node.remove();
109 }
110
111 node.attributes.forEach((name, value) {
112 if (name.startsWith('on')) {
113 _validateEventHandler(node, name, value);
114 } else if (name == 'pseudo' && _currentInfo is ComponentInfo) {
115 // Any component's custom pseudo-element(s) defined?
116 _processPseudoAttribute(node, value.split(' '));
117 }
118 });
119
120 var keepSpaces = _keepIndentationSpaces;
121 if (node.tagName == 'template' &&
122 node.attributes.containsKey('indentation')) {
123 var value = node.attributes['indentation'];
124 if (value != 'remove' && value != 'preserve') {
125 _messages.warning(
126 "Invalid value for 'indentation' ($value). By default we preserve "
127 "the indentation. Valid values are either 'remove' or 'preserve'.",
128 node.sourceSpan);
129 }
130 _keepIndentationSpaces = value != 'remove';
131 }
132
133 // Invoke super to visit children.
134 super.visitElement(node);
135
136 _keepIndentationSpaces = keepSpaces;
137 _currentInfo = lastInfo;
138 }
139
140 void _analyzeComponent(ComponentInfo component) {
141 var baseTag = component.extendsTag;
142 component.extendsComponent = baseTag == null ? null
143 : _fileInfo.components[baseTag];
144 if (component.extendsComponent == null && isCustomTag(baseTag)) {
145 _messages.warning(
146 'custom element with tag name ${component.extendsTag} not found.',
147 component.element.sourceSpan);
148 }
149 }
150
151 void _bindCustomElement(Element node) {
152 // <fancy-button>
153 var component = _fileInfo.components[node.tagName];
154 if (component == null) {
155 // TODO(jmesserly): warn for unknown element tags?
156
157 // <button is="fancy-button">
158 var componentName = node.attributes['is'];
159 if (componentName != null) {
160 component = _fileInfo.components[componentName];
161 } else if (isCustomTag(node.tagName)) {
162 componentName = node.tagName;
163 }
164 if (component == null && componentName != null &&
165 componentName != 'polymer-element') {
166 _messages.warning(
167 'custom element with tag name $componentName not found.',
168 node.sourceSpan);
169 }
170 }
171
172 if (component != null) {
173 var baseTag = component.baseExtendsTag;
174 var nodeTag = node.tagName;
175 var hasIsAttribute = node.attributes.containsKey('is');
176
177 if (baseTag != null && !hasIsAttribute) {
178 _messages.warning(
179 'custom element "${component.tagName}" extends from "$baseTag", but'
180 ' this tag will not include the default properties of "$baseTag". '
181 'To fix this, either write this tag as <$baseTag '
182 'is="${component.tagName}"> or remove the "extends" attribute from '
183 'the custom element declaration.', node.sourceSpan);
184 } else if (hasIsAttribute) {
185 if (baseTag == null) {
186 _messages.warning(
187 'custom element "${component.tagName}" doesn\'t declare any type '
188 'extensions. To fix this, either rewrite this tag as '
189 '<${component.tagName}> or add \'extends="$nodeTag"\' to '
190 'the custom element declaration.', node.sourceSpan);
191 } else if (baseTag != nodeTag) {
192 _messages.warning(
193 'custom element "${component.tagName}" extends from "$baseTag". '
194 'Did you mean to write <$baseTag is="${component.tagName}">?',
195 node.sourceSpan);
196 }
197 }
198 }
199 }
200
201 void _processPseudoAttribute(Node node, List<String> values) {
202 List mangledValues = [];
203 for (var pseudoElement in values) {
204 if (_global.pseudoElements.containsKey(pseudoElement)) continue;
205
206 _uniqueIds.moveNext();
207 var newValue = "${pseudoElement}_${_uniqueIds.current}";
208 _global.pseudoElements[pseudoElement] = newValue;
209 // Mangled name of pseudo-element.
210 mangledValues.add(newValue);
211
212 if (!pseudoElement.startsWith('x-')) {
213 // TODO(terry): The name must start with x- otherwise it's not a custom
214 // pseudo-element. May want to relax since components no
215 // longer need to start with x-. See isse #509 on
216 // pseudo-element prefix.
217 _messages.warning("Custom pseudo-element must be prefixed with 'x-'.",
218 node.sourceSpan);
219 }
220 }
221
222 // Update the pseudo attribute with the new mangled names.
223 node.attributes['pseudo'] = mangledValues.join(' ');
224 }
225
226 /**
227 * Support for inline event handlers that take expressions.
228 * For example: `on-double-click=myHandler($event, todo)`.
229 */
230 void _validateEventHandler(Element node, String name, String value) {
231 if (!name.startsWith('on-')) {
232 // TODO(jmesserly): do we need an option to suppress this warning?
233 _messages.warning('Event handler $name will be interpreted as an inline '
234 'JavaScript event handler. Use the form '
235 'on-event-name="handlerName" if you want a Dart handler '
236 'that will automatically update the UI based on model changes.',
237 node.sourceSpan);
238 }
239
240 if (value.contains('.') || value.contains('(')) {
241 // TODO(sigmund): should we allow more if we use fancy-syntax?
242 _messages.warning('Invalid event handler body "$value". Declare a method '
243 'in your custom element "void handlerName(event, detail, target)" '
244 'and use the form on-event-name="handlerName".',
245 node.sourceSpan);
246 }
247 }
248
249 /**
250 * Normalizes references in [info]. On the [analyzeDefinitions] phase, the
251 * analyzer extracted names of files and components. Here we link those names
252 * to actual info classes. In particular:
253 * * we initialize the [FileInfo.components] map in [info] by importing all
254 * [declaredComponents],
255 * * we scan all [info.componentLinks] and import their
256 * [info.declaredComponents], using [files] to map the href to the file
257 * info. Names in [info] will shadow names from imported files.
258 */
259 void _normalize(FileInfo info, Map<String, FileInfo> files) {
260 for (var component in info.declaredComponents) {
261 _addComponent(info, component);
262 }
263
264 for (var link in info.componentLinks) {
265 var file = files[link.resolvedPath];
266 // We already issued an error for missing files.
267 if (file == null) continue;
268 file.declaredComponents.forEach((c) => _addComponent(info, c));
269 }
270 }
271
272 /** Adds a component's tag name to the names in scope for [fileInfo]. */
273 void _addComponent(FileInfo fileInfo, ComponentInfo component) {
274 var existing = fileInfo.components[component.tagName];
275 if (existing != null) {
276 if (existing == component) {
277 // This is the same exact component as the existing one.
278 return;
279 }
280
281 if (existing is ComponentInfo && component is! ComponentInfo) {
282 // Components declared in [fileInfo] shadow component names declared in
283 // imported files.
284 return;
285 }
286
287 if (existing.hasConflict) {
288 // No need to report a second error for the same name.
289 return;
290 }
291
292 existing.hasConflict = true;
293
294 if (component is ComponentInfo) {
295 _messages.error('duplicate custom element definition for '
296 '"${component.tagName}".', existing.sourceSpan);
297 _messages.error('duplicate custom element definition for '
298 '"${component.tagName}" (second location).', component.sourceSpan);
299 } else {
300 _messages.error('imported duplicate custom element definitions '
301 'for "${component.tagName}".', existing.sourceSpan);
302 _messages.error('imported duplicate custom element definitions '
303 'for "${component.tagName}" (second location).',
304 component.sourceSpan);
305 }
306 } else {
307 fileInfo.components[component.tagName] = component;
308 }
309 }
310 }
311
312 /** A visitor that finds `<link rel="import">` and `<element>` tags. */
313 class _ElementLoader extends TreeVisitor {
314 final GlobalInfo _global;
315 final FileInfo _fileInfo;
316 LibraryInfo _currentInfo;
317 String _packageRoot;
318 bool _inHead = false;
319 Messages _messages;
320
321 /**
322 * Adds emitted warning/error messages to [_messages]. [_messages]
323 * must not be null.
324 */
325 _ElementLoader(this._global, this._fileInfo, this._packageRoot,
326 this._messages) {
327 _currentInfo = _fileInfo;
328 }
329
330 void visitElement(Element node) {
331 switch (node.tagName) {
332 case 'link': visitLinkElement(node); break;
333 case 'element':
334 _messages.warning('<element> elements are not supported, use'
335 ' <polymer-element> instead', node.sourceSpan);
336 break;
337 case 'polymer-element':
338 visitElementElement(node);
339 break;
340 case 'script': visitScriptElement(node); break;
341 case 'head':
342 var savedInHead = _inHead;
343 _inHead = true;
344 super.visitElement(node);
345 _inHead = savedInHead;
346 break;
347 default: super.visitElement(node); break;
348 }
349 }
350
351 /**
352 * Process `link rel="import"` as specified in:
353 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/components/index.ht ml#link-type-component>
354 */
355 void visitLinkElement(Element node) {
356 var rel = node.attributes['rel'];
357 if (rel != 'component' && rel != 'components' &&
358 rel != 'import' && rel != 'stylesheet') return;
359
360 if (!_inHead) {
361 _messages.warning('link rel="$rel" only valid in '
362 'head.', node.sourceSpan);
363 return;
364 }
365
366 if (rel == 'component' || rel == 'components') {
367 _messages.warning('import syntax is changing, use '
368 'rel="import" instead of rel="$rel".', node.sourceSpan);
369 }
370
371 var href = node.attributes['href'];
372 if (href == null || href == '') {
373 _messages.warning('link rel="$rel" missing href.',
374 node.sourceSpan);
375 return;
376 }
377
378 bool isStyleSheet = rel == 'stylesheet';
379 var urlInfo = UrlInfo.resolve(href, _fileInfo.inputUrl, node.sourceSpan,
380 _packageRoot, _messages, ignoreAbsolute: isStyleSheet);
381 if (urlInfo == null) return;
382 if (isStyleSheet) {
383 _fileInfo.styleSheetHrefs.add(urlInfo);
384 } else {
385 _fileInfo.componentLinks.add(urlInfo);
386 }
387 }
388
389 void visitElementElement(Element node) {
390 // TODO(jmesserly): what do we do in this case? It seems like an <element>
391 // inside a Shadow DOM should be scoped to that <template> tag, and not
392 // visible from the outside.
393 if (_currentInfo is ComponentInfo) {
394 _messages.error('Nested component definitions are not yet supported.',
395 node.sourceSpan);
396 return;
397 }
398
399 var tagName = node.attributes['name'];
400 var extendsTag = node.attributes['extends'];
401
402 if (tagName == null) {
403 _messages.error('Missing tag name of the component. Please include an '
404 'attribute like \'name="your-tag-name"\'.',
405 node.sourceSpan);
406 return;
407 }
408
409 var component = new ComponentInfo(node, tagName, extendsTag);
410 _fileInfo.declaredComponents.add(component);
411 _addComponent(component);
412
413 var lastInfo = _currentInfo;
414 _currentInfo = component;
415 super.visitElement(node);
416 _currentInfo = lastInfo;
417 }
418
419 /** Adds a component's tag name to the global list. */
420 void _addComponent(ComponentInfo component) {
421 var existing = _global.components[component.tagName];
422 if (existing != null) {
423 if (existing.hasConflict) {
424 // No need to report a second error for the same name.
425 return;
426 }
427
428 existing.hasConflict = true;
429
430 _messages.error('duplicate custom element definition for '
431 '"${component.tagName}".', existing.sourceSpan);
432 _messages.error('duplicate custom element definition for '
433 '"${component.tagName}" (second location).', component.sourceSpan);
434 } else {
435 _global.components[component.tagName] = component;
436 }
437 }
438
439 void visitScriptElement(Element node) {
440 var scriptType = node.attributes['type'];
441 var src = node.attributes["src"];
442
443 if (scriptType == null) {
444 // Note: in html5 leaving off type= is fine, but it defaults to
445 // text/javascript. Because this might be a common error, we warn about it
446 // in two cases:
447 // * an inline script tag in a web component
448 // * a script src= if the src file ends in .dart (component or not)
449 //
450 // The hope is that neither of these cases should break existing valid
451 // code, but that they'll help component authors avoid having their Dart
452 // code accidentally interpreted as JavaScript by the browser.
453 if (src == null && _currentInfo is ComponentInfo) {
454 _messages.warning('script tag in component with no type will '
455 'be treated as JavaScript. Did you forget type="application/dart"?',
456 node.sourceSpan);
457 }
458 if (src != null && src.endsWith('.dart')) {
459 _messages.warning('script tag with .dart source file but no type will '
460 'be treated as JavaScript. Did you forget type="application/dart"?',
461 node.sourceSpan);
462 }
463 return;
464 }
465
466 if (scriptType != 'application/dart') return;
467
468 if (src != null) {
469 if (!src.endsWith('.dart')) {
470 _messages.warning('"application/dart" scripts should '
471 'use the .dart file extension.',
472 node.sourceSpan);
473 }
474
475 if (node.innerHtml.trim() != '') {
476 _messages.error('script tag has "src" attribute and also has script '
477 'text.', node.sourceSpan);
478 }
479 }
480 }
481 }
OLDNEW
« no previous file with comments | « pkg/polymer/lib/polymer_element.dart ('k') | pkg/polymer/lib/src/barback_runner.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698