@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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -0
  3. package/dist/chunk-4UZNJ5MS.mjs +32 -0
  4. package/dist/chunk-4UZNJ5MS.mjs.map +1 -0
  5. package/dist/chunk-EBU4C5NV.mjs +77 -0
  6. package/dist/chunk-EBU4C5NV.mjs.map +1 -0
  7. package/dist/chunk-LE6SW2M3.mjs +6231 -0
  8. package/dist/chunk-LE6SW2M3.mjs.map +1 -0
  9. package/dist/chunk-MHDQJMLE.mjs +39 -0
  10. package/dist/chunk-MHDQJMLE.mjs.map +1 -0
  11. package/dist/configs/recommended.d.mts +109 -0
  12. package/dist/configs/recommended.d.ts +109 -0
  13. package/dist/configs/recommended.js +5877 -0
  14. package/dist/configs/recommended.js.map +1 -0
  15. package/dist/configs/recommended.mjs +5 -0
  16. package/dist/configs/recommended.mjs.map +1 -0
  17. package/dist/configs/typescript.d.mts +103 -0
  18. package/dist/configs/typescript.d.ts +103 -0
  19. package/dist/configs/typescript.js +6246 -0
  20. package/dist/configs/typescript.js.map +1 -0
  21. package/dist/configs/typescript.mjs +6 -0
  22. package/dist/configs/typescript.mjs.map +1 -0
  23. package/dist/configs/v2-strict.d.mts +130 -0
  24. package/dist/configs/v2-strict.d.ts +130 -0
  25. package/dist/configs/v2-strict.js +6317 -0
  26. package/dist/configs/v2-strict.js.map +1 -0
  27. package/dist/configs/v2-strict.mjs +7 -0
  28. package/dist/configs/v2-strict.mjs.map +1 -0
  29. package/dist/configs/v2.d.mts +127 -0
  30. package/dist/configs/v2.d.ts +127 -0
  31. package/dist/configs/v2.js +6289 -0
  32. package/dist/configs/v2.js.map +1 -0
  33. package/dist/configs/v2.mjs +6 -0
  34. package/dist/configs/v2.mjs.map +1 -0
  35. package/dist/index.d.mts +740 -0
  36. package/dist/index.d.ts +740 -0
  37. package/dist/index.js +6365 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/index.mjs +42 -0
  40. package/dist/index.mjs.map +1 -0
  41. package/package.json +109 -0
  42. package/src/compat.ts +53 -0
  43. package/src/configs/recommended.ts +48 -0
  44. package/src/configs/typescript.ts +21 -0
  45. package/src/configs/v2-strict.ts +28 -0
  46. package/src/configs/v2.ts +68 -0
  47. package/src/deps.d.ts +1 -0
  48. package/src/index.ts +29 -0
  49. package/src/plugin.ts +92 -0
  50. package/src/rules/components-return-once.ts +209 -0
  51. package/src/rules/event-handlers.ts +397 -0
  52. package/src/rules/imports.ts +338 -0
  53. package/src/rules/jsx-no-duplicate-props.ts +110 -0
  54. package/src/rules/jsx-no-script-url.ts +61 -0
  55. package/src/rules/jsx-no-undef.ts +230 -0
  56. package/src/rules/jsx-uses-vars.ts +66 -0
  57. package/src/rules/no-accessor-as-prop.ts +147 -0
  58. package/src/rules/no-array-handlers.ts +57 -0
  59. package/src/rules/no-boolean-enumerated-attribute.ts +209 -0
  60. package/src/rules/no-browser-globals-in-server-function.ts +108 -0
  61. package/src/rules/no-destructure.ts +245 -0
  62. package/src/rules/no-innerhtml.ts +143 -0
  63. package/src/rules/no-invalid-server-capture.ts +104 -0
  64. package/src/rules/no-module-scope-reactive-primitive.ts +69 -0
  65. package/src/rules/no-proxy-apis.ts +96 -0
  66. package/src/rules/no-react-deps.ts +68 -0
  67. package/src/rules/no-react-specific-props.ts +60 -0
  68. package/src/rules/no-reactive-read-in-effect-callback.ts +570 -0
  69. package/src/rules/no-restated-default-options.ts +96 -0
  70. package/src/rules/no-single-arg-create-effect.ts +56 -0
  71. package/src/rules/no-store-mutation-outside-setter.ts +137 -0
  72. package/src/rules/no-unknown-namespaces.ts +140 -0
  73. package/src/rules/no-unused-signal.ts +119 -0
  74. package/src/rules/no-write-in-pure-computation.ts +231 -0
  75. package/src/rules/prefer-classlist.ts +93 -0
  76. package/src/rules/prefer-for.ts +93 -0
  77. package/src/rules/prefer-onSettled-for-side-effects.ts +101 -0
  78. package/src/rules/prefer-show.ts +102 -0
  79. package/src/rules/prefer-structured-class.ts +117 -0
  80. package/src/rules/reactivity.ts +1730 -0
  81. package/src/rules/removed-api.ts +212 -0
  82. package/src/rules/require-async-server-function.ts +122 -0
  83. package/src/rules/self-closing-comp.ts +150 -0
  84. package/src/rules/style-prop.ts +140 -0
  85. package/src/rules/valid-use-server.ts +192 -0
  86. package/src/rules/validate-jsx-nesting.ts +1 -0
  87. package/src/utils/reactive-imports.ts +382 -0
  88. package/src/utils/reactive-types.ts +272 -0
  89. package/src/utils/reactive-values.ts +162 -0
  90. 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
+ });