UNPKG

chrome-devtools-frontend

Version:
176 lines (154 loc) 6.59 kB
// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type {TSESTree} from '@typescript-eslint/utils'; import path from 'path'; import {isLitHtmlTemplateCall} from './utils/lit.ts'; import {createRule} from './utils/ruleCreator.ts'; type AssignmentExpression = TSESTree.AssignmentExpression; type TemplateElement = TSESTree.TemplateElement; // Define MessageIds used in the rule type MessageIds =|'missingCheckboxStylesImport'|'missingCheckboxStylesAdoption'; const FRONT_END_DIRECTORY = path.join( // @ts-expect-error import.meta.dirname, '..', '..', '..', 'front_end', ); // NOTE: the actual file is input.ts, but for the sake of importing we want // input.js as that's what the import statement would reference. const COMMON_INPUT_STYLES = path.join(FRONT_END_DIRECTORY, 'ui', 'components', 'input', 'input.js'); export default createRule<[], MessageIds>({ name: 'inject-checkbox-styles', meta: { type: 'problem', docs: { description: 'Ensure common checkbox styles are imported in Lit components', category: 'Possible Errors', }, messages: { // Define messages corresponding to MessageIds missingCheckboxStylesImport: 'When rendering a checkbox, ensure the common checkbox styles are imported from components/input/input.ts.', missingCheckboxStylesAdoption: 'When rendering a checkbox, ensure the common checkbox styles are adopted into the component shadow root.', }, schema: [] }, defaultOptions: [], create: function(context) { const filename = context.filename; let foundInputStylesImport = false; let inputStylesImportedName: string|null = null; // Type the node explicitly let adoptedStyleSheetsCallNode: AssignmentExpression|null = null; // Use a more specific type for the set elements if possible, otherwise Node or TemplateElement const litCheckboxElements = new Set<TemplateElement>(); return { TaggedTemplateExpression(node) { // Assuming isLitHtmlTemplateCall is typed appropriately in utils.ts if (!isLitHtmlTemplateCall(node)) { return; } const litNodesContainingCheckbox = node.quasi.quasis.filter(element => { // element is TemplateElement return element.value.raw.includes('type="checkbox"'); }); for (const quasiNode of litNodesContainingCheckbox) { // We store the node so we can use it as the basis for the ESLint error later. litCheckboxElements.add(quasiNode); } }, ImportDeclaration(node) { if (foundInputStylesImport) { return; } // Ensure node.source.value is a string before resolving if (typeof node.source.value !== 'string') { return; } // Get the absolute path of the current file's directory, so we can // compare it to COMMON_INPUT_STYLES and see if the file does import the common styles. const absoluteDirectory = path.dirname(path.resolve(filename)); // Use try-catch for path resolution as it might fail for invalid paths try { const fullImportPath = path.resolve(absoluteDirectory, node.source.value); foundInputStylesImport = fullImportPath === COMMON_INPUT_STYLES; if (foundInputStylesImport) { // Ensure specifiers exist and the first one has a local name if (node.specifiers && node.specifiers.length > 0 && node.specifiers[0].local) { inputStylesImportedName = node.specifiers[0].local.name; } } } catch (e) { // Ignore path resolution errors console.error(`Error resolving import path: ${node.source.value} in ${filename}`, e); } }, AssignmentExpression(node: AssignmentExpression) { if (node.left.type === 'MemberExpression' && node.left.property.type === 'Identifier' && node.left.property.name === 'adoptedStyleSheets') { adoptedStyleSheetsCallNode = node; } }, 'Program:exit'() { if (litCheckboxElements.size === 0) { // No checkboxes to check, so we are done. return; } if (!foundInputStylesImport) { for (const checkbox of litCheckboxElements) { context.report({ node: checkbox, // checkbox is TemplateElement messageId: 'missingCheckboxStylesImport', }); } return; } if (!adoptedStyleSheetsCallNode) { for (const checkbox of litCheckboxElements) { context.report({ node: checkbox, // checkbox is TemplateElement messageId: 'missingCheckboxStylesAdoption', }); } return; } // Ensure the right side is an ArrayExpression before accessing elements if (adoptedStyleSheetsCallNode.right.type !== 'ArrayExpression') { // Handle cases where adoptedStyleSheets is assigned something other than an array // This might indicate an error or an unexpected pattern. // Depending on requirements, you might report an error or just return. return; } const inputCheckboxStylesAdoptionReference = adoptedStyleSheetsCallNode.right.elements.find(elem => { // Ensure elem is not null and is a MemberExpression before accessing properties if (!elem || elem.type !== 'MemberExpression') { return false; } // Ensure object and property are Identifiers before accessing name if (elem.object.type !== 'Identifier' || elem.property.type !== 'Identifier') { return false; } // Check that if we imported the styles as `Input`, that the reference here matches. // Use non-null assertion for inputStylesImportedName as it's checked by foundInputStylesImport logic if (elem.object.name !== inputStylesImportedName) { return false; } if (elem.property.name !== 'checkboxStyles') { return false; } return true; }); if (!inputCheckboxStylesAdoptionReference) { context.report({ node: adoptedStyleSheetsCallNode, // Report on the assignment expression node messageId: 'missingCheckboxStylesAdoption', }); } } }; } });