| /** |
| * 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 |
| * @param {Set<string>} customElementBases |
| * @return {boolean} |
| */ |
| function hasLitIdentifier(node, customElementBases) { |
| return node.type === 'Identifier' && customElementBases.has(node.name); |
| } |
| /** |
| * Returns if the given node is a lit element by expression |
| * @param {ESTree.Node} node |
| * @param {Set<string>} customElementBases |
| * @return {boolean} |
| */ |
| function isLitByExpression(node, customElementBases) { |
| if (node) { |
| if (hasLitIdentifier(node, customElementBases)) { |
| return true; |
| } |
| if (node.type === 'CallExpression') { |
| return node.arguments.some((n) => isLitByExpression(n, customElementBases)); |
| } |
| } |
| return false; |
| } |
| /** |
| * Returns if the given node is a lit class |
| * @param {ESTree.Class} clazz |
| * @param {Rule.RuleContext} context |
| * @return { boolean } |
| */ |
| export function isLitClass(clazz, context) { |
| if (hasCustomElementDecorator(clazz)) { |
| return true; |
| } |
| const customElementBases = getElementBaseClasses(context); |
| if (clazz.superClass) { |
| return (hasLitIdentifier(clazz.superClass, customElementBases) || |
| isLitByExpression(clazz.superClass, customElementBases)); |
| } |
| return hasLitIdentifier(clazz, customElementBases); |
| } |
| /** |
| * 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()); |
| } |
| /** |
| * Retrieves the configured element base class list |
| * |
| * @param {Rule.RuleContext} context ESLint rule context |
| * @return {string[]} |
| */ |
| export function getElementBaseClasses(context) { |
| var _a; |
| const bases = new Set(['LitElement']); |
| if (Array.isArray((_a = context.settings.lit) === null || _a === void 0 ? void 0 : _a.elementBaseClasses)) { |
| const configuredBases = context.settings.lit.elementBaseClasses; |
| for (const base of configuredBases) { |
| bases.add(base); |
| } |
| } |
| return bases; |
| } |