chrome-devtools-frontend
Version:
Chrome DevTools UI
176 lines (154 loc) • 6.59 kB
text/typescript
// 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',
});
}
}
};
}
});