UNPKG

eslint-plugin-lit

Version:
272 lines (271 loc) 11.2 kB
import * as parse5 from 'parse5'; import treeAdapter from 'parse5-htmlparser2-tree-adapter'; import { templateExpressionToHtml, getExpressionPlaceholder } from './util.js'; const isRootNode = (node) => node.type === 'root'; const analyzerCache = new WeakMap(); /** * Analyzes a given template expression for traversing its contained * HTML tree. */ export class TemplateAnalyzer { /** * Create an analyzer instance for a given node * * @param {ESTree.TaggedTemplateExpression} node Node to use * @return {!TemplateAnalyzer} */ static create(node) { let cached = analyzerCache.get(node); if (!cached) { cached = new TemplateAnalyzer(node); analyzerCache.set(node, cached); } return cached; } /** * Constructor * * @param {ESTree.TaggedTemplateExpression} node Node to analyze */ constructor(node) { this.errors = []; this.source = ''; this._node = node; this.source = templateExpressionToHtml(node); const opts = { treeAdapter: treeAdapter, sourceCodeLocationInfo: true, onParseError: (err) => { this.errors.push(err); } }; if (/<html/i.test(this.source)) { this._ast = parse5.parse(this.source, opts); } else { this._ast = parse5.parseFragment(this.source, opts); } } /** * Returns the ESTree location equivalent of a given attribute * * @param {treeAdapter.Element} element Element which owns this attribute * @param {string} attr Attribute name to retrieve * @param {SourceCode} source Source code from ESLint * @return {?ESTree.SourceLocation} */ getLocationForAttribute(element, attr, source) { if (!element.sourceCodeLocation || !element.sourceCodeLocation.attrs) { return null; } const loc = element.sourceCodeLocation.attrs[attr.toLowerCase()]; return loc ? this.resolveLocation(loc, source) : null; } /** * Returns the value of the specified attribute. * If this is an expression, the expression will be returned. Otherwise, * the raw value will be returned. * NOTE: if an attribute has multiple expressions in its value, this will * return the *first* expression. * @param {treeAdapter.Element} element Element which owns this attribute * @param {string} attr Attribute name to retrieve * @param {SourceCode} source Source code from ESLint * @return {?ESTree.Expression|string} */ getAttributeValue(element, attr, source) { const value = element.attribs[attr]; if (value === undefined) { return null; } const loc = this.getLocationForAttribute(element, attr, source); if (!loc) { return value; } // We add the attribute name length so we only pick up expressions // inside the value part const start = source.getIndexFromLoc(loc.start) + attr.length; const end = source.getIndexFromLoc(loc.end); const containedExpr = this._node.quasi.expressions.find((expr) => { if (!expr.loc) { return false; } const exprStart = source.getIndexFromLoc(expr.loc.start); const exprEnd = source.getIndexFromLoc(expr.loc.end); return exprStart >= start && exprEnd <= end; }); if (containedExpr !== undefined) { return containedExpr; } return value; } /** * Returns the raw attribute source of a given attribute * * @param {treeAdapter.Element} element Element which owns this attribute * @param {string} attr Attribute name to retrieve * @return {string} */ getRawAttributeValue(element, attr) { if (!element.sourceCodeLocation) { return null; } const xAttribs = element['x-attribsPrefix']; let originalAttr = attr.toLowerCase(); if (xAttribs && xAttribs[attr]) { originalAttr = `${xAttribs[attr]}:${attr}`; } const loc = element.sourceCodeLocation.attrs[originalAttr]; const source = this.source.substring(loc.startOffset, loc.endOffset); const firstEq = source.indexOf('='); const left = firstEq === -1 ? source : source.substr(0, firstEq); const right = firstEq === -1 ? undefined : source.substr(firstEq + 1); let unquotedValue = right; if (right) { if (right.startsWith('"') && right.endsWith('"')) { unquotedValue = right.replace(/(^"|"$)/g, ''); } else if (right.startsWith("'") && right.endsWith("'")) { unquotedValue = right.replace(/(^'|'$)/g, ''); } } return { name: left, value: unquotedValue, quotedValue: right }; } /** * Resolves a Parse5 location into an ESTree range * * @param {parse5.Location} loc Location to convert * @param {SourceCode} source ESLint source code object * @return {ESTree.SourceLocation} */ resolveLocation(loc, source) { var _a, _b, _c, _d, _e, _f, _g, _h; if (!this._node.loc || !this._node.quasi.loc) { return null; } let currentOffset = 0; // Initial correction is the offset of the overall template literal let endCorrection = ((_b = (_a = this._node.quasi.range) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 0) + 1; let startCorrection = endCorrection; let startCorrected = false; for (let i = 0; i < this._node.quasi.quasis.length; i++) { const quasi = this._node.quasi.quasis[i]; const expr = this._node.quasi.expressions[i]; const nextQuasi = this._node.quasi.quasis[i + 1]; currentOffset += quasi.value.raw.length; // If we haven't already found the quasi containing the start offset // and this quasi ends after it, set the start offset's correction // value and leave it from now on. if (!startCorrected && loc.startOffset < currentOffset) { startCorrection = endCorrection; startCorrected = true; } // If the location ends before this point, it must fit entirely in // this quasi and the quasis before it, so we don't care about the rest if (loc.endOffset < currentOffset) { break; } // If there's no range, something's really messed up so just fall back // to the template literal's location if (!quasi.range) { return (_c = this._node.quasi.loc) !== null && _c !== void 0 ? _c : null; } if (expr) { const placeholder = getExpressionPlaceholder(this._node, quasi); const oldOffset = currentOffset; // If there's an expression, increment the offset by its placeholder's // length (e.g. ${v} may actually be {{__Q:0__}} in HTML) currentOffset += placeholder.length; // If the offset fits inside the placeholder range, there's nothing // smart we can do, so return the expression's location if ((loc.startOffset >= oldOffset && loc.startOffset < currentOffset) || (loc.endOffset >= oldOffset && loc.endOffset < currentOffset)) { return (_d = expr.loc) !== null && _d !== void 0 ? _d : null; } // If the expression has no range, it won't be the only problem // so lets just fall back to the template literal's location if (!expr.range) { return (_e = this._node.quasi.loc) !== null && _e !== void 0 ? _e : null; } // Increment the correction value by the size of the expression. // Given an expression ${foo}, its range only covers "foo", not any // whitespace or the brackets. // To work around this, we use the end of the previous quasi and the // start of the next quasi as our [start, end] rather than the // expression's own [start, end]. const exprEnd = (_g = (_f = nextQuasi === null || nextQuasi === void 0 ? void 0 : nextQuasi.range) === null || _f === void 0 ? void 0 : _f[0]) !== null && _g !== void 0 ? _g : expr.range[1]; const exprStart = quasi.range[1]; endCorrection -= placeholder.length; endCorrection += exprEnd - exprStart + 3; } } // If the start never got corrected, parse5 is trying to give us a bad day // and probably gave us an offset after the end of the string (it does // this). So we should fall back to whatever the current end correction is if (!startCorrected) { startCorrection = endCorrection; } try { const start = source.getLocFromIndex(loc.startOffset + startCorrection); const end = source.getLocFromIndex(loc.endOffset + endCorrection); return { start, end }; } catch (_err) { return (_h = this._node.quasi.loc) !== null && _h !== void 0 ? _h : null; } } /** * Traverse the inner HTML tree with a given visitor * * @param {Visitor} visitor Visitor to apply * @return {void} */ traverse(visitor) { const visit = (node, parent) => { if (!node) { return; } if (visitor.enter) { visitor.enter(node, parent); } if (isRootNode(node)) { if (visitor.enterDocumentFragment) { visitor.enterDocumentFragment(node, parent); } } else if (treeAdapter.isCommentNode(node)) { if (visitor.enterCommentNode) { visitor.enterCommentNode(node, parent); } } else if (treeAdapter.isTextNode(node)) { if (visitor.enterTextNode) { visitor.enterTextNode(node, parent); } } else if (treeAdapter.isElementNode(node)) { if (visitor.enterElement) { visitor.enterElement(node, parent); } } if (treeAdapter.isElementNode(node) || isRootNode(node)) { const children = node.childNodes; if (children && children.length > 0) { children.forEach((child) => { visit(child, node); }); } } if (visitor.exit) { visitor.exit(node, parent); } }; visit(this._ast, null); } }