@qq99/eslint-plugin-solid 0.18.0-no-reactive-read-in-effect-callback.0
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/LICENSE +21 -0
- package/README.md +289 -0
- package/dist/chunk-4UZNJ5MS.mjs +32 -0
- package/dist/chunk-4UZNJ5MS.mjs.map +1 -0
- package/dist/chunk-EBU4C5NV.mjs +77 -0
- package/dist/chunk-EBU4C5NV.mjs.map +1 -0
- package/dist/chunk-LE6SW2M3.mjs +6231 -0
- package/dist/chunk-LE6SW2M3.mjs.map +1 -0
- package/dist/chunk-MHDQJMLE.mjs +39 -0
- package/dist/chunk-MHDQJMLE.mjs.map +1 -0
- package/dist/configs/recommended.d.mts +109 -0
- package/dist/configs/recommended.d.ts +109 -0
- package/dist/configs/recommended.js +5877 -0
- package/dist/configs/recommended.js.map +1 -0
- package/dist/configs/recommended.mjs +5 -0
- package/dist/configs/recommended.mjs.map +1 -0
- package/dist/configs/typescript.d.mts +103 -0
- package/dist/configs/typescript.d.ts +103 -0
- package/dist/configs/typescript.js +6246 -0
- package/dist/configs/typescript.js.map +1 -0
- package/dist/configs/typescript.mjs +6 -0
- package/dist/configs/typescript.mjs.map +1 -0
- package/dist/configs/v2-strict.d.mts +130 -0
- package/dist/configs/v2-strict.d.ts +130 -0
- package/dist/configs/v2-strict.js +6317 -0
- package/dist/configs/v2-strict.js.map +1 -0
- package/dist/configs/v2-strict.mjs +7 -0
- package/dist/configs/v2-strict.mjs.map +1 -0
- package/dist/configs/v2.d.mts +127 -0
- package/dist/configs/v2.d.ts +127 -0
- package/dist/configs/v2.js +6289 -0
- package/dist/configs/v2.js.map +1 -0
- package/dist/configs/v2.mjs +6 -0
- package/dist/configs/v2.mjs.map +1 -0
- package/dist/index.d.mts +740 -0
- package/dist/index.d.ts +740 -0
- package/dist/index.js +6365 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +42 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +109 -0
- package/src/compat.ts +53 -0
- package/src/configs/recommended.ts +48 -0
- package/src/configs/typescript.ts +21 -0
- package/src/configs/v2-strict.ts +28 -0
- package/src/configs/v2.ts +68 -0
- package/src/deps.d.ts +1 -0
- package/src/index.ts +29 -0
- package/src/plugin.ts +92 -0
- package/src/rules/components-return-once.ts +209 -0
- package/src/rules/event-handlers.ts +397 -0
- package/src/rules/imports.ts +338 -0
- package/src/rules/jsx-no-duplicate-props.ts +110 -0
- package/src/rules/jsx-no-script-url.ts +61 -0
- package/src/rules/jsx-no-undef.ts +230 -0
- package/src/rules/jsx-uses-vars.ts +66 -0
- package/src/rules/no-accessor-as-prop.ts +147 -0
- package/src/rules/no-array-handlers.ts +57 -0
- package/src/rules/no-boolean-enumerated-attribute.ts +209 -0
- package/src/rules/no-browser-globals-in-server-function.ts +108 -0
- package/src/rules/no-destructure.ts +245 -0
- package/src/rules/no-innerhtml.ts +143 -0
- package/src/rules/no-invalid-server-capture.ts +104 -0
- package/src/rules/no-module-scope-reactive-primitive.ts +69 -0
- package/src/rules/no-proxy-apis.ts +96 -0
- package/src/rules/no-react-deps.ts +68 -0
- package/src/rules/no-react-specific-props.ts +60 -0
- package/src/rules/no-reactive-read-in-effect-callback.ts +570 -0
- package/src/rules/no-restated-default-options.ts +96 -0
- package/src/rules/no-single-arg-create-effect.ts +56 -0
- package/src/rules/no-store-mutation-outside-setter.ts +137 -0
- package/src/rules/no-unknown-namespaces.ts +140 -0
- package/src/rules/no-unused-signal.ts +119 -0
- package/src/rules/no-write-in-pure-computation.ts +231 -0
- package/src/rules/prefer-classlist.ts +93 -0
- package/src/rules/prefer-for.ts +93 -0
- package/src/rules/prefer-onSettled-for-side-effects.ts +101 -0
- package/src/rules/prefer-show.ts +102 -0
- package/src/rules/prefer-structured-class.ts +117 -0
- package/src/rules/reactivity.ts +1730 -0
- package/src/rules/removed-api.ts +212 -0
- package/src/rules/require-async-server-function.ts +122 -0
- package/src/rules/self-closing-comp.ts +150 -0
- package/src/rules/style-prop.ts +140 -0
- package/src/rules/valid-use-server.ts +192 -0
- package/src/rules/validate-jsx-nesting.ts +1 -0
- package/src/utils/reactive-imports.ts +382 -0
- package/src/utils/reactive-types.ts +272 -0
- package/src/utils/reactive-values.ts +162 -0
- package/src/utils.ts +442 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIXME: remove this comments and import when below issue is fixed.
|
|
3
|
+
* This import is necessary for type generation due to a bug in the TypeScript compiler.
|
|
4
|
+
* See: https://github.com/microsoft/TypeScript/issues/42873
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
8
|
+
|
|
9
|
+
import { ESLintUtils, TSESTree as T } from "@typescript-eslint/utils";
|
|
10
|
+
import { jsxHasProp, jsxPropName } from "../utils";
|
|
11
|
+
|
|
12
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
13
|
+
|
|
14
|
+
type MessageIds = "preferClasslist";
|
|
15
|
+
type Options = [{ classnames?: Array<string> }?];
|
|
16
|
+
|
|
17
|
+
export default createRule<Options, MessageIds>({
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
"Enforce using the classlist prop over importing a classnames helper. The classlist prop accepts an object `{ [class: string]: boolean }` just like classnames.",
|
|
23
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-classlist.md",
|
|
24
|
+
},
|
|
25
|
+
fixable: "code",
|
|
26
|
+
deprecated: true,
|
|
27
|
+
schema: [
|
|
28
|
+
{
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
classnames: {
|
|
32
|
+
type: "array",
|
|
33
|
+
description: "An array of names to treat as `classnames` functions",
|
|
34
|
+
default: ["cn", "clsx", "classnames"],
|
|
35
|
+
items: {
|
|
36
|
+
type: "string",
|
|
37
|
+
},
|
|
38
|
+
minItems: 1,
|
|
39
|
+
uniqueItems: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
messages: {
|
|
46
|
+
preferClasslist:
|
|
47
|
+
"The classlist prop should be used instead of {{ classnames }} to efficiently set classes based on an object.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
defaultOptions: [],
|
|
51
|
+
create(context) {
|
|
52
|
+
const classnames = context.options[0]?.classnames ?? ["cn", "clsx", "classnames"];
|
|
53
|
+
return {
|
|
54
|
+
JSXAttribute(node) {
|
|
55
|
+
if (
|
|
56
|
+
["class", "className"].indexOf(jsxPropName(node)) === -1 ||
|
|
57
|
+
jsxHasProp(
|
|
58
|
+
(node.parent as T.JSXOpeningElement | undefined)?.attributes ?? [],
|
|
59
|
+
"classlist"
|
|
60
|
+
)
|
|
61
|
+
) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (node.value?.type === "JSXExpressionContainer") {
|
|
65
|
+
const expr = node.value.expression;
|
|
66
|
+
if (
|
|
67
|
+
expr.type === "CallExpression" &&
|
|
68
|
+
expr.callee.type === "Identifier" &&
|
|
69
|
+
classnames.indexOf(expr.callee.name) !== -1 &&
|
|
70
|
+
expr.arguments.length === 1 &&
|
|
71
|
+
expr.arguments[0].type === "ObjectExpression"
|
|
72
|
+
) {
|
|
73
|
+
context.report({
|
|
74
|
+
node,
|
|
75
|
+
messageId: "preferClasslist",
|
|
76
|
+
data: {
|
|
77
|
+
classnames: expr.callee.name,
|
|
78
|
+
},
|
|
79
|
+
fix: (fixer) => {
|
|
80
|
+
const attrRange = node.range;
|
|
81
|
+
const objectRange = expr.arguments[0].range;
|
|
82
|
+
return [
|
|
83
|
+
fixer.replaceTextRange([attrRange[0], objectRange[0]], "classlist={"),
|
|
84
|
+
fixer.replaceTextRange([objectRange[1], attrRange[1]], "}"),
|
|
85
|
+
];
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIXME: remove this comments and import when below issue is fixed.
|
|
3
|
+
* This import is necessary for type generation due to a bug in the TypeScript compiler.
|
|
4
|
+
* See: https://github.com/microsoft/TypeScript/issues/42873
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
8
|
+
|
|
9
|
+
import { TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
|
|
10
|
+
import { isFunctionNode, isJSXElementOrFragment } from "../utils";
|
|
11
|
+
|
|
12
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
13
|
+
const { getPropertyName } = ASTUtils;
|
|
14
|
+
|
|
15
|
+
export default createRule({
|
|
16
|
+
meta: {
|
|
17
|
+
type: "problem",
|
|
18
|
+
docs: {
|
|
19
|
+
description:
|
|
20
|
+
"Enforce using Solid's `<For />` component for mapping an array to JSX elements.",
|
|
21
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-for.md",
|
|
22
|
+
},
|
|
23
|
+
fixable: "code",
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: {
|
|
26
|
+
preferFor:
|
|
27
|
+
"Use Solid's `<For />` component for efficiently rendering lists. Array#map causes DOM elements to be recreated.",
|
|
28
|
+
preferForOrIndex:
|
|
29
|
+
"Use Solid's `<For />` component or `<Index />` component for rendering lists. Array#map causes DOM elements to be recreated.",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
const reportPreferFor = (node: T.CallExpression) => {
|
|
35
|
+
const jsxExpressionContainerNode = node.parent as T.JSXExpressionContainer;
|
|
36
|
+
const arrayNode = (node.callee as T.MemberExpression).object;
|
|
37
|
+
const mapFnNode = node.arguments[0];
|
|
38
|
+
context.report({
|
|
39
|
+
node,
|
|
40
|
+
messageId: "preferFor",
|
|
41
|
+
fix: (fixer) => {
|
|
42
|
+
const beforeArray: [number, number] = [
|
|
43
|
+
jsxExpressionContainerNode.range[0],
|
|
44
|
+
arrayNode.range[0],
|
|
45
|
+
];
|
|
46
|
+
const betweenArrayAndMapFn: [number, number] = [arrayNode.range[1], mapFnNode.range[0]];
|
|
47
|
+
const afterMapFn: [number, number] = [
|
|
48
|
+
mapFnNode.range[1],
|
|
49
|
+
jsxExpressionContainerNode.range[1],
|
|
50
|
+
];
|
|
51
|
+
// We can insert the <For /> component
|
|
52
|
+
return [
|
|
53
|
+
fixer.replaceTextRange(beforeArray, "<For each={"),
|
|
54
|
+
fixer.replaceTextRange(betweenArrayAndMapFn, "}>{"),
|
|
55
|
+
fixer.replaceTextRange(afterMapFn, "}</For>"),
|
|
56
|
+
];
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
CallExpression(node) {
|
|
63
|
+
const callOrChain = node.parent?.type === "ChainExpression" ? node.parent : node;
|
|
64
|
+
if (
|
|
65
|
+
callOrChain.parent?.type === "JSXExpressionContainer" &&
|
|
66
|
+
isJSXElementOrFragment(callOrChain.parent.parent)
|
|
67
|
+
) {
|
|
68
|
+
// check for Array.prototype.map in JSX
|
|
69
|
+
if (
|
|
70
|
+
node.callee.type === "MemberExpression" &&
|
|
71
|
+
getPropertyName(node.callee) === "map" &&
|
|
72
|
+
node.arguments.length === 1 && // passing thisArg to Array.prototype.map is rare, deopt in that case
|
|
73
|
+
isFunctionNode(node.arguments[0])
|
|
74
|
+
) {
|
|
75
|
+
const mapFnNode = node.arguments[0];
|
|
76
|
+
if (mapFnNode.params.length === 1 && mapFnNode.params[0].type !== "RestElement") {
|
|
77
|
+
// The map fn doesn't take an index param, so it can't possibly be an index-keyed list. Use <For />.
|
|
78
|
+
// The returned JSX, if it's coming from React, will have an unnecessary `key` prop to be removed in
|
|
79
|
+
// the useless-keys rule.
|
|
80
|
+
reportPreferFor(node);
|
|
81
|
+
} else {
|
|
82
|
+
// Too many possible solutions to make a suggestion or fix
|
|
83
|
+
context.report({
|
|
84
|
+
node,
|
|
85
|
+
messageId: "preferForOrIndex",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIXME: remove this comments and import when below issue is fixed.
|
|
3
|
+
* This import is necessary for type generation due to a bug in the TypeScript compiler.
|
|
4
|
+
* See: https://github.com/microsoft/TypeScript/issues/42873
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
8
|
+
|
|
9
|
+
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
|
|
10
|
+
import {
|
|
11
|
+
findParent,
|
|
12
|
+
getFunctionName,
|
|
13
|
+
isFunctionNode,
|
|
14
|
+
isJSXElementOrFragment,
|
|
15
|
+
type FunctionNode,
|
|
16
|
+
} from "../utils";
|
|
17
|
+
|
|
18
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
19
|
+
|
|
20
|
+
type MessageIds = "sideEffectInBody";
|
|
21
|
+
type Options = [];
|
|
22
|
+
|
|
23
|
+
const TIMER_CALLS = new Set(["setInterval", "setTimeout"]);
|
|
24
|
+
const OBSERVER_CONSTRUCTORS = new Set([
|
|
25
|
+
"IntersectionObserver",
|
|
26
|
+
"ResizeObserver",
|
|
27
|
+
"MutationObserver",
|
|
28
|
+
"PerformanceObserver",
|
|
29
|
+
]);
|
|
30
|
+
const LISTENER_TARGETS = new Set(["window", "document"]);
|
|
31
|
+
|
|
32
|
+
/** Does this function look like a component (PascalCase name or returns JSX)? */
|
|
33
|
+
const isComponentLike = (fn: FunctionNode): boolean => {
|
|
34
|
+
const name = getFunctionName(fn);
|
|
35
|
+
if (name && /^[A-Z]/.test(name)) return true;
|
|
36
|
+
if (fn.type === "ArrowFunctionExpression" && isJSXElementOrFragment(fn.body)) return true;
|
|
37
|
+
if (fn.body.type === "BlockStatement") {
|
|
38
|
+
return fn.body.body.some(
|
|
39
|
+
(statement) =>
|
|
40
|
+
statement.type === "ReturnStatement" && isJSXElementOrFragment(statement.argument)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* Side-effectful setup in a component body runs during setup — including on
|
|
48
|
+
* the server during SSR, and before the DOM has settled. `onSettled` runs
|
|
49
|
+
* client-side at the right time. `onCleanup` itself is never flagged: it is
|
|
50
|
+
* legitimate and lower-overhead than an `onSettled` return-cleanup; a paired
|
|
51
|
+
* `onCleanup` is a symptom of setup work that belongs in `onSettled`, not the
|
|
52
|
+
* offense.
|
|
53
|
+
*/
|
|
54
|
+
export default createRule<Options, MessageIds>({
|
|
55
|
+
meta: {
|
|
56
|
+
type: "suggestion",
|
|
57
|
+
docs: {
|
|
58
|
+
description:
|
|
59
|
+
"Enforce running side-effectful setup (timers, global listeners, observers) inside `onSettled` instead of the component body.",
|
|
60
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-onSettled-for-side-effects.md",
|
|
61
|
+
},
|
|
62
|
+
schema: [],
|
|
63
|
+
messages: {
|
|
64
|
+
sideEffectInBody:
|
|
65
|
+
"{{name}} in the component body runs during setup, including on the server during SSR. Move it into `onSettled(() => { ...; return cleanup; })` so it runs client-side once the DOM has settled.",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
defaultOptions: [],
|
|
69
|
+
create(context) {
|
|
70
|
+
const report = (node: T.Node, name: string) => {
|
|
71
|
+
// Only flag calls made directly in a component body; the same call
|
|
72
|
+
// inside onSettled, an effect, or an event handler is fine.
|
|
73
|
+
const enclosingFunction = findParent(node, isFunctionNode);
|
|
74
|
+
if (!enclosingFunction || !isComponentLike(enclosingFunction)) return;
|
|
75
|
+
context.report({ node, messageId: "sideEffectInBody", data: { name } });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
CallExpression(node) {
|
|
80
|
+
const { callee } = node;
|
|
81
|
+
if (callee.type === "Identifier" && TIMER_CALLS.has(callee.name)) {
|
|
82
|
+
report(node, `\`${callee.name}\``);
|
|
83
|
+
} else if (
|
|
84
|
+
callee.type === "MemberExpression" &&
|
|
85
|
+
!callee.computed &&
|
|
86
|
+
callee.property.type === "Identifier" &&
|
|
87
|
+
callee.property.name === "addEventListener" &&
|
|
88
|
+
callee.object.type === "Identifier" &&
|
|
89
|
+
LISTENER_TARGETS.has(callee.object.name)
|
|
90
|
+
) {
|
|
91
|
+
report(node, `\`${callee.object.name}.addEventListener\``);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
NewExpression(node) {
|
|
95
|
+
if (node.callee.type === "Identifier" && OBSERVER_CONSTRUCTORS.has(node.callee.name)) {
|
|
96
|
+
report(node, `\`new ${node.callee.name}\``);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIXME: remove this comments and import when below issue is fixed.
|
|
3
|
+
* This import is necessary for type generation due to a bug in the TypeScript compiler.
|
|
4
|
+
* See: https://github.com/microsoft/TypeScript/issues/42873
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
8
|
+
|
|
9
|
+
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
|
|
10
|
+
import { isJSXElementOrFragment } from "../utils";
|
|
11
|
+
import { getSourceCode } from "../compat";
|
|
12
|
+
|
|
13
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
14
|
+
|
|
15
|
+
const EXPENSIVE_TYPES = ["JSXElement", "JSXFragment", "Identifier"];
|
|
16
|
+
|
|
17
|
+
export default createRule({
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description:
|
|
22
|
+
"Enforce using Solid's `<Show />` component for conditionally showing content. Solid's compiler covers this case, so it's a stylistic rule only.",
|
|
23
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-show.md",
|
|
24
|
+
},
|
|
25
|
+
fixable: "code",
|
|
26
|
+
schema: [],
|
|
27
|
+
messages: {
|
|
28
|
+
preferShowAnd: "Use Solid's `<Show />` component for conditionally showing content.",
|
|
29
|
+
preferShowTernary:
|
|
30
|
+
"Use Solid's `<Show />` component for conditionally showing content with a fallback.",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultOptions: [],
|
|
34
|
+
create(context) {
|
|
35
|
+
const sourceCode = getSourceCode(context);
|
|
36
|
+
const putIntoJSX = (node: T.Node): string => {
|
|
37
|
+
const text = sourceCode.getText(node);
|
|
38
|
+
return isJSXElementOrFragment(node) ? text : `{${text}}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const logicalExpressionHandler = (node: T.LogicalExpression) => {
|
|
42
|
+
if (node.operator === "&&" && EXPENSIVE_TYPES.includes(node.right.type)) {
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: "preferShowAnd",
|
|
46
|
+
fix: (fixer) =>
|
|
47
|
+
fixer.replaceText(
|
|
48
|
+
node.parent?.type === "JSXExpressionContainer" &&
|
|
49
|
+
isJSXElementOrFragment(node.parent.parent)
|
|
50
|
+
? node.parent
|
|
51
|
+
: node,
|
|
52
|
+
`<Show when={${sourceCode.getText(node.left)}}>${putIntoJSX(node.right)}</Show>`
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const conditionalExpressionHandler = (node: T.ConditionalExpression) => {
|
|
58
|
+
if (
|
|
59
|
+
EXPENSIVE_TYPES.includes(node.consequent.type) ||
|
|
60
|
+
EXPENSIVE_TYPES.includes(node.alternate.type)
|
|
61
|
+
) {
|
|
62
|
+
context.report({
|
|
63
|
+
node,
|
|
64
|
+
messageId: "preferShowTernary",
|
|
65
|
+
fix: (fixer) =>
|
|
66
|
+
fixer.replaceText(
|
|
67
|
+
node.parent?.type === "JSXExpressionContainer" &&
|
|
68
|
+
isJSXElementOrFragment(node.parent.parent)
|
|
69
|
+
? node.parent
|
|
70
|
+
: node,
|
|
71
|
+
`<Show when={${sourceCode.getText(node.test)}} fallback={${sourceCode.getText(
|
|
72
|
+
node.alternate
|
|
73
|
+
)}}>${putIntoJSX(node.consequent)}</Show>`
|
|
74
|
+
),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
JSXExpressionContainer(node) {
|
|
81
|
+
if (!isJSXElementOrFragment(node.parent)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (node.expression.type === "LogicalExpression") {
|
|
85
|
+
logicalExpressionHandler(node.expression);
|
|
86
|
+
} else if (
|
|
87
|
+
node.expression.type === "ArrowFunctionExpression" &&
|
|
88
|
+
node.expression.body.type === "LogicalExpression"
|
|
89
|
+
) {
|
|
90
|
+
logicalExpressionHandler(node.expression.body);
|
|
91
|
+
} else if (node.expression.type === "ConditionalExpression") {
|
|
92
|
+
conditionalExpressionHandler(node.expression);
|
|
93
|
+
} else if (
|
|
94
|
+
node.expression.type === "ArrowFunctionExpression" &&
|
|
95
|
+
node.expression.body.type === "ConditionalExpression"
|
|
96
|
+
) {
|
|
97
|
+
conditionalExpressionHandler(node.expression.body);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
import { getSourceCode } from "../compat";
|
|
4
|
+
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
6
|
+
|
|
7
|
+
type MessageIds = "manualClassString" | "useStructuredForm";
|
|
8
|
+
type Options = [];
|
|
9
|
+
|
|
10
|
+
/** Does this expression contain a runtime-varying part (conditional, logical, or call)? */
|
|
11
|
+
const containsDynamicPart = (node: T.Node): boolean => {
|
|
12
|
+
switch (node.type) {
|
|
13
|
+
case "ConditionalExpression":
|
|
14
|
+
case "LogicalExpression":
|
|
15
|
+
case "CallExpression":
|
|
16
|
+
return true;
|
|
17
|
+
case "BinaryExpression":
|
|
18
|
+
return (
|
|
19
|
+
node.operator === "+" && (containsDynamicPart(node.left) || containsDynamicPart(node.right))
|
|
20
|
+
);
|
|
21
|
+
case "TemplateLiteral":
|
|
22
|
+
return node.expressions.some(containsDynamicPart);
|
|
23
|
+
default:
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isStringLiteral = (node: T.Node): node is T.StringLiteral =>
|
|
29
|
+
node.type === "Literal" && typeof node.value === "string";
|
|
30
|
+
|
|
31
|
+
const isEmptyString = (node: T.Node): boolean => isStringLiteral(node) && node.value === "";
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* In Solid 2.0, `class` accepts arrays and objects natively
|
|
35
|
+
* (`ClassValue = string | number | boolean | null | undefined |
|
|
36
|
+
* Record<string, boolean> | ClassValue[]`), and toggles only the affected
|
|
37
|
+
* classes. Manually-built strings — the React/classnames reflex — still work
|
|
38
|
+
* but re-run the whole concatenation on every change. This rule nudges toward
|
|
39
|
+
* either structured form; it imposes no preference between array and object.
|
|
40
|
+
*/
|
|
41
|
+
export default createRule<Options, MessageIds>({
|
|
42
|
+
meta: {
|
|
43
|
+
type: "suggestion",
|
|
44
|
+
docs: {
|
|
45
|
+
description:
|
|
46
|
+
"Enforce using the structured array/object forms of the `class` prop over manually-built class strings.",
|
|
47
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-structured-class.md",
|
|
48
|
+
},
|
|
49
|
+
hasSuggestions: true,
|
|
50
|
+
schema: [],
|
|
51
|
+
messages: {
|
|
52
|
+
manualClassString:
|
|
53
|
+
"Manually building class strings re-runs the whole expression on every change. In Solid 2.0, `class` accepts arrays and objects directly: `class={[base, { active: active() }]}`.",
|
|
54
|
+
useStructuredForm: "Convert to the structured array form.",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultOptions: [],
|
|
58
|
+
create(context) {
|
|
59
|
+
const sourceCode = getSourceCode(context);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build a suggestion for the simple binary shape
|
|
63
|
+
* `"base " + (cond ? "cls" : "")` -> `["base", cond && "cls"]`.
|
|
64
|
+
* Returns null for anything more complex.
|
|
65
|
+
*/
|
|
66
|
+
const simpleBinarySuggestion = (
|
|
67
|
+
expression: T.Expression
|
|
68
|
+
): ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix) | null => {
|
|
69
|
+
if (expression.type !== "BinaryExpression" || expression.operator !== "+") return null;
|
|
70
|
+
const { left, right } = expression;
|
|
71
|
+
if (!isStringLiteral(left) || right.type !== "ConditionalExpression") return null;
|
|
72
|
+
if (!isEmptyString(right.alternate)) return null;
|
|
73
|
+
|
|
74
|
+
const base = left.value.trim();
|
|
75
|
+
if (!base) return null;
|
|
76
|
+
const test = sourceCode.getText(right.test);
|
|
77
|
+
const consequent = sourceCode.getText(right.consequent);
|
|
78
|
+
return (fixer) =>
|
|
79
|
+
fixer.replaceText(expression, `[${JSON.stringify(base)}, ${test} && ${consequent}]`);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
JSXAttribute(node) {
|
|
84
|
+
if (node.name.type !== "JSXIdentifier" || node.name.name !== "class") return;
|
|
85
|
+
if (node.value?.type !== "JSXExpressionContainer") return;
|
|
86
|
+
const expression = node.value.expression;
|
|
87
|
+
if (expression.type === "JSXEmptyExpression") return;
|
|
88
|
+
|
|
89
|
+
let manual = false;
|
|
90
|
+
if (expression.type === "TemplateLiteral") {
|
|
91
|
+
// `btn-${kind}` (plain interpolation) is fine; conditionals inside are not.
|
|
92
|
+
manual = expression.expressions.some(containsDynamicPart);
|
|
93
|
+
} else if (expression.type === "BinaryExpression" && expression.operator === "+") {
|
|
94
|
+
manual = containsDynamicPart(expression);
|
|
95
|
+
} else if (
|
|
96
|
+
expression.type === "CallExpression" &&
|
|
97
|
+
expression.callee.type === "MemberExpression" &&
|
|
98
|
+
expression.callee.property.type === "Identifier" &&
|
|
99
|
+
expression.callee.property.name === "join"
|
|
100
|
+
) {
|
|
101
|
+
// [..].join(" ") or [..].filter(Boolean).join(" ")
|
|
102
|
+
manual = true;
|
|
103
|
+
}
|
|
104
|
+
if (!manual) return;
|
|
105
|
+
|
|
106
|
+
const suggestionFix = simpleBinarySuggestion(expression);
|
|
107
|
+
context.report({
|
|
108
|
+
node: expression,
|
|
109
|
+
messageId: "manualClassString",
|
|
110
|
+
suggest: suggestionFix
|
|
111
|
+
? [{ messageId: "useStructuredForm", fix: suggestionFix }]
|
|
112
|
+
: undefined,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
});
|