-
Notifications
You must be signed in to change notification settings - Fork 648
Expand file tree
/
Copy pathinject-checkbox-styles.ts
More file actions
186 lines (163 loc) · 6.71 KB
/
inject-checkbox-styles.ts
File metadata and controls
186 lines (163 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// Copyright 2020 The Chromium Authors
// 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 'node:path';
import {isLitHtmlTemplateCall} from './utils/lit.ts';
import {createRule} from './utils/ruleCreator.ts';
type AssignmentExpression = TSESTree.AssignmentExpression;
type TemplateElement = TSESTree.TemplateElement;
type Node = TSESTree.Node;
// Define MessageIds used in the rule
type MessageIds =|'missingCheckboxStylesImport'|'missingCheckboxStylesAdoption';
const FRONT_END_DIRECTORY = path.join(
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 or included in the template.',
},
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>();
let hasCheckboxStylesInTemplate = false;
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);
}
if (node.quasi.expressions.some(isCheckboxStylesReference)) {
hasCheckboxStylesInTemplate = true;
}
},
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 (hasCheckboxStylesInTemplate) {
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(isCheckboxStylesReference);
if (!inputCheckboxStylesAdoptionReference) {
context.report({
node: adoptedStyleSheetsCallNode, // Report on the assignment expression node
messageId: 'missingCheckboxStylesAdoption',
});
}
}
};
function isCheckboxStylesReference(elem: Node|null): boolean {
// Ensure elem is not null and is a MemberExpression before accessing properties
if (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;
}
}
});