UNPKG

eslint-plugin-lit

Version:
290 lines (289 loc) 9.72 kB
/** * Returns if given node has a customElement decorator * @param {ESTree.Class} node * @return {boolean} */ function hasCustomElementDecorator(node) { const decoratedNode = node; if (!decoratedNode.decorators || !Array.isArray(decoratedNode.decorators)) { return false; } for (const decorator of decoratedNode.decorators) { if (decorator.expression.type === 'CallExpression' && decorator.expression.callee.type === 'Identifier' && decorator.expression.callee.name === 'customElement') { return true; } } return false; } /** * Returns if given node has a lit identifier * @param {ESTree.Node} node * @return {boolean} */ function hasLitIdentifier(node) { return node.type === 'Identifier' && node.name === 'LitElement'; } /** * Returns if the given node is a lit element by expression * @param {ESTree.Node} node * @return {boolean} */ function isLitByExpression(node) { if (node) { if (hasLitIdentifier(node)) { return true; } if (node.type === 'CallExpression') { return node.arguments.some(isLitByExpression); } } return false; } /** * Returns if the given node is a lit class * @param {ESTree.Class} clazz * @return { boolean } */ export function isLitClass(clazz) { if (hasCustomElementDecorator(clazz)) { return true; } if (clazz.superClass) { return (hasLitIdentifier(clazz.superClass) || isLitByExpression(clazz.superClass)); } return hasLitIdentifier(clazz); } /** * Get the name of a node * * @param {ESTree.Node} node Node to retrieve name of * @return {?string} */ export function getIdentifierName(node) { if (node.type === 'Identifier') { return node.name; } if (node.type === 'Literal') { return node.raw; } return undefined; } /** * Extracts property metadata from a given property object * @param {ESTree.Node} key Node to extract from * @param {ESTree.ObjectExpression} value Node to extract from * @return {object} */ export function extractPropertyEntry(key, value) { let state = false; let attribute = true; let attributeName = undefined; for (const prop of value.properties) { if (prop.type === 'Property' && prop.key.type === 'Identifier' && prop.value.type === 'Literal') { if (prop.key.name === 'state' && prop.value.value === true) { state = true; } if (prop.key.name === 'attribute') { if (prop.value.value === false) { attribute = false; } else if (typeof prop.value.value === 'string') { attributeName = prop.value.value; } } } } return { expr: value, key, state, attribute, attributeName }; } /** * Returns the class fields of a class * @param {ESTree.Class} node Class to retrieve class fields for * @return {ReadonlyMap<string, ESTreeObjectExpression>} */ export function getClassFields(node) { const result = new Map(); for (const member of node.body.body) { if (member.type === 'PropertyDefinition' && member.key.type === 'Identifier' && // TODO: we should cast as the equivalent tsestree PropertyDefinition !member.declare) { result.set(member.key.name, member); } } return result; } const propertyDecorators = ['state', 'property', 'internalProperty']; const internalDecorators = ['state', 'internalProperty']; /** * Get the properties object of an element class * * @param {ESTree.Class} node Class to retrieve map from * @return {ReadonlyMap<string, ESTreeObjectExpression>} */ export function getPropertyMap(node) { var _a; const result = new Map(); for (const member of node.body.body) { if (member.type === 'PropertyDefinition' && member.static && member.key.type === 'Identifier' && member.key.name === 'properties' && ((_a = member.value) === null || _a === void 0 ? void 0 : _a.type) === 'ObjectExpression') { for (const prop of member.value.properties) { if (prop.type === 'Property') { const name = getIdentifierName(prop.key); if (name && prop.value.type === 'ObjectExpression') { result.set(name, extractPropertyEntry(prop.key, prop.value)); } } } } if (member.type === 'MethodDefinition' && member.static && member.kind === 'get' && member.key.type === 'Identifier' && member.key.name === 'properties' && member.value.body) { const ret = member.value.body.body.find((m) => { var _a; return m.type === 'ReturnStatement' && ((_a = m.argument) === null || _a === void 0 ? void 0 : _a.type) === 'ObjectExpression'; }); if (ret) { const arg = ret.argument; for (const prop of arg.properties) { if (prop.type === 'Property') { const name = getIdentifierName(prop.key); if (name && prop.value.type === 'ObjectExpression') { result.set(name, extractPropertyEntry(prop.key, prop.value)); } } } } } if (member.type === 'MethodDefinition' || member.type === 'PropertyDefinition') { const babelProp = member; const key = member.key; const memberName = getIdentifierName(key); if (memberName && babelProp.decorators) { for (const decorator of babelProp.decorators) { if (decorator.expression.type === 'CallExpression' && decorator.expression.callee.type === 'Identifier' && propertyDecorators.includes(decorator.expression.callee.name)) { const dArg = decorator.expression.arguments[0]; if ((dArg === null || dArg === void 0 ? void 0 : dArg.type) === 'ObjectExpression') { const state = internalDecorators.includes(decorator.expression.callee.name); const entry = extractPropertyEntry(key, dArg); if (state) { entry.state = true; } result.set(memberName, entry); } else { const state = internalDecorators.includes(decorator.expression.callee.name); result.set(memberName, { key, expr: null, state, attribute: true }); } } } } } } return result; } /** * Determines if a node has a lit property decorator * @param {ESTree.Node} node Node to test * @return {boolean} */ export function hasLitPropertyDecorator(node) { const decoratedNode = node; if (!decoratedNode.decorators || !Array.isArray(decoratedNode.decorators)) { return false; } for (const decorator of decoratedNode.decorators) { if (decorator.expression.type === 'CallExpression' && decorator.expression.callee.type === 'Identifier' && propertyDecorators.includes(decorator.expression.callee.name)) { return true; } } return false; } /** * Generates a placeholder string for a given quasi * * @param {ESTree.TaggedTemplateExpression} node Root node * @param {ESTree.TemplateElement} quasi Quasi to generate placeholder * for * @return {string} */ export function getExpressionPlaceholder(node, quasi) { const i = node.quasi.quasis.indexOf(quasi); // Just a rough guess at if this might be an attribute binding or not const possibleAttr = /\s[^\s\/>"'=]+=$/; if (possibleAttr.test(quasi.value.raw)) { return `"{{__Q:${i}__}}"`; } return `{{__Q:${i}__}}`; } /** * Tests whether a string is a placeholder or not * * @param {string} value Value to test * @return {boolean} */ export function isExpressionPlaceholder(value) { return /^\{\{__Q:\d+__\}\}$/.test(value); } /** * Converts a template expression into HTML * * @param {ESTree.TaggedTemplateExpression} node Node to convert * @return {string} */ export function templateExpressionToHtml(node) { let html = ''; for (let i = 0; i < node.quasi.quasis.length; i++) { const quasi = node.quasi.quasis[i]; const expr = node.quasi.expressions[i]; html += quasi.value.raw; if (expr) { html += getExpressionPlaceholder(node, quasi); } } return html; } /** * Converts a camelCase string to snake_case string * * @param {string} camelCaseStr String to convert * @return {string} */ export function toSnakeCase(camelCaseStr) { return camelCaseStr.replace(/[A-Z]/g, (m) => '_' + m.toLowerCase()); } /** * Converts a camelCase string to kebab-case string * * @param {string} camelCaseStr String to convert * @return {string} */ export function toKebabCase(camelCaseStr) { return camelCaseStr.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); }