UNPKG

eslint-plugin-lit

Version:
230 lines (229 loc) 9.06 kB
/** * @fileoverview Requires use of query decorators instead of manual DOM queries * @author Kirill Karpov <https://github.com/null0rUndefined> */ import { isLitClass } from '../util.js'; //------------------------------------------------------------------------------ // Selectors //------------------------------------------------------------------------------ const querySelectorCall = 'CallExpression' + '[callee.type="MemberExpression"]' + '[callee.object.type="MemberExpression"]' + '[callee.object.object.type="ThisExpression"]' + ':matches(' + '[callee.object.property.name="shadowRoot"],' + '[callee.object.property.name="renderRoot"]' + ')' + ':matches(' + '[callee.property.name="querySelector"],' + '[callee.property.name="querySelectorAll"]' + ')'; const assignedCall = 'CallExpression' + '[callee.type="MemberExpression"]' + ':matches(' + '[callee.property.name="assignedElements"],' + '[callee.property.name="assignedNodes"]' + ')' + '[callee.object.type="CallExpression"]' + '[callee.object.callee.type="MemberExpression"]' + '[callee.object.callee.object.type="MemberExpression"]' + '[callee.object.callee.object.object.type="ThisExpression"]' + ':matches(' + '[callee.object.callee.object.property.name="shadowRoot"],' + '[callee.object.callee.object.property.name="renderRoot"]' + ')' + '[callee.object.callee.property.name="querySelector"]'; //------------------------------------------------------------------------------ // Constants //------------------------------------------------------------------------------ const assignedMethodNames = new Set(['assignedElements', 'assignedNodes']); const renderRootProperties = new Set(['shadowRoot', 'renderRoot']); const defaultOptions = { querySelector: true, querySelectorAll: true, assignedElements: true, assignedNodes: true }; const querySelectorMessageMap = new Map([ ['querySelector', 'querySelector'], ['querySelectorAll', 'querySelectorAll'] ]); const assignedMessageMap = new Map([ ['assignedElements', 'assignedElements'], ['assignedNodes', 'assignedNodes'] ]); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Determines if a call expression is the inner querySelector of a chained * assignedElements/assignedNodes call, to avoid double-reporting. * * @param {ESTree.CallExpression & Rule.NodeParentExtension} node Call expression to test * @return {boolean} */ function isChainedWithAssignedCall(node) { const parent = node.parent; return ((parent === null || parent === void 0 ? void 0 : parent.type) === 'MemberExpression' && parent.property.type === 'Identifier' && assignedMethodNames.has(parent.property.name)); } /** * Returns the method name from a member expression callee, or null if the * property is not a simple identifier. * * @param {ESTree.MemberExpression} callee Callee to inspect * @return {string|null} */ function getMethodName(callee) { return callee.property.type === 'Identifier' ? callee.property.name : null; } /** * Returns the render root property name (shadowRoot or renderRoot) from a * callee whose object is a member expression on `this`, or null if it does * not match the expected shape. * * @param {ESTree.MemberExpression} callee Callee to inspect * @return {string|null} */ function getRenderRootName(callee) { const obj = callee.object; if (obj.type !== 'MemberExpression' || obj.property.type !== 'Identifier') { return null; } const name = obj.property.name; return renderRootProperties.has(name) ? name : null; } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ export const rule = { meta: { type: 'suggestion', docs: { description: 'Requires use of query decorators instead of manual DOM queries', recommended: false, url: 'https://github.com/43081j/eslint-plugin-lit/blob/master/docs/rules/prefer-query-decorators.md' }, schema: [ { type: 'object', properties: { querySelector: { type: 'boolean' }, querySelectorAll: { type: 'boolean' }, assignedElements: { type: 'boolean' }, assignedNodes: { type: 'boolean' } }, additionalProperties: false } ], messages: { preferQuery: 'Use @query decorator instead of this.{{ root }}.querySelector()', preferQueryAll: 'Use @queryAll decorator instead of this.{{ root }}.querySelectorAll()', preferQueryAssignedElements: 'Use @queryAssignedElements decorator instead of' + ' this.{{ root }}.querySelector().assignedElements()', preferQueryAssignedNodes: 'Use @queryAssignedNodes decorator instead of' + ' this.{{ root }}.querySelector().assignedNodes()' }, defaultOptions: [defaultOptions] }, create(context) { const options = { ...defaultOptions, ...context.options[0] }; let litClassDepth = 0; //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- /** * Class entered * * @param {ESTree.Class} node Node entered * @return {void} */ function classEnter(node) { if (isLitClass(node, context)) { litClassDepth++; } } /** * Class exited * * @param {ESTree.Class} node Node exited * @return {void} */ function classExit(node) { if (isLitClass(node, context)) { litClassDepth--; } } /** * querySelector or querySelectorAll call found * * @param {ESTree.CallExpression} node Node entered * @return {void} */ function handleQuerySelectorCall(node) { if (litClassDepth === 0) { return; } if (isChainedWithAssignedCall(node) || node.callee.type !== 'MemberExpression') { return; } const callee = node.callee; const methodName = getMethodName(callee); const rootName = getRenderRootName(callee); if (!methodName || !rootName) { return; } const optionKey = querySelectorMessageMap.get(methodName); if (!optionKey || !options[optionKey]) { return; } const messageId = methodName === 'querySelector' ? 'preferQuery' : 'preferQueryAll'; context.report({ node, messageId, data: { root: rootName } }); } /** * assignedElements or assignedNodes call found * * @param {ESTree.CallExpression} node Node entered * @return {void} */ function handleAssignedCall(node) { if (litClassDepth === 0 || node.callee.type !== 'MemberExpression') return; const callee = node.callee; const methodName = getMethodName(callee); if (!methodName || callee.object.type !== 'CallExpression') { return; } const querySelectorCallExpr = callee.object; if (querySelectorCallExpr.callee.type !== 'MemberExpression') { return; } const querySelectorCallee = querySelectorCallExpr.callee; const rootName = getRenderRootName(querySelectorCallee); if (!rootName) { return; } const optionKey = assignedMessageMap.get(methodName); if (!optionKey || !options[optionKey]) { return; } const messageId = methodName === 'assignedElements' ? 'preferQueryAssignedElements' : 'preferQueryAssignedNodes'; context.report({ node, messageId, data: { root: rootName } }); } //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- return { ClassExpression: (node) => classEnter(node), ClassDeclaration: (node) => classEnter(node), 'ClassExpression:exit': (node) => classExit(node), 'ClassDeclaration:exit': (node) => classExit(node), [querySelectorCall]: (node) => handleQuerySelectorCall(node), [assignedCall]: (node) => handleAssignedCall(node) }; } };