@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,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
|
+
});
|