@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,209 @@
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 { getSourceCode } from "../compat";
11
+
12
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
13
+
14
+ type MessageIds = "booleanValue" | "booleanExpression" | "useStrings";
15
+ type Options = [];
16
+
17
+ interface AttributePolicy {
18
+ /** The string tokens meaning on/off for this attribute. */
19
+ tokens: [trueToken: string, falseToken: string];
20
+ /** Why a boolean/bare `true` misbehaves; undefined when `true` is harmless. */
21
+ trueBehavior?: string;
22
+ /** Why a boolean `false` misbehaves; the primary hazard for every entry. */
23
+ falseBehavior: string;
24
+ }
25
+
26
+ const INHERITED = (token: string) =>
27
+ `a boolean false removes the attribute, leaving the inherited state rather than "${token}"`;
28
+ const ARIA_FALSE =
29
+ 'a boolean false removes the attribute, producing the "undefined" state rather than "false"';
30
+ const ARIA_TRUE =
31
+ 'a boolean true writes an empty attribute value, which assistive technologies do not reliably treat as "true"';
32
+
33
+ /**
34
+ * Enumerated attributes whose boolean forms misbehave. Deliberately excludes
35
+ * ARIA attributes that default to "false" (aria-disabled, aria-busy, ...):
36
+ * for those, removing the attribute IS the false state, so a boolean false is
37
+ * harmless and flagging it would be noise.
38
+ */
39
+ const ATTRIBUTES = new Map<string, AttributePolicy>([
40
+ [
41
+ "draggable",
42
+ {
43
+ tokens: ["true", "false"],
44
+ trueBehavior:
45
+ 'a bare or boolean-true draggable writes an empty value, which is invalid and behaves as "auto"',
46
+ falseBehavior: 'a boolean false removes the attribute, which behaves as "auto", not "false"',
47
+ },
48
+ ],
49
+ ["spellcheck", { tokens: ["true", "false"], falseBehavior: INHERITED("false") }],
50
+ ["contenteditable", { tokens: ["true", "false"], falseBehavior: INHERITED("false") }],
51
+ ["translate", { tokens: ["yes", "no"], falseBehavior: INHERITED("no") }],
52
+ // ARIA tristate/undefined-default states: absence means "undefined" (not
53
+ // checkable / not expandable / hidden-ness computed from the tree), which is
54
+ // a different state from an explicit "false".
55
+ [
56
+ "aria-checked",
57
+ { tokens: ["true", "false"], trueBehavior: ARIA_TRUE, falseBehavior: ARIA_FALSE },
58
+ ],
59
+ [
60
+ "aria-expanded",
61
+ { tokens: ["true", "false"], trueBehavior: ARIA_TRUE, falseBehavior: ARIA_FALSE },
62
+ ],
63
+ [
64
+ "aria-hidden",
65
+ { tokens: ["true", "false"], trueBehavior: ARIA_TRUE, falseBehavior: ARIA_FALSE },
66
+ ],
67
+ [
68
+ "aria-pressed",
69
+ { tokens: ["true", "false"], trueBehavior: ARIA_TRUE, falseBehavior: ARIA_FALSE },
70
+ ],
71
+ [
72
+ "aria-selected",
73
+ { tokens: ["true", "false"], trueBehavior: ARIA_TRUE, falseBehavior: ARIA_FALSE },
74
+ ],
75
+ ]);
76
+
77
+ const BOOLEAN_OPERATORS = new Set([
78
+ "===",
79
+ "!==",
80
+ "==",
81
+ "!=",
82
+ "<",
83
+ ">",
84
+ "<=",
85
+ ">=",
86
+ "in",
87
+ "instanceof",
88
+ ]);
89
+
90
+ /** Is this expression provably boolean-valued without type information? */
91
+ const isProvablyBoolean = (node: T.Node): boolean => {
92
+ switch (node.type) {
93
+ case "Literal":
94
+ return typeof node.value === "boolean";
95
+ case "UnaryExpression":
96
+ return node.operator === "!";
97
+ case "BinaryExpression":
98
+ return BOOLEAN_OPERATORS.has(node.operator);
99
+ case "LogicalExpression":
100
+ return (
101
+ node.operator !== "??" && isProvablyBoolean(node.left) && isProvablyBoolean(node.right)
102
+ );
103
+ default:
104
+ return false;
105
+ }
106
+ };
107
+
108
+ /*
109
+ * Enumerated attributes look boolean but aren't: they want literal string
110
+ * tokens ("true"/"false", or "yes"/"no" for translate). Solid's attribute
111
+ * path removes the attribute entirely when the value is `false` and writes an
112
+ * empty string for `true`, so booleans silently produce a different state
113
+ * than the matching token would. The rule only reports values that provably
114
+ * misbehave: boolean literals in the broken direction, bare `draggable`, and
115
+ * expressions that are boolean by construction. Dynamic values whose type
116
+ * can't be proven locally are left alone.
117
+ */
118
+ export default createRule<Options, MessageIds>({
119
+ meta: {
120
+ type: "problem",
121
+ docs: {
122
+ description:
123
+ 'Disallow boolean values on enumerated attributes like draggable and tristate aria-*, which take string tokens — a boolean false removes the attribute instead of writing "false".',
124
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-boolean-enumerated-attribute.md",
125
+ },
126
+ fixable: "code",
127
+ hasSuggestions: true,
128
+ schema: [],
129
+ messages: {
130
+ booleanValue:
131
+ "'{{name}}' is an enumerated attribute, not a boolean one: {{behavior}}. Use the string \"{{token}}\".",
132
+ booleanExpression:
133
+ "'{{name}}' is an enumerated attribute, not a boolean one: {{behavior}}. Map the expression to its string tokens.",
134
+ useStrings: 'Write the string tokens: {expr ? "{{trueToken}}" : "{{falseToken}}"}.',
135
+ },
136
+ },
137
+ defaultOptions: [],
138
+ create(context) {
139
+ const sourceCode = getSourceCode(context);
140
+
141
+ return {
142
+ JSXAttribute(node) {
143
+ // Only native elements: components receive these as ordinary props.
144
+ const element = node.parent;
145
+ if (
146
+ element?.type !== "JSXOpeningElement" ||
147
+ element.name.type !== "JSXIdentifier" ||
148
+ !/^[a-z]/.test(element.name.name)
149
+ ) {
150
+ return;
151
+ }
152
+ if (node.name.type !== "JSXIdentifier") return;
153
+ const name = node.name.name;
154
+ const policy = ATTRIBUTES.get(name);
155
+ if (!policy) return;
156
+ const [trueToken, falseToken] = policy.tokens;
157
+
158
+ // A bare attribute compiles to `true`, i.e. an empty attribute value —
159
+ // valid for the empty-string-means-true attributes, broken elsewhere.
160
+ if (!node.value) {
161
+ if (policy.trueBehavior) {
162
+ context.report({
163
+ node,
164
+ messageId: "booleanValue",
165
+ data: { name, behavior: policy.trueBehavior, token: trueToken },
166
+ fix: (fixer) => fixer.insertTextAfter(node, `="${trueToken}"`),
167
+ });
168
+ }
169
+ return;
170
+ }
171
+
172
+ if (node.value.type !== "JSXExpressionContainer") return;
173
+ const expression = node.value.expression;
174
+
175
+ if (expression.type === "Literal" && typeof expression.value === "boolean") {
176
+ const behavior = expression.value ? policy.trueBehavior : policy.falseBehavior;
177
+ if (!behavior) return; // boolean true is harmless here
178
+ const token = expression.value ? trueToken : falseToken;
179
+ context.report({
180
+ node: node.value,
181
+ messageId: "booleanValue",
182
+ data: { name, behavior, token },
183
+ fix: (fixer) => fixer.replaceText(node.value!, `"${token}"`),
184
+ });
185
+ return;
186
+ }
187
+
188
+ if (isProvablyBoolean(expression)) {
189
+ context.report({
190
+ node: node.value,
191
+ messageId: "booleanExpression",
192
+ data: { name, behavior: policy.falseBehavior },
193
+ suggest: [
194
+ {
195
+ messageId: "useStrings",
196
+ data: { trueToken, falseToken },
197
+ fix: (fixer) =>
198
+ fixer.replaceText(
199
+ node.value!,
200
+ `{${sourceCode.getText(expression)} ? "${trueToken}" : "${falseToken}"}`
201
+ ),
202
+ },
203
+ ],
204
+ });
205
+ }
206
+ },
207
+ };
208
+ },
209
+ });
@@ -0,0 +1,108 @@
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, TSESLint as Lint, ESLintUtils } from "@typescript-eslint/utils";
10
+ import {
11
+ isFunctionNode,
12
+ hasUseServerDirective,
13
+ getUseServerEligibility,
14
+ isInsideUseServerFunction,
15
+ programHasUseServerDirective,
16
+ } from "../utils";
17
+ import { getSourceCode } from "../compat";
18
+
19
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
20
+
21
+ type MessageIds = "browserGlobal";
22
+ type Options = [];
23
+
24
+ /**
25
+ * Globals that exist only in browsers. Deliberately conservative: server
26
+ * runtimes (Node, Deno, Bun, edge workers) provide `fetch`, `Response`,
27
+ * `FormData`, `URL`, `crypto`, and even `navigator`, so only unambiguous
28
+ * DOM/BOM globals are listed.
29
+ */
30
+ const BROWSER_GLOBALS = new Set([
31
+ "window",
32
+ "document",
33
+ "alert",
34
+ "confirm",
35
+ "prompt",
36
+ "localStorage",
37
+ "sessionStorage",
38
+ "history",
39
+ "location",
40
+ "matchMedia",
41
+ "getComputedStyle",
42
+ "requestAnimationFrame",
43
+ "cancelAnimationFrame",
44
+ "customElements",
45
+ ]);
46
+
47
+ export default createRule<Options, MessageIds>({
48
+ meta: {
49
+ type: "problem",
50
+ docs: {
51
+ description:
52
+ "Disallow browser-only globals inside server functions, which run exclusively on the server.",
53
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-browser-globals-in-server-function.md",
54
+ },
55
+ schema: [],
56
+ messages: {
57
+ browserGlobal:
58
+ "'{{name}}' is a browser global, and server functions only run on the server, where it does not exist. Read request data from the function's arguments or use server platform APIs instead.",
59
+ },
60
+ },
61
+ defaultOptions: [],
62
+ create(context) {
63
+ const sourceCode = getSourceCode(context);
64
+ let moduleLevel = false;
65
+
66
+ const checkReferences = (references: Lint.Scope.Reference[]) => {
67
+ for (const reference of references) {
68
+ const name = reference.identifier.name;
69
+ if (!BROWSER_GLOBALS.has(name)) continue;
70
+ // A binding with definitions is the user's own variable; a def-less
71
+ // resolution is a configured global (e.g. `globals.browser`).
72
+ if (reference.resolved && reference.resolved.defs.length > 0) continue;
73
+ // `typeof window` guards are dead code here but intentional
74
+ const parent = reference.identifier.parent;
75
+ if (parent?.type === "UnaryExpression" && parent.operator === "typeof") continue;
76
+ context.report({
77
+ node: reference.identifier,
78
+ messageId: "browserGlobal",
79
+ data: { name },
80
+ });
81
+ }
82
+ };
83
+
84
+ return {
85
+ Program(node) {
86
+ moduleLevel = programHasUseServerDirective(node);
87
+ if (moduleLevel) {
88
+ // The whole module runs on the server; check every reference in it.
89
+ // References resolved to configured globals (`globals.browser`)
90
+ // never reach a scope's `through`, so walk each scope directly.
91
+ for (const scope of sourceCode.scopeManager?.scopes ?? []) {
92
+ checkReferences(scope.references);
93
+ }
94
+ }
95
+ },
96
+ ":function"(node: T.Node) {
97
+ if (moduleLevel || !isFunctionNode(node)) return;
98
+ if (!hasUseServerDirective(node)) return;
99
+ if (getUseServerEligibility(node) !== "eligible") return;
100
+ if (isInsideUseServerFunction(node)) return;
101
+ const scope = sourceCode.scopeManager?.acquire(node);
102
+ // `through` holds every reference crossing the function's boundary,
103
+ // including refs from nested scopes that resolve to outer globals.
104
+ if (scope) checkReferences(scope.through);
105
+ },
106
+ };
107
+ },
108
+ });
@@ -0,0 +1,245 @@
1
+ import { TSESTree as T, TSESLint, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
2
+ import { getFunctionName, type FunctionNode } from "../utils";
3
+ import { getSourceCode } from "../compat";
4
+
5
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
6
+ const { getStringIfConstant } = ASTUtils;
7
+
8
+ const getName = (node: T.Node): string | null => {
9
+ switch (node.type) {
10
+ case "Literal":
11
+ return typeof node.value === "string" ? node.value : null;
12
+ case "Identifier":
13
+ return node.name;
14
+ case "AssignmentPattern":
15
+ return getName(node.left);
16
+ default:
17
+ return getStringIfConstant(node);
18
+ }
19
+ };
20
+
21
+ interface PropertyInfo {
22
+ real: T.Literal | T.Identifier | T.Expression;
23
+ var: string;
24
+ computed: boolean;
25
+ init: T.Expression | undefined;
26
+ }
27
+
28
+ // Given ({ 'hello-world': helloWorld = 5 }), returns { real: Literal('hello-world'), var: 'helloWorld', computed: false, init: Literal(5) }
29
+ const getPropertyInfo = (prop: T.Property): PropertyInfo | null => {
30
+ const valueName = getName(prop.value);
31
+ if (valueName !== null) {
32
+ return {
33
+ real: prop.key,
34
+ var: valueName,
35
+ computed: prop.computed,
36
+ init: prop.value.type === "AssignmentPattern" ? prop.value.right : undefined,
37
+ };
38
+ } else {
39
+ return null;
40
+ }
41
+ };
42
+
43
+ export default createRule({
44
+ meta: {
45
+ type: "problem",
46
+ docs: {
47
+ description:
48
+ "Disallow destructuring props. In Solid, props must be used with property accesses (`props.foo`) to preserve reactivity. This rule only tracks destructuring in the parameter list.",
49
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-destructure.md",
50
+ },
51
+ fixable: "code",
52
+ schema: [],
53
+ messages: {
54
+ noDestructure:
55
+ "Destructuring component props breaks Solid's reactivity; use property access instead.",
56
+ // noWriteToProps: "Component props are readonly, writing to props is not supported.",
57
+ },
58
+ },
59
+ defaultOptions: [],
60
+ create(context) {
61
+ const functionStack: Array<{
62
+ /** switched to true by :exit if JSX is detected in the current function */
63
+ hasJSX: boolean;
64
+ }> = [];
65
+ const currentFunction = () => functionStack[functionStack.length - 1];
66
+ const onFunctionEnter = () => {
67
+ functionStack.push({ hasJSX: false });
68
+ };
69
+ // Matches the component detection in components-return-once: containing JSX
70
+ // isn't enough on its own, because data callbacks (`items.map(({ id }) => ...)`)
71
+ // and lowercase-named helpers (`renderItem`) also return JSX; their object
72
+ // param is a plain data item, not reactive props.
73
+ const isComponent = (node: FunctionNode) =>
74
+ !getFunctionName(node)?.match(/^[a-z]/) &&
75
+ node.parent?.type !== "JSXExpressionContainer" && // "render props" aren't components
76
+ !(
77
+ // functions passed as arguments are callbacks, unless the callee is a
78
+ // component-wrapper (PascalCase, like HOCs). A FunctionDeclaration can
79
+ // never be a call argument, so the cast below is safe.
80
+ (
81
+ node.parent?.type === "CallExpression" &&
82
+ node.parent.arguments.includes(node as T.CallExpressionArgument) &&
83
+ !(node.parent.callee as T.Identifier).name?.match(/^[A-Z]/)
84
+ )
85
+ );
86
+
87
+ const onFunctionExit = (node: FunctionNode) => {
88
+ if (node.params.length === 1) {
89
+ const props = node.params[0];
90
+ if (props.type === "ObjectPattern" && currentFunction().hasJSX && isComponent(node)) {
91
+ // Props are destructured in the function params, not the body. We actually don't
92
+ // need to handle the case where props are destructured in the body, because that
93
+ // will be a violation of "solid/reactivity".
94
+ context.report({
95
+ node: props,
96
+ messageId: "noDestructure",
97
+ fix: (fixer) => fixDestructure(node, props, fixer),
98
+ });
99
+ }
100
+ }
101
+
102
+ // Pop on exit
103
+ functionStack.pop();
104
+ };
105
+
106
+ function* fixDestructure(
107
+ func: FunctionNode,
108
+ props: T.ObjectPattern,
109
+ fixer: TSESLint.RuleFixer
110
+ ): Generator<TSESLint.RuleFix> {
111
+ const propsName = "props";
112
+ const properties = props.properties;
113
+
114
+ const propertyInfo: Array<PropertyInfo> = [];
115
+ let rest: T.RestElement | null = null;
116
+
117
+ for (const property of properties) {
118
+ if (property.type === "RestElement") {
119
+ rest = property;
120
+ } else {
121
+ const info = getPropertyInfo(property);
122
+ if (info === null) {
123
+ continue;
124
+ }
125
+ propertyInfo.push(info);
126
+ }
127
+ }
128
+
129
+ const hasDefaults = propertyInfo.some((info) => info.init);
130
+
131
+ // Replace destructured props with a `props` identifier (`_props` in case of rest params/defaults)
132
+ const origProps = !(hasDefaults || rest) ? propsName : "_" + propsName;
133
+ if (props.typeAnnotation) {
134
+ // in `{ prop1, prop2 }: Props`, leave `: Props` alone
135
+ const range = [props.range[0], props.typeAnnotation.range[0]] as const;
136
+ yield fixer.replaceTextRange(range, origProps);
137
+ } else {
138
+ yield fixer.replaceText(props, origProps);
139
+ }
140
+
141
+ const sourceCode = getSourceCode(context);
142
+
143
+ const defaultsObjectString = () =>
144
+ propertyInfo
145
+ .filter((info) => info.init)
146
+ .map(
147
+ (info) =>
148
+ `${info.computed ? "[" : ""}${sourceCode.getText(info.real)}${
149
+ info.computed ? "]" : ""
150
+ }: ${sourceCode.getText(info.init)}`
151
+ )
152
+ .join(", ");
153
+ const splitPropsArray = () =>
154
+ `[${propertyInfo
155
+ .map((info) =>
156
+ info.real.type === "Identifier"
157
+ ? JSON.stringify(info.real.name)
158
+ : sourceCode.getText(info.real)
159
+ )
160
+ .join(", ")}]`;
161
+
162
+ let lineToInsert = "";
163
+ if (hasDefaults && rest) {
164
+ // Insert a line that assigns _props
165
+ lineToInsert = ` const [${propsName}, ${
166
+ (rest.argument.type === "Identifier" && rest.argument.name) || "rest"
167
+ }] = splitProps(mergeProps({ ${defaultsObjectString()} }, ${origProps}), ${splitPropsArray()});`;
168
+ } else if (hasDefaults) {
169
+ // Insert a line that assigns _props merged with defaults to props
170
+ lineToInsert = ` const ${propsName} = mergeProps({ ${defaultsObjectString()} }, ${origProps});\n`;
171
+ } else if (rest) {
172
+ // Insert a line that keeps named props and extracts the rest into a new reactive rest object
173
+ lineToInsert = ` const [${propsName}, ${
174
+ (rest.argument.type === "Identifier" && rest.argument.name) || "rest"
175
+ }] = splitProps(${origProps}, ${splitPropsArray()});\n`;
176
+ }
177
+
178
+ if (lineToInsert) {
179
+ const body = func.body;
180
+ if (body.type === "BlockStatement") {
181
+ if (body.body.length > 0) {
182
+ // Inject lines handling defaults/rest params before the first statement in the block.
183
+ yield fixer.insertTextBefore(body.body[0], lineToInsert);
184
+ }
185
+ // with an empty block statement body, no need to inject code
186
+ } else {
187
+ // The function is an arrow function that implicitly returns an expression, possibly with wrapping parentheses.
188
+ // These must be removed to convert the function body to a block statement for code injection.
189
+ const maybeOpenParen = sourceCode.getTokenBefore(body);
190
+ if (maybeOpenParen?.value === "(") {
191
+ yield fixer.remove(maybeOpenParen);
192
+ }
193
+ const maybeCloseParen = sourceCode.getTokenAfter(body);
194
+ if (maybeCloseParen?.value === ")") {
195
+ yield fixer.remove(maybeCloseParen);
196
+ }
197
+
198
+ // Inject lines handling defaults/rest params
199
+ yield fixer.insertTextBefore(body, `{\n${lineToInsert} return (`);
200
+ yield fixer.insertTextAfter(body, `);\n}`);
201
+ }
202
+ }
203
+
204
+ const scope = sourceCode.scopeManager?.acquire(func);
205
+ if (scope) {
206
+ // iterate through destructured variables, associated with real node
207
+ for (const [info, variable] of propertyInfo.map(
208
+ (info) => [info, scope.set.get(info.var)] as const
209
+ )) {
210
+ if (variable) {
211
+ // replace all usages of the variable with props accesses
212
+ for (const reference of variable.references) {
213
+ if (reference.isReadOnly()) {
214
+ const access =
215
+ info.real.type === "Identifier" && !info.computed
216
+ ? `.${info.real.name}`
217
+ : `[${sourceCode.getText(info.real)}]`;
218
+ yield fixer.replaceText(reference.identifier, `${propsName}${access}`);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ return {
227
+ FunctionDeclaration: onFunctionEnter,
228
+ FunctionExpression: onFunctionEnter,
229
+ ArrowFunctionExpression: onFunctionEnter,
230
+ "FunctionDeclaration:exit": onFunctionExit,
231
+ "FunctionExpression:exit": onFunctionExit,
232
+ "ArrowFunctionExpression:exit": onFunctionExit,
233
+ JSXElement() {
234
+ if (functionStack.length) {
235
+ currentFunction().hasJSX = true;
236
+ }
237
+ },
238
+ JSXFragment() {
239
+ if (functionStack.length) {
240
+ currentFunction().hasJSX = true;
241
+ }
242
+ },
243
+ };
244
+ },
245
+ });