/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {comparePositionAndRange, Document, DomModule, ParsedHtmlDocument, Severity, Warning} from 'prax-analyzer';
import {HtmlRule} from '../html/rule';
import {registry} from '../registry';
import {stripIndentation} from '../util';
/**
* Unbalanced binding expression delimiters occurs when a value such as
* `[[myValue]]` or `{{myValue}}` have too many or too few brackets on either
* side.
*/
class ElementBeforeDomModule extends HtmlRule {
readonly code = 'element-before-dom-module';
readonly description = stripIndentation(`
Warns for an element being declared before its dom-module.
For example, this is invalid:
But this is fine:
`);
public async checkDocument(
parsedHtml: ParsedHtmlDocument, document: Document): Promise {
const warnings: Warning[] = [];
const domModules = document.getFeatures({kind: 'dom-module'});
if (domModules.size === 0) {
return warnings; // Early exit quick in the trivial case.
}
const domModulesByTagName = new Map();
for (const domModule of domModules) {
if (!domModule.id) {
continue;
}
domModulesByTagName.set(domModule.id, domModule);
}
// Gather together all elements defined in this file and in direct imports.
// Group them together with the source range in this file that they first
// become active. (We don't look into transitive imports because circularity
// makes ordering complicated.)
const localElements =
Array.from(document.getFeatures({kind: 'polymer-element'}))
.filter((el) => !!el.sourceRange)
.map((el) => ({sourceRange: el.sourceRange!, elements: [el]}));
const elementsByImport =
Array.from(document.getFeatures({kind: 'import'}))
.filter((i) => i.sourceRange)
.map((i) => {
if (!i.document) {
return undefined!;
}
// For complicated reasons, non-module script src tags are kinda
// treated like independent documents, and kinda like inline
// scripts. Long story short, we need to make sure that any
// elements defined "in them" aren't actually defined in us, their
// importer.
const elements =
Array.from(i.document.getFeatures({kind: 'polymer-element'}));
const nonlocalElements = elements.filter(
(e) => e.sourceRange && e.sourceRange.file !== document.url);
return {sourceRange: i.sourceRange!, elements: nonlocalElements};
})
.filter((v) => !!v);
// Sort the element groups by the order in which they appear in the
// document.
const sorted = localElements.concat(elementsByImport).sort((a, b) => {
return comparePositionAndRange(a.sourceRange.start, b.sourceRange);
});
const seenSoFar = new Set();
for (const pair of sorted) {
for (const element of pair.elements) {
const tagName = element.tagName;
if (!tagName) {
continue;
}
const domModule = domModulesByTagName.get(tagName);
if (!domModule || seenSoFar.has(tagName)) {
continue;
}
seenSoFar.add(tagName);
// Ok! Finally! domModule is a from `document`, and
// `element` is its element definition, first defined at
// `pair.sourceRange` in `document`. Now we compare them, and if the
// element comes before the dom-module, that's an error!
if (comparePositionAndRange(
pair.sourceRange.start, domModule.sourceRange) === -1) {
// TODO(rictic): if we ever support multiple source ranges on
// warnings, this would be a good candidate.
warnings.push(new Warning({
parsedDocument: parsedHtml,
code: this.code,
message: `A Polymer element must be defined after its ` +
`\`\`. If it can't find its \`\` ` +
`when it is registered, it will assume it does not have one.`,
severity: Severity.ERROR,
sourceRange:
parsedHtml.sourceRangeForStartTag(domModule.astNode.node) ||
domModule.sourceRange,
}));
}
}
}
return warnings;
}
}
registry.register(new ElementBeforeDomModule());