@murky-web/oxlint-plugin-solid 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,411 @@
1
+ const FALSE_SENTINEL = false;
2
+ const FIRST_INDEX = 0;
3
+ const LAST_INDEX = -1;
4
+ const FILE_START_RANGE = [FIRST_INDEX, FIRST_INDEX];
5
+ const NO_PARAMETERS = 0;
6
+ const ONE_PARAMETER = 1;
7
+ const PASCAL_CASE_COMPONENT_NAME = /^[A-Z][A-Za-z0-9]*$/;
8
+ const SOLID_IMPORT_SOURCE = "solid-js";
9
+
10
+ function isAstNode(value) {
11
+ return (
12
+ typeof value === "object" &&
13
+ value !== null &&
14
+ typeof value.type === "string"
15
+ );
16
+ }
17
+
18
+ function isExportedFunctionComponent(node) {
19
+ return (
20
+ node.type === "FunctionDeclaration" &&
21
+ node.id?.type === "Identifier" &&
22
+ isPascalCaseComponentName(node.id.name) &&
23
+ node.parent?.type === "ExportNamedDeclaration" &&
24
+ node.async !== true &&
25
+ node.generator !== true &&
26
+ !node.typeParameters
27
+ );
28
+ }
29
+
30
+ function isJsxLikeNode(node) {
31
+ return node?.type === "JSXElement" || node?.type === "JSXFragment";
32
+ }
33
+
34
+ function isPascalCaseComponentName(name) {
35
+ return PASCAL_CASE_COMPONENT_NAME.test(name);
36
+ }
37
+
38
+ function isPropsChildrenMember(node, propsIdentifierName) {
39
+ return (
40
+ node.type === "MemberExpression" &&
41
+ node.object?.type === "Identifier" &&
42
+ node.object.name === propsIdentifierName &&
43
+ node.property?.type === "Identifier" &&
44
+ node.property.name === "children" &&
45
+ node.computed === false
46
+ );
47
+ }
48
+
49
+ function isSolidImportDeclaration(statement) {
50
+ return (
51
+ statement.type === "ImportDeclaration" &&
52
+ statement.source?.value === SOLID_IMPORT_SOURCE
53
+ );
54
+ }
55
+
56
+ function getDesiredSolidTypeName(usesChildren) {
57
+ if (usesChildren) {
58
+ return "ParentComponent";
59
+ }
60
+
61
+ return "Component";
62
+ }
63
+
64
+ function getImportedName(specifier) {
65
+ if (specifier.imported.type === "Identifier") {
66
+ return specifier.imported.name;
67
+ }
68
+
69
+ return specifier.imported.value;
70
+ }
71
+
72
+ function getPropsParam(functionNode) {
73
+ if (functionNode.params.length === NO_PARAMETERS) {
74
+ return {
75
+ name: "",
76
+ text: "",
77
+ };
78
+ }
79
+
80
+ if (functionNode.params.length !== ONE_PARAMETER) {
81
+ return FALSE_SENTINEL;
82
+ }
83
+
84
+ const [param] = functionNode.params;
85
+ if (param.type !== "Identifier") {
86
+ return FALSE_SENTINEL;
87
+ }
88
+
89
+ return {
90
+ name: param.name,
91
+ text: param.name,
92
+ };
93
+ }
94
+
95
+ function getPropsSuffix(typeReference) {
96
+ if (typeReference === "") {
97
+ return "";
98
+ }
99
+
100
+ return `<${typeReference}>`;
101
+ }
102
+
103
+ function getSolidImportDeclarations(programNode) {
104
+ return programNode.body.filter((statement) => {
105
+ return isSolidImportDeclaration(statement);
106
+ });
107
+ }
108
+
109
+ function getTypeReferenceForProps(functionNode, sourceCode) {
110
+ if (functionNode.params.length === NO_PARAMETERS) {
111
+ return "";
112
+ }
113
+
114
+ if (functionNode.params.length !== ONE_PARAMETER) {
115
+ return FALSE_SENTINEL;
116
+ }
117
+
118
+ const [param] = functionNode.params;
119
+ if (param.type !== "Identifier" || !param.typeAnnotation) {
120
+ return FALSE_SENTINEL;
121
+ }
122
+
123
+ return sourceCode.getText(param.typeAnnotation.typeAnnotation);
124
+ }
125
+
126
+ function findLocalTypeName(importDeclarations, desiredTypeName) {
127
+ for (const statement of importDeclarations) {
128
+ for (const specifier of statement.specifiers) {
129
+ if (specifier.type !== "ImportSpecifier") {
130
+ continue;
131
+ }
132
+
133
+ if (getImportedName(specifier) === desiredTypeName) {
134
+ return specifier.local.name;
135
+ }
136
+ }
137
+ }
138
+
139
+ return "";
140
+ }
141
+
142
+ function hasJsxReturn(body) {
143
+ if (body?.type !== "BlockStatement") {
144
+ return false;
145
+ }
146
+
147
+ for (const statement of body.body) {
148
+ if (
149
+ statement.type === "ReturnStatement" &&
150
+ isJsxLikeNode(statement.argument)
151
+ ) {
152
+ return true;
153
+ }
154
+ }
155
+
156
+ return false;
157
+ }
158
+
159
+ function visitAstNode(node, visitor) {
160
+ if (!isAstNode(node)) {
161
+ return;
162
+ }
163
+
164
+ visitor(node);
165
+
166
+ for (const [key, value] of Object.entries(node)) {
167
+ if (key === "parent") {
168
+ continue;
169
+ }
170
+
171
+ if (Array.isArray(value)) {
172
+ for (const child of value) {
173
+ if (isAstNode(child)) {
174
+ visitAstNode(child, visitor);
175
+ }
176
+ }
177
+ continue;
178
+ }
179
+
180
+ if (isAstNode(value)) {
181
+ visitAstNode(value, visitor);
182
+ }
183
+ }
184
+ }
185
+
186
+ function usesPropsChildren(functionNode, propsIdentifierName) {
187
+ let found = false;
188
+
189
+ visitAstNode(functionNode.body, (node) => {
190
+ if (found || !isPropsChildrenMember(node, propsIdentifierName)) {
191
+ return;
192
+ }
193
+
194
+ found = true;
195
+ });
196
+
197
+ return found;
198
+ }
199
+
200
+ function getSolidTypeImportInfo(programNode, desiredTypeName) {
201
+ const importDeclarations = getSolidImportDeclarations(programNode);
202
+
203
+ return {
204
+ importDeclaration: importDeclarations.at(FIRST_INDEX),
205
+ localTypeName: findLocalTypeName(importDeclarations, desiredTypeName),
206
+ };
207
+ }
208
+
209
+ function getNamedImportSpecifiers(importDeclaration) {
210
+ return importDeclaration.specifiers.filter((specifier) => {
211
+ return specifier.type === "ImportSpecifier";
212
+ });
213
+ }
214
+
215
+ function getValueImportSpecifiers(importDeclaration) {
216
+ return importDeclaration.specifiers.filter((specifier) => {
217
+ return (
218
+ specifier.type === "ImportDefaultSpecifier" ||
219
+ specifier.type === "ImportNamespaceSpecifier"
220
+ );
221
+ });
222
+ }
223
+
224
+ function getTypeKeyword(importDeclaration, sourceCode) {
225
+ if (sourceCode.getText(importDeclaration).startsWith("import type")) {
226
+ return "";
227
+ }
228
+
229
+ return "type ";
230
+ }
231
+
232
+ function insertSolidTypeIntoExistingImport({
233
+ desiredTypeName,
234
+ fixer,
235
+ importDeclaration,
236
+ sourceCode,
237
+ }) {
238
+ const namedSpecifiers = getNamedImportSpecifiers(importDeclaration);
239
+ if (namedSpecifiers.length > NO_PARAMETERS) {
240
+ const lastNamedSpecifier = namedSpecifiers.at(LAST_INDEX);
241
+ const typeKeyword = getTypeKeyword(importDeclaration, sourceCode);
242
+ return fixer.insertTextAfter(
243
+ lastNamedSpecifier,
244
+ `, ${typeKeyword}${desiredTypeName}`,
245
+ );
246
+ }
247
+
248
+ const valueSpecifiers = getValueImportSpecifiers(importDeclaration);
249
+ if (valueSpecifiers.length > NO_PARAMETERS) {
250
+ const lastValueSpecifier = valueSpecifiers.at(LAST_INDEX);
251
+ return fixer.insertTextAfter(
252
+ lastValueSpecifier,
253
+ `, { type ${desiredTypeName} }`,
254
+ );
255
+ }
256
+
257
+ return fixer.insertTextBefore(
258
+ importDeclaration,
259
+ `import type { ${desiredTypeName} } from "solid-js";\n`,
260
+ );
261
+ }
262
+
263
+ function createImportFix({ desiredTypeName, fixer, programNode, sourceCode }) {
264
+ const { importDeclaration, localTypeName } = getSolidTypeImportInfo(
265
+ programNode,
266
+ desiredTypeName,
267
+ );
268
+
269
+ if (localTypeName !== "") {
270
+ return {
271
+ localTypeName,
272
+ operation: null,
273
+ };
274
+ }
275
+
276
+ if (importDeclaration) {
277
+ return {
278
+ localTypeName: desiredTypeName,
279
+ operation: insertSolidTypeIntoExistingImport({
280
+ desiredTypeName,
281
+ fixer,
282
+ importDeclaration,
283
+ sourceCode,
284
+ }),
285
+ };
286
+ }
287
+
288
+ return {
289
+ localTypeName: desiredTypeName,
290
+ operation: fixer.insertTextBeforeRange(
291
+ FILE_START_RANGE,
292
+ `import type { ${desiredTypeName} } from "solid-js";\n`,
293
+ ),
294
+ };
295
+ }
296
+
297
+ function buildReplacementText({
298
+ componentName,
299
+ localTypeName,
300
+ propsParam,
301
+ sourceCode,
302
+ typeReference,
303
+ usesChildren,
304
+ functionNode,
305
+ }) {
306
+ const asyncPrefix = "";
307
+ const childrenComment = usesChildren ? "" : "";
308
+ const propsSuffix = getPropsSuffix(typeReference);
309
+ const paramsText = propsParam.text;
310
+ const bodyText = sourceCode.getText(functionNode.body);
311
+
312
+ return `export const ${componentName}: ${localTypeName}${propsSuffix} = (${paramsText}) => ${bodyText}${asyncPrefix}${childrenComment}`;
313
+ }
314
+
315
+ function createRuleFix({
316
+ fixer,
317
+ functionNode,
318
+ localTypeName,
319
+ propsParam,
320
+ sourceCode,
321
+ typeReference,
322
+ usesChildren,
323
+ }) {
324
+ const componentName = functionNode.id.name;
325
+ const replacementText = buildReplacementText({
326
+ componentName,
327
+ functionNode,
328
+ localTypeName,
329
+ propsParam,
330
+ sourceCode,
331
+ typeReference,
332
+ usesChildren,
333
+ });
334
+
335
+ return fixer.replaceText(functionNode.parent, replacementText);
336
+ }
337
+
338
+ export const preferArrowComponentsRule = {
339
+ meta: {
340
+ docs: {
341
+ description:
342
+ "Prefer Solid components written as arrow consts typed with Component or ParentComponent.",
343
+ recommended: false,
344
+ },
345
+ fixable: "code",
346
+ schema: [],
347
+ type: "suggestion",
348
+ },
349
+ create(context) {
350
+ const sourceCode = context.sourceCode;
351
+ const programNode = sourceCode.ast;
352
+
353
+ return {
354
+ FunctionDeclaration(node) {
355
+ if (
356
+ !isExportedFunctionComponent(node) ||
357
+ !hasJsxReturn(node.body)
358
+ ) {
359
+ return;
360
+ }
361
+
362
+ const propsParam = getPropsParam(node);
363
+ const typeReference = getTypeReferenceForProps(
364
+ node,
365
+ sourceCode,
366
+ );
367
+ if (
368
+ propsParam === FALSE_SENTINEL ||
369
+ typeReference === FALSE_SENTINEL
370
+ ) {
371
+ return;
372
+ }
373
+
374
+ const usesChildren =
375
+ propsParam.name !== "" &&
376
+ usesPropsChildren(node, propsParam.name);
377
+ const desiredTypeName = getDesiredSolidTypeName(usesChildren);
378
+
379
+ context.report({
380
+ fix(fixer) {
381
+ const importFix = createImportFix({
382
+ desiredTypeName,
383
+ fixer,
384
+ programNode,
385
+ sourceCode,
386
+ });
387
+ const replacementFix = createRuleFix({
388
+ fixer,
389
+ functionNode: node,
390
+ localTypeName: importFix.localTypeName,
391
+ propsParam,
392
+ sourceCode,
393
+ typeReference,
394
+ usesChildren,
395
+ });
396
+
397
+ return [
398
+ ...(importFix.operation
399
+ ? [importFix.operation]
400
+ : []),
401
+ replacementFix,
402
+ ];
403
+ },
404
+ message:
405
+ "Prefer export const components typed as Component<Props> or ParentComponent<Props>.",
406
+ node,
407
+ });
408
+ },
409
+ };
410
+ },
411
+ };
@@ -0,0 +1,89 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ import { jsxHasProp, jsxPropName } from "../utils.mjs";
4
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
5
+ export default createRule({
6
+ meta: {
7
+ type: "problem",
8
+ docs: {
9
+ description:
10
+ "Enforce using the classlist prop over importing a classnames helper. The classlist prop accepts an object `{ [class: string]: boolean }` just like classnames.",
11
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-classlist.md",
12
+ },
13
+ fixable: "code",
14
+ deprecated: true,
15
+ schema: [
16
+ {
17
+ type: "object",
18
+ properties: {
19
+ classnames: {
20
+ type: "array",
21
+ description:
22
+ "An array of names to treat as `classnames` functions",
23
+ default: ["cn", "clsx", "classnames"],
24
+ items: {
25
+ type: "string",
26
+ },
27
+ minItems: 1,
28
+ uniqueItems: true,
29
+ },
30
+ },
31
+ additionalProperties: false,
32
+ },
33
+ ],
34
+ messages: {
35
+ preferClasslist:
36
+ "The classlist prop should be used instead of {{ classnames }} to efficiently set classes based on an object.",
37
+ },
38
+ },
39
+ defaultOptions: [],
40
+ create(context) {
41
+ const classnames = context.options[0]?.classnames ?? [
42
+ "cn",
43
+ "clsx",
44
+ "classnames",
45
+ ];
46
+ return {
47
+ JSXAttribute(node) {
48
+ if (
49
+ ["class", "className"].indexOf(jsxPropName(node)) === -1 ||
50
+ jsxHasProp(node.parent?.attributes ?? [], "classlist")
51
+ ) {
52
+ return;
53
+ }
54
+ if (node.value?.type === "JSXExpressionContainer") {
55
+ const expr = node.value.expression;
56
+ if (
57
+ expr.type === "CallExpression" &&
58
+ expr.callee.type === "Identifier" &&
59
+ classnames.indexOf(expr.callee.name) !== -1 &&
60
+ expr.arguments.length === 1 &&
61
+ expr.arguments[0].type === "ObjectExpression"
62
+ ) {
63
+ context.report({
64
+ node,
65
+ messageId: "preferClasslist",
66
+ data: {
67
+ classnames: expr.callee.name,
68
+ },
69
+ fix: (fixer) => {
70
+ const attrRange = node.range;
71
+ const objectRange = expr.arguments[0].range;
72
+ return [
73
+ fixer.replaceTextRange(
74
+ [attrRange[0], objectRange[0]],
75
+ "classlist={",
76
+ ),
77
+ fixer.replaceTextRange(
78
+ [objectRange[1], attrRange[1]],
79
+ "}",
80
+ ),
81
+ ];
82
+ },
83
+ });
84
+ }
85
+ }
86
+ },
87
+ };
88
+ },
89
+ });
@@ -0,0 +1,92 @@
1
+ import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
2
+
3
+ import { isFunctionNode, isJSXElementOrFragment } from "../utils.mjs";
4
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
5
+ const { getPropertyName } = ASTUtils;
6
+ export default createRule({
7
+ meta: {
8
+ type: "problem",
9
+ docs: {
10
+ description:
11
+ "Enforce using Solid's `<For />` component for mapping an array to JSX elements.",
12
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-for.md",
13
+ },
14
+ fixable: "code",
15
+ schema: [],
16
+ messages: {
17
+ preferFor:
18
+ "Use Solid's `<For />` component for efficiently rendering lists. Array#map causes DOM elements to be recreated.",
19
+ preferForOrIndex:
20
+ "Use Solid's `<For />` component or `<Index />` component for rendering lists. Array#map causes DOM elements to be recreated.",
21
+ },
22
+ },
23
+ defaultOptions: [],
24
+ create(context) {
25
+ const reportPreferFor = (node) => {
26
+ const jsxExpressionContainerNode = node.parent;
27
+ const arrayNode = node.callee.object;
28
+ const mapFnNode = node.arguments[0];
29
+ context.report({
30
+ node,
31
+ messageId: "preferFor",
32
+ fix: (fixer) => {
33
+ const beforeArray = [
34
+ jsxExpressionContainerNode.range[0],
35
+ arrayNode.range[0],
36
+ ];
37
+ const betweenArrayAndMapFn = [
38
+ arrayNode.range[1],
39
+ mapFnNode.range[0],
40
+ ];
41
+ const afterMapFn = [
42
+ mapFnNode.range[1],
43
+ jsxExpressionContainerNode.range[1],
44
+ ];
45
+ // We can insert the <For /> component
46
+ return [
47
+ fixer.replaceTextRange(beforeArray, "<For each={"),
48
+ fixer.replaceTextRange(betweenArrayAndMapFn, "}>{"),
49
+ fixer.replaceTextRange(afterMapFn, "}</For>"),
50
+ ];
51
+ },
52
+ });
53
+ };
54
+ return {
55
+ CallExpression(node) {
56
+ const callOrChain =
57
+ node.parent?.type === "ChainExpression"
58
+ ? node.parent
59
+ : node;
60
+ if (
61
+ callOrChain.parent?.type === "JSXExpressionContainer" &&
62
+ isJSXElementOrFragment(callOrChain.parent.parent)
63
+ ) {
64
+ // check for Array.prototype.map in JSX
65
+ if (
66
+ node.callee.type === "MemberExpression" &&
67
+ getPropertyName(node.callee) === "map" &&
68
+ node.arguments.length === 1 && // passing thisArg to Array.prototype.map is rare, deopt in that case
69
+ isFunctionNode(node.arguments[0])
70
+ ) {
71
+ const mapFnNode = node.arguments[0];
72
+ if (
73
+ mapFnNode.params.length === 1 &&
74
+ mapFnNode.params[0].type !== "RestElement"
75
+ ) {
76
+ // The map fn doesn't take an index param, so it can't possibly be an index-keyed list. Use <For />.
77
+ // The returned JSX, if it's coming from React, will have an unnecessary `key` prop to be removed in
78
+ // the useless-keys rule.
79
+ reportPreferFor(node);
80
+ } else {
81
+ // Too many possible solutions to make a suggestion or fix
82
+ context.report({
83
+ node,
84
+ messageId: "preferForOrIndex",
85
+ });
86
+ }
87
+ }
88
+ }
89
+ },
90
+ };
91
+ },
92
+ });
@@ -0,0 +1,92 @@
1
+ import { ESLintUtils } from "@typescript-eslint/utils";
2
+
3
+ import { getSourceCode } from "../compat.mjs";
4
+ import { isJSXElementOrFragment } from "../utils.mjs";
5
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
6
+ const EXPENSIVE_TYPES = ["JSXElement", "JSXFragment", "Identifier"];
7
+ export default createRule({
8
+ meta: {
9
+ type: "problem",
10
+ docs: {
11
+ description:
12
+ "Enforce using Solid's `<Show />` component for conditionally showing content. Solid's compiler covers this case, so it's a stylistic rule only.",
13
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-show.md",
14
+ },
15
+ fixable: "code",
16
+ schema: [],
17
+ messages: {
18
+ preferShowAnd:
19
+ "Use Solid's `<Show />` component for conditionally showing content.",
20
+ preferShowTernary:
21
+ "Use Solid's `<Show />` component for conditionally showing content with a fallback.",
22
+ },
23
+ },
24
+ defaultOptions: [],
25
+ create(context) {
26
+ const sourceCode = getSourceCode(context);
27
+ const putIntoJSX = (node) => {
28
+ const text = sourceCode.getText(node);
29
+ return isJSXElementOrFragment(node) ? text : `{${text}}`;
30
+ };
31
+ const logicalExpressionHandler = (node) => {
32
+ if (
33
+ node.operator === "&&" &&
34
+ EXPENSIVE_TYPES.includes(node.right.type)
35
+ ) {
36
+ context.report({
37
+ node,
38
+ messageId: "preferShowAnd",
39
+ fix: (fixer) =>
40
+ fixer.replaceText(
41
+ node.parent?.type === "JSXExpressionContainer" &&
42
+ isJSXElementOrFragment(node.parent.parent)
43
+ ? node.parent
44
+ : node,
45
+ `<Show when={${sourceCode.getText(node.left)}}>${putIntoJSX(node.right)}</Show>`,
46
+ ),
47
+ });
48
+ }
49
+ };
50
+ const conditionalExpressionHandler = (node) => {
51
+ if (
52
+ EXPENSIVE_TYPES.includes(node.consequent.type) ||
53
+ EXPENSIVE_TYPES.includes(node.alternate.type)
54
+ ) {
55
+ context.report({
56
+ node,
57
+ messageId: "preferShowTernary",
58
+ fix: (fixer) =>
59
+ fixer.replaceText(
60
+ node.parent?.type === "JSXExpressionContainer" &&
61
+ isJSXElementOrFragment(node.parent.parent)
62
+ ? node.parent
63
+ : node,
64
+ `<Show when={${sourceCode.getText(node.test)}} fallback={${sourceCode.getText(node.alternate)}}>${putIntoJSX(node.consequent)}</Show>`,
65
+ ),
66
+ });
67
+ }
68
+ };
69
+ return {
70
+ JSXExpressionContainer(node) {
71
+ if (!isJSXElementOrFragment(node.parent)) {
72
+ return;
73
+ }
74
+ if (node.expression.type === "LogicalExpression") {
75
+ logicalExpressionHandler(node.expression);
76
+ } else if (
77
+ node.expression.type === "ArrowFunctionExpression" &&
78
+ node.expression.body.type === "LogicalExpression"
79
+ ) {
80
+ logicalExpressionHandler(node.expression.body);
81
+ } else if (node.expression.type === "ConditionalExpression") {
82
+ conditionalExpressionHandler(node.expression);
83
+ } else if (
84
+ node.expression.type === "ArrowFunctionExpression" &&
85
+ node.expression.body.type === "ConditionalExpression"
86
+ ) {
87
+ conditionalExpressionHandler(node.expression.body);
88
+ }
89
+ },
90
+ };
91
+ },
92
+ });