@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,570 @@
1
+ import type { TSESLint } from "@typescript-eslint/utils";
2
+ import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils";
3
+ import { findVariable } from "../compat";
4
+ import { createReactiveImportResolver } from "../utils/reactive-imports";
5
+ import {
6
+ Value,
7
+ object,
8
+ scalar,
9
+ setter,
10
+ container,
11
+ asStore,
12
+ propertyValue,
13
+ propertyName,
14
+ mergeValues,
15
+ awaitedValue,
16
+ promiseValue,
17
+ hasKind,
18
+ callResult,
19
+ propertiesOf,
20
+ iteratorValue,
21
+ iterationValue,
22
+ iterationReturn,
23
+ computationValue,
24
+ } from "../utils/reactive-values";
25
+ import {
26
+ FunctionNode,
27
+ getSolidSourceRegex,
28
+ ignoreTransparentWrappers,
29
+ isFunctionNode,
30
+ isPropsByName,
31
+ isSolidV2,
32
+ } from "../utils";
33
+
34
+ type MessageIds = "untrackedRead";
35
+ type Options = [{ reactiveObjectFactories?: string[] }];
36
+ type Variable = TSESLint.Scope.Variable;
37
+ type Bindings = Map<Variable, Value>;
38
+
39
+ /**
40
+ * Track the shape of values crossing the compute/apply boundary. Plain
41
+ * containers can retain proxies or accessors in individual fields; copying a
42
+ * container is not necessarily a snapshot. Imported return values are traced
43
+ * through bounded local source analysis, not a full project type check.
44
+ */
45
+ export default ESLintUtils.RuleCreator.withoutDocs<Options, MessageIds>({
46
+ meta: {
47
+ type: "problem",
48
+ docs: {
49
+ description:
50
+ "Disallow reading reactive values in the untracked apply callback of a Solid 2 split effect.",
51
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-reactive-read-in-effect-callback.md",
52
+ },
53
+ schema: [
54
+ {
55
+ type: "object",
56
+ properties: {
57
+ reactiveObjectFactories: {
58
+ type: "array",
59
+ items: { type: "string" },
60
+ uniqueItems: true,
61
+ description: "Local names of additional functions that return reactive objects.",
62
+ default: [],
63
+ },
64
+ },
65
+ additionalProperties: false,
66
+ },
67
+ ],
68
+ messages: {
69
+ untrackedRead:
70
+ "Reactive value '{{name}}' is read in an untracked effect callback. Read it in the compute function and pass a plain snapshot to the callback.",
71
+ },
72
+ },
73
+ defaultOptions: [{ reactiveObjectFactories: [] }],
74
+ create(context, [options]) {
75
+ if (!isSolidV2(context)) return {};
76
+
77
+ const sourceCode = context.sourceCode;
78
+ const solidSource = getSolidSourceRegex(context);
79
+ const { value: importedValue, type: typeOf } = createReactiveImportResolver(
80
+ context.physicalFilename,
81
+ solidSource,
82
+ sourceCode.text
83
+ );
84
+ const factories = new Set(options.reactiveObjectFactories);
85
+ const reported = new Set<number>();
86
+ const parameterReads = new WeakMap<Bindings, T.Node[]>();
87
+
88
+ const unwrap = (node: T.Node): T.Node =>
89
+ node.type === "ChainExpression" ? unwrap(node.expression) : ignoreTransparentWrappers(node);
90
+
91
+ // Resolve only stable local aliases, with a guard for circular definitions.
92
+ const resolve = (node: T.Node | undefined, seen = new Set<T.Node>()): T.Node | undefined => {
93
+ if (!node) return undefined;
94
+ node = unwrap(node);
95
+ if (seen.has(node)) return undefined;
96
+ seen.add(node);
97
+ if (node.type === "Identifier") {
98
+ const variable = findVariable(context, node);
99
+ const def = variable?.defs[0];
100
+ if (def?.type === "FunctionName") return def.node;
101
+ if (
102
+ def?.type === "Variable" &&
103
+ def.node.id.type === "Identifier" &&
104
+ !variable?.references.some((ref) => ref.isWrite() && !ref.init)
105
+ ) {
106
+ return resolve(def.node.init ?? undefined, seen);
107
+ }
108
+ }
109
+ if (node.type === "MemberExpression") {
110
+ const base = resolve(node.object, seen);
111
+ const key = propertyName(node.property, node.computed);
112
+ if (base?.type === "ObjectExpression" && key !== null) {
113
+ const property = base.properties.find(
114
+ (p) => p.type === "Property" && propertyName(p.key, p.computed) === key
115
+ );
116
+ if (property?.type === "Property") return resolve(property.value, seen);
117
+ }
118
+ }
119
+ return node;
120
+ };
121
+
122
+ const resolveFunction = (node: T.Node | undefined): FunctionNode | null => {
123
+ const resolved = resolve(node);
124
+ return isFunctionNode(resolved) ? resolved : null;
125
+ };
126
+
127
+ // Resolve the binding, not just its spelling: aliases, namespaces and
128
+ // shadowed imports must all have the same behavior.
129
+ const importedBinding = (node: T.Node): { source: string; name: string } | null => {
130
+ node = resolve(node) ?? node;
131
+ const id =
132
+ node.type === "Identifier"
133
+ ? node
134
+ : node.type === "MemberExpression" && node.object.type === "Identifier"
135
+ ? node.object
136
+ : null;
137
+ if (!id) return null;
138
+ const def = findVariable(context, id)?.defs[0];
139
+ if (
140
+ def?.type !== "ImportBinding" ||
141
+ def.parent.type !== "ImportDeclaration" ||
142
+ def.parent.importKind === "type"
143
+ )
144
+ return null;
145
+ if (node.type === "Identifier" && def.node.type === "ImportSpecifier") {
146
+ if (def.node.importKind === "type") return null;
147
+ const name =
148
+ def.node.imported.type === "Identifier"
149
+ ? def.node.imported.name
150
+ : def.node.imported.value;
151
+ return { source: def.parent.source.value, name };
152
+ }
153
+ if (node.type === "Identifier" && def.node.type === "ImportDefaultSpecifier") {
154
+ return { source: def.parent.source.value, name: "default" };
155
+ }
156
+ if (node.type === "MemberExpression" && def.node.type === "ImportNamespaceSpecifier") {
157
+ const name = propertyName(node.property, node.computed);
158
+ return name === null ? null : { source: def.parent.source.value, name };
159
+ }
160
+ return null;
161
+ };
162
+
163
+ const solidImport = (node: T.Node): string | null => {
164
+ const binding = importedBinding(node);
165
+ return binding && solidSource.test(binding.source) ? binding.name : null;
166
+ };
167
+
168
+ const isCallbackPropCall = (node: T.MemberExpression, env: Bindings): boolean => {
169
+ // Conventional event callbacks are imperative, not dependencies. Limit
170
+ // this to direct calls on props with a known void callback signature;
171
+ // ordinary data props, accessor props, and store methods still warn.
172
+ const name = propertyName(node.property, node.computed);
173
+ if (
174
+ !name ||
175
+ !/^on[A-Z]/.test(name) ||
176
+ node.parent.type !== "CallExpression" ||
177
+ node.parent.callee !== node
178
+ )
179
+ return false;
180
+ const base = resolve(node.object);
181
+ if (base?.type !== "Identifier" || !isPropsByName(base.name)) return false;
182
+ const variable = findVariable(context, base);
183
+ return (
184
+ variable?.defs[0]?.type === "Parameter" &&
185
+ !env.has(variable) &&
186
+ typeOf(node.range) === "callback"
187
+ );
188
+ };
189
+
190
+ const report = (node: T.Node): void => {
191
+ if (reported.has(node.range[0])) return;
192
+ reported.add(node.range[0]);
193
+ context.report({
194
+ node,
195
+ messageId: "untrackedRead",
196
+ data: { name: sourceCode.getText(node) },
197
+ });
198
+ };
199
+
200
+ // Parser visitor keys exclude parent links and include TS/JSX nodes.
201
+ // Nested functions are visited only for known calls. Async helpers called
202
+ // by apply remain untracked on both sides of their suspension points.
203
+ const walk = (node: T.Node, visit: (node: T.Node) => void): void => {
204
+ if (isFunctionNode(node)) return;
205
+ visit(node);
206
+ for (const key of sourceCode.visitorKeys[node.type] ?? []) {
207
+ const child = (node as unknown as Record<string, unknown>)[key];
208
+ if (Array.isArray(child)) {
209
+ for (const item of child) if (item?.type) walk(item as T.Node, visit);
210
+ } else if (child && typeof child === "object" && "type" in child) {
211
+ walk(child as T.Node, visit);
212
+ }
213
+ }
214
+ };
215
+
216
+ const returns = (fn: FunctionNode): T.Node[] => {
217
+ if (fn.body.type !== "BlockStatement") return [fn.body];
218
+ const result: T.Node[] = [];
219
+ walk(fn.body, (node) => {
220
+ if (node.type === "ReturnStatement" && node.argument) result.push(node.argument);
221
+ });
222
+ return result;
223
+ };
224
+
225
+ const bind = (
226
+ pattern: T.Node,
227
+ value: Value,
228
+ env: Bindings,
229
+ check = false,
230
+ seen = new Set<T.Node>()
231
+ ): void => {
232
+ if (seen.has(pattern)) return;
233
+ const next = new Set(seen).add(pattern);
234
+ if (pattern.type === "Identifier") {
235
+ const variable = findVariable(context, pattern);
236
+ if (variable) env.set(variable, value);
237
+ } else if (pattern.type === "AssignmentPattern") {
238
+ if (check && !value) parameterReads.get(env)?.push(pattern.right);
239
+ bind(pattern.left, value ?? infer(pattern.right, env, next), env, check, next);
240
+ } else if (pattern.type === "ObjectPattern") {
241
+ for (const property of pattern.properties) {
242
+ if (check && hasKind(value, "object")) report(property);
243
+ if (property.type === "RestElement") {
244
+ bind(
245
+ property.argument,
246
+ { kind: "plain", properties: new Map(propertiesOf(value)) },
247
+ env,
248
+ false,
249
+ next
250
+ );
251
+ } else {
252
+ if (check && property.computed) parameterReads.get(env)?.push(property.key);
253
+ bind(
254
+ property.value,
255
+ propertyValue(value, propertyName(property.key, property.computed)),
256
+ env,
257
+ check,
258
+ next
259
+ );
260
+ }
261
+ }
262
+ } else if (pattern.type === "ArrayPattern") {
263
+ if (check && hasKind(value, "object")) report(pattern);
264
+ pattern.elements.forEach((element, index) => {
265
+ if (element) bind(element, propertyValue(value, String(index)), env, check, next);
266
+ });
267
+ } else if (pattern.type === "RestElement") {
268
+ bind(pattern.argument, value, env, check, next);
269
+ }
270
+ };
271
+
272
+ const callBindings = (
273
+ fn: FunctionNode,
274
+ args: Value[],
275
+ env: Bindings,
276
+ check = false,
277
+ seen = new Set<T.Node>()
278
+ ): Bindings => {
279
+ const local = new Map(env);
280
+ if (check) parameterReads.set(local, []);
281
+ fn.params.forEach((param, index) => {
282
+ const value =
283
+ param.type === "RestElement"
284
+ ? container(args.slice(index).map((arg, i) => [String(i), arg]))
285
+ : args[index] ?? null;
286
+ bind(param, value, local, check, seen);
287
+ });
288
+ return local;
289
+ };
290
+
291
+ const resultOf = (fn: FunctionNode, env: Bindings, seen: Set<T.Node>): Value => {
292
+ if (seen.has(fn)) return null;
293
+ const next = new Set(seen).add(fn);
294
+ const result = returns(fn).reduce<Value>(
295
+ (value, node) => mergeValues(value, infer(node, env, next)),
296
+ null
297
+ );
298
+ if (fn.generator) {
299
+ let yielded: Value = null;
300
+ walk(fn.body, (node) => {
301
+ if (node.type !== "YieldExpression" || !node.argument) return;
302
+ const value = infer(node.argument, env, next);
303
+ yielded = mergeValues(yielded, node.delegate ? iterationValue(value) : value);
304
+ });
305
+ return iteratorValue(fn.async, yielded, result);
306
+ }
307
+ return fn.async ? promiseValue(result) : result;
308
+ };
309
+
310
+ const computeValue = (
311
+ node: T.Node | undefined,
312
+ env: Bindings,
313
+ seen = new Set<T.Node>()
314
+ ): Value => {
315
+ if (!node) return null;
316
+ const fn = resolveFunction(node);
317
+ if (fn) return computationValue(resultOf(fn, env, seen));
318
+ const binding = importedBinding(node);
319
+ return computationValue(
320
+ binding
321
+ ? importedValue(binding.source, binding.name, [])
322
+ : callResult(infer(node, env, seen))
323
+ );
324
+ };
325
+
326
+ const infer = (input: T.Node | undefined, env: Bindings, seen = new Set<T.Node>()): Value => {
327
+ if (!input) return null;
328
+ const node = unwrap(input);
329
+ if (seen.has(node)) return null;
330
+ const next = new Set(seen).add(node);
331
+ if (node.type === "AwaitExpression") return awaitedValue(infer(node.argument, env, next));
332
+ if (node.type === "YieldExpression") {
333
+ if (!node.delegate) return null;
334
+ const result = iterationReturn(infer(node.argument ?? undefined, env, next));
335
+ let parent: T.Node | undefined = node.parent;
336
+ while (parent && !isFunctionNode(parent)) parent = parent.parent;
337
+ return parent && isFunctionNode(parent) && parent.async ? awaitedValue(result) : result;
338
+ }
339
+ if (node.type === "Identifier") {
340
+ const variable = findVariable(context, node);
341
+ if (!variable) return null;
342
+ if (env.has(variable)) return env.get(variable)!;
343
+ const def = variable.defs[0];
344
+ if (def?.type === "ImportBinding") {
345
+ const binding = importedBinding(node);
346
+ return binding ? importedValue(binding.source, binding.name) : null;
347
+ }
348
+ if (def?.type === "Parameter") return isPropsByName(node.name) ? object : null;
349
+ if (def?.type !== "Variable" || !def.node.init || seen.has(def.node)) return null;
350
+ if (variable.references.some((ref) => ref.isWrite() && !ref.init)) return null;
351
+ next.add(def.node);
352
+ const value = infer(def.node.init, env, next);
353
+ const bindings = new Map<Variable, Value>();
354
+ bind(def.node.id, value, bindings, false, next);
355
+ return bindings.get(variable) ?? null;
356
+ }
357
+ if (node.type === "MemberExpression") {
358
+ const value = propertyValue(
359
+ infer(node.object, env, next),
360
+ propertyName(node.property, node.computed)
361
+ );
362
+ // Reading a scalar field is still reactive, but its copied result is
363
+ // not a proxy. Refine only the value, never suppress the original read.
364
+ return (value === null || value.kind === "object") && typeOf(node.range) === "scalar"
365
+ ? scalar
366
+ : value;
367
+ }
368
+ if (
369
+ node.type === "Literal" ||
370
+ node.type === "TemplateLiteral" ||
371
+ node.type === "BinaryExpression" ||
372
+ node.type === "UnaryExpression"
373
+ )
374
+ return scalar;
375
+ if (node.type === "ObjectExpression") {
376
+ const properties = new Map<string, Value>();
377
+ for (const property of node.properties) {
378
+ if (property.type === "SpreadElement") {
379
+ const value = infer(property.argument, env, next);
380
+ for (const [key, entry] of propertiesOf(value)) properties.set(key, entry);
381
+ } else {
382
+ const key = propertyName(property.key, property.computed);
383
+ if (key !== null) properties.set(key, infer(property.value, env, next));
384
+ }
385
+ }
386
+ return { kind: "plain", properties };
387
+ }
388
+ if (node.type === "ArrayExpression") {
389
+ return container(
390
+ node.elements.map((element, i) => [String(i), infer(element ?? undefined, env, next)])
391
+ );
392
+ }
393
+ if (node.type === "ConditionalExpression") {
394
+ return mergeValues(infer(node.consequent, env, next), infer(node.alternate, env, next));
395
+ }
396
+ if (node.type === "LogicalExpression") {
397
+ return mergeValues(infer(node.left, env, next), infer(node.right, env, next));
398
+ }
399
+ if (node.type === "CallExpression") {
400
+ // Only the unshadowed built-in Promise.resolve has these semantics.
401
+ if (
402
+ node.callee.type === "MemberExpression" &&
403
+ node.callee.object.type === "Identifier" &&
404
+ node.callee.object.name === "Promise" &&
405
+ propertyName(node.callee.property, node.callee.computed) === "resolve"
406
+ ) {
407
+ const variable = findVariable(context, node.callee.object);
408
+ if (!variable?.defs.length && !variable?.references.some((ref) => ref.isWrite()))
409
+ return promiseValue(infer(node.arguments[0], env, next));
410
+ }
411
+ const primitive = solidImport(node.callee);
412
+ const first = node.arguments[0];
413
+ const fn = resolveFunction(first);
414
+ if (
415
+ primitive === "createSignal" ||
416
+ primitive === "createOptimistic" ||
417
+ primitive === "createMemo"
418
+ ) {
419
+ // Solid unwraps async computations, but a signal initialized with a
420
+ // Promise as data still holds that Promise.
421
+ const result = computeValue(first, env, next) ?? infer(first, env, next);
422
+ const accessor: Value = { kind: "accessor", result };
423
+ return primitive === "createMemo"
424
+ ? accessor
425
+ : container([
426
+ ["0", accessor],
427
+ ["1", setter],
428
+ ]);
429
+ }
430
+ if (
431
+ primitive === "createStore" ||
432
+ primitive === "createOptimisticStore" ||
433
+ primitive === "createProjection"
434
+ ) {
435
+ const initial = fn ? node.arguments[1] : first;
436
+ const value = asStore(infer(initial, env, next)) ?? object;
437
+ return primitive === "createProjection"
438
+ ? value
439
+ : container([
440
+ ["0", value],
441
+ ["1", setter],
442
+ ]);
443
+ }
444
+ if (primitive === "omit") return infer(first, env, next);
445
+ if (primitive === "merge") {
446
+ return node.arguments.reduce<Value>(
447
+ (value, arg) => mergeValues(value, infer(arg, env, next)),
448
+ null
449
+ );
450
+ }
451
+ if (node.callee.type === "Identifier" && factories.has(node.callee.name)) return object;
452
+ const binding = importedBinding(node.callee);
453
+ if (binding) {
454
+ return importedValue(
455
+ binding.source,
456
+ binding.name,
457
+ node.arguments.map((arg) => infer(arg, env, next))
458
+ );
459
+ }
460
+ const callee = infer(node.callee, env, next);
461
+ if (hasKind(callee, "accessor")) return callResult(callee);
462
+ const called = resolveFunction(node.callee);
463
+ if (called && !next.has(called)) {
464
+ return resultOf(
465
+ called,
466
+ callBindings(
467
+ called,
468
+ node.arguments.map((arg) => infer(arg, env, next)),
469
+ env,
470
+ false,
471
+ new Set(next).add(called)
472
+ ),
473
+ next
474
+ );
475
+ }
476
+ }
477
+ return null;
478
+ };
479
+
480
+ const analyze = (fn: FunctionNode, env: Bindings, active = new Set<FunctionNode>()): void => {
481
+ if (active.has(fn) || fn.generator) return;
482
+ const next = new Set(active).add(fn);
483
+ // Default parameter expressions also execute in the apply phase.
484
+ const visit = (node: T.Node): void => {
485
+ if (node.type === "VariableDeclarator" && node.init) {
486
+ bind(node.id, infer(node.init, env), env, true);
487
+ } else if (node.type === "AssignmentExpression") {
488
+ bind(node.left, infer(node.right, env), env, true);
489
+ } else if (node.type === "SpreadElement" && hasKind(infer(node.argument, env), "object")) {
490
+ report(node);
491
+ } else if (node.type === "MemberExpression" && hasKind(infer(node.object, env), "object")) {
492
+ const parent = node.parent;
493
+ if (
494
+ !isCallbackPropCall(node, env) &&
495
+ !(
496
+ parent?.type === "AssignmentExpression" &&
497
+ parent.left === node &&
498
+ parent.operator === "="
499
+ )
500
+ ) {
501
+ report(node);
502
+ }
503
+ }
504
+ if (node.type !== "CallExpression") return;
505
+ const callee = infer(node.callee, env);
506
+ if (hasKind(callee, "accessor")) report(node);
507
+ const called = resolveFunction(node.callee);
508
+ if (called) {
509
+ analyze(
510
+ called,
511
+ callBindings(
512
+ called,
513
+ node.arguments.map((arg) => infer(arg, env)),
514
+ env,
515
+ true
516
+ ),
517
+ next
518
+ );
519
+ } else if (hasKind(callee, "setter")) {
520
+ const updater = resolveFunction(node.arguments[0]);
521
+ // The setter's previous value/draft is safe; captured values are not.
522
+ if (updater) analyze(updater, callBindings(updater, [], env, true), next);
523
+ } else if (
524
+ node.callee.type === "MemberExpression" &&
525
+ ["forEach", "map", "filter", "some", "every", "find", "findIndex", "flatMap"].includes(
526
+ propertyName(node.callee.property, node.callee.computed) ?? ""
527
+ )
528
+ ) {
529
+ const callback = resolveFunction(node.arguments[0]);
530
+ if (callback) {
531
+ const item = propertyValue(infer(node.callee.object, env), "0");
532
+ analyze(callback, callBindings(callback, [item], env, true), next);
533
+ }
534
+ }
535
+ };
536
+ for (const expression of parameterReads.get(env) ?? []) walk(expression, visit);
537
+ walk(fn.body, visit);
538
+ };
539
+
540
+ const effects: T.CallExpression[] = [];
541
+ return {
542
+ CallExpression(node) {
543
+ effects.push(node);
544
+ },
545
+ "Program:exit"() {
546
+ for (const node of effects) {
547
+ if (
548
+ !["createEffect", "createRenderEffect"].includes(solidImport(node.callee) ?? "") ||
549
+ node.arguments.length < 2
550
+ )
551
+ continue;
552
+ let apply = resolveFunction(node.arguments[1]);
553
+ if (!apply) {
554
+ const bundle = resolve(node.arguments[1]);
555
+ if (bundle?.type === "ObjectExpression") {
556
+ const effect = bundle.properties.find(
557
+ (p) => p.type === "Property" && propertyName(p.key, p.computed) === "effect"
558
+ );
559
+ if (effect?.type === "Property") apply = resolveFunction(effect.value);
560
+ }
561
+ }
562
+ if (!apply) continue;
563
+ const env: Bindings = new Map();
564
+ const value = computeValue(node.arguments[0], env);
565
+ analyze(apply, callBindings(apply, [value, value], env, true));
566
+ }
567
+ },
568
+ };
569
+ },
570
+ });
@@ -0,0 +1,96 @@
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 { getSolidSourceRegex, trackImports } from "../utils";
11
+ import { getScope } from "../compat";
12
+
13
+ const createRule = ESLintUtils.RuleCreator.withoutDocs;
14
+
15
+ type MessageIds = "restatedDefault";
16
+ type Options = [];
17
+
18
+ // Defaults verified against the Solid 2.0 RC source (client/flow.ts):
19
+ // <For> is keyed by default; <Show> and <Match> are non-keyed by default.
20
+ const JSX_PROP_DEFAULTS: Record<string, Record<string, boolean>> = {
21
+ For: { keyed: true },
22
+ Show: { keyed: false },
23
+ Match: { keyed: false },
24
+ };
25
+
26
+ /*
27
+ * Explicitly passing a prop's default value is noise that AI-generated code
28
+ * produces constantly. Same category as typescript-eslint's
29
+ * no-unnecessary-type-arguments: opt-in, stylistic, autofixable.
30
+ */
31
+ export default createRule<Options, MessageIds>({
32
+ meta: {
33
+ type: "suggestion",
34
+ docs: {
35
+ description: "Disallow restating a prop or option value that is already the default.",
36
+ url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-restated-default-options.md",
37
+ },
38
+ fixable: "code",
39
+ schema: [],
40
+ messages: {
41
+ restatedDefault:
42
+ "Passing `{{prop}}={{{value}}}` restates the default for `<{{component}}>`; omit it.",
43
+ },
44
+ },
45
+ defaultOptions: [],
46
+ create(context) {
47
+ const { matchImport, handleImportDeclaration } = trackImports(getSolidSourceRegex(context));
48
+ return {
49
+ ImportDeclaration: handleImportDeclaration,
50
+ JSXAttribute(node) {
51
+ if (node.name.type !== "JSXIdentifier") return;
52
+ const element = node.parent as T.JSXOpeningElement;
53
+ if (element.name.type !== "JSXIdentifier") return;
54
+
55
+ // Resolve through the import map so aliased Solid imports match and a
56
+ // same-named component from another library (or a local declaration)
57
+ // never does. An *unbound* <For>/<Show>/<Match> is still Solid's: the
58
+ // compiler auto-imports the control-flow built-ins.
59
+ let componentName = matchImport(Object.keys(JSX_PROP_DEFAULTS), element.name.name);
60
+ if (
61
+ !componentName &&
62
+ element.name.name in JSX_PROP_DEFAULTS &&
63
+ ASTUtils.findVariable(getScope(context, element), element.name.name) == null
64
+ ) {
65
+ componentName = element.name.name;
66
+ }
67
+ if (!componentName) return;
68
+ const defaults = JSX_PROP_DEFAULTS[componentName];
69
+ const prop = node.name.name;
70
+ if (!(prop in defaults)) return;
71
+ const defaultValue = defaults[prop];
72
+
73
+ let stated: boolean | null = null;
74
+ if (node.value == null) {
75
+ // bare prop, e.g. `<Show keyed>` means true
76
+ stated = true;
77
+ } else if (
78
+ node.value.type === "JSXExpressionContainer" &&
79
+ node.value.expression.type === "Literal" &&
80
+ typeof node.value.expression.value === "boolean"
81
+ ) {
82
+ stated = node.value.expression.value;
83
+ }
84
+
85
+ if (stated === defaultValue) {
86
+ context.report({
87
+ node,
88
+ messageId: "restatedDefault",
89
+ data: { prop, value: String(stated), component: element.name.name },
90
+ fix: (fixer) => fixer.remove(node),
91
+ });
92
+ }
93
+ },
94
+ };
95
+ },
96
+ });