@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.
- package/README.md +62 -0
- package/package.json +44 -0
- package/src/compat.mjs +53 -0
- package/src/index.mjs +56 -0
- package/src/rules/components_return_once.mjs +202 -0
- package/src/rules/event_handlers.mjs +298 -0
- package/src/rules/imports.mjs +205 -0
- package/src/rules/jsx_no_duplicate_props.mjs +87 -0
- package/src/rules/jsx_no_script_url.mjs +54 -0
- package/src/rules/jsx_no_undef.mjs +217 -0
- package/src/rules/jsx_uses_vars.mjs +55 -0
- package/src/rules/no_array_handlers.mjs +53 -0
- package/src/rules/no_destructure.mjs +210 -0
- package/src/rules/no_innerhtml.mjs +145 -0
- package/src/rules/no_proxy_apis.mjs +96 -0
- package/src/rules/no_react_deps.mjs +65 -0
- package/src/rules/no_react_specific_props.mjs +71 -0
- package/src/rules/no_unknown_namespaces.mjs +100 -0
- package/src/rules/prefer_arrow_components.mjs +411 -0
- package/src/rules/prefer_classlist.mjs +89 -0
- package/src/rules/prefer_for.mjs +92 -0
- package/src/rules/prefer_show.mjs +92 -0
- package/src/rules/reactivity.mjs +1300 -0
- package/src/rules/self_closing_comp.mjs +153 -0
- package/src/rules/style_prop.mjs +155 -0
- package/src/rules/validate_jsx_nesting.mjs +16 -0
- package/src/utils.mjs +337 -0
|
@@ -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
|
+
});
|