@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,1730 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File overview here, scroll to bottom.
|
|
3
|
+
* @link https://github.com/solidjs-community/eslint-plugin-solid/blob/main/docs/reactivity.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TSESTree as T, TSESLint, ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
|
|
7
|
+
import { traverse } from "estraverse";
|
|
8
|
+
import {
|
|
9
|
+
findParent,
|
|
10
|
+
findInScope,
|
|
11
|
+
isPropsByName,
|
|
12
|
+
FunctionNode,
|
|
13
|
+
isFunctionNode,
|
|
14
|
+
ProgramOrFunctionNode,
|
|
15
|
+
isProgramOrFunctionNode,
|
|
16
|
+
getSolidSourceRegex,
|
|
17
|
+
trackImports,
|
|
18
|
+
isDOMElementName,
|
|
19
|
+
ignoreTransparentWrappers,
|
|
20
|
+
getFunctionName,
|
|
21
|
+
isJSXElementOrFragment,
|
|
22
|
+
isSolidV2,
|
|
23
|
+
trace,
|
|
24
|
+
createNameMatcher,
|
|
25
|
+
} from "../utils";
|
|
26
|
+
import { findVariable, getScope, CompatContext, getSourceCode } from "../compat";
|
|
27
|
+
|
|
28
|
+
const { getFunctionHeadLocation } = ASTUtils;
|
|
29
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
30
|
+
|
|
31
|
+
type Variable = TSESLint.Scope.Variable;
|
|
32
|
+
type Reference = TSESLint.Scope.Reference;
|
|
33
|
+
|
|
34
|
+
interface ReactiveVariable {
|
|
35
|
+
/**
|
|
36
|
+
* The reactive variable references we're concerned with (i.e. not init).
|
|
37
|
+
* References are removed after they are analyzed.
|
|
38
|
+
*/
|
|
39
|
+
references: Array<Reference>;
|
|
40
|
+
/**
|
|
41
|
+
* The function node in which the reactive variable was declared, or for a
|
|
42
|
+
* derived signal (function), the deepest function node that declares a
|
|
43
|
+
* referenced signal.
|
|
44
|
+
*/
|
|
45
|
+
declarationScope: ProgramOrFunctionNode;
|
|
46
|
+
/**
|
|
47
|
+
* The reactive variable. Not used directly, only needed for identification
|
|
48
|
+
* in pushUniqueDerivedSignal.
|
|
49
|
+
*/
|
|
50
|
+
variable: Variable;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface TrackedScope {
|
|
54
|
+
/**
|
|
55
|
+
* The root node, usually a function or JSX expression container, to allow
|
|
56
|
+
* reactive variables under.
|
|
57
|
+
*/
|
|
58
|
+
node: T.Node;
|
|
59
|
+
/**
|
|
60
|
+
* The reactive variable should be one of these types:
|
|
61
|
+
* - "function": synchronous function or signal variable
|
|
62
|
+
* - "called-function": synchronous or asynchronous function like a timer or
|
|
63
|
+
* event handler that isn't really a tracked scope but allows reactivity
|
|
64
|
+
* - "expression": some value containing reactivity somewhere
|
|
65
|
+
*/
|
|
66
|
+
expect: "function" | "called-function" | "expression";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ScopeStackItem {
|
|
70
|
+
/** the node for the current scope, or program if global scope */
|
|
71
|
+
node: ProgramOrFunctionNode;
|
|
72
|
+
/**
|
|
73
|
+
* nodes whose descendants in the current scope are allowed to be reactive.
|
|
74
|
+
* JSXExpressionContainers can be any expression containing reactivity, while
|
|
75
|
+
* function nodes/identifiers are typically arguments to solid-js primitives
|
|
76
|
+
* and should match a tracked scope exactly.
|
|
77
|
+
*/
|
|
78
|
+
trackedScopes: Array<TrackedScope> = [];
|
|
79
|
+
/** nameless functions with reactivity, should exactly match a tracked scope */
|
|
80
|
+
unnamedDerivedSignals = new Set<FunctionNode>();
|
|
81
|
+
/** switched to true by time of :exit if JSX is detected in the current scope */
|
|
82
|
+
hasJSX = false;
|
|
83
|
+
|
|
84
|
+
constructor(node: ProgramOrFunctionNode) {
|
|
85
|
+
this.node = node;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class ScopeStack extends Array<ScopeStackItem> {
|
|
90
|
+
currentScope = () => this[this.length - 1];
|
|
91
|
+
parentScope = () => this[this.length - 2];
|
|
92
|
+
|
|
93
|
+
/** Add references to a signal, memo, derived signal, etc. */
|
|
94
|
+
pushSignal(
|
|
95
|
+
variable: Variable,
|
|
96
|
+
declarationScope: ProgramOrFunctionNode = this.currentScope().node
|
|
97
|
+
) {
|
|
98
|
+
this.signals.push({
|
|
99
|
+
references: variable.references.filter((reference) => !reference.init),
|
|
100
|
+
variable,
|
|
101
|
+
declarationScope,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Add references to a signal, merging with existing references if the
|
|
107
|
+
* variable is the same. Derived signals are special; they don't use the
|
|
108
|
+
* declaration scope of the function, but rather the minimum declaration scope
|
|
109
|
+
* of any signals they contain.
|
|
110
|
+
*/
|
|
111
|
+
pushUniqueSignal(variable: Variable, declarationScope: ProgramOrFunctionNode) {
|
|
112
|
+
const foundSignal = this.signals.find((s) => s.variable === variable);
|
|
113
|
+
if (!foundSignal) {
|
|
114
|
+
this.pushSignal(variable, declarationScope);
|
|
115
|
+
} else {
|
|
116
|
+
foundSignal.declarationScope = this.findDeepestDeclarationScope(
|
|
117
|
+
foundSignal.declarationScope,
|
|
118
|
+
declarationScope
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Add references to a props or store. */
|
|
124
|
+
pushProps(
|
|
125
|
+
variable: Variable,
|
|
126
|
+
declarationScope: ProgramOrFunctionNode = this.currentScope().node
|
|
127
|
+
) {
|
|
128
|
+
this.props.push({
|
|
129
|
+
references: variable.references.filter((reference) => !reference.init),
|
|
130
|
+
variable,
|
|
131
|
+
declarationScope,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Function callbacks that run synchronously and don't create a new scope. */
|
|
136
|
+
syncCallbacks = new Set<FunctionNode>();
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Iterate through and remove the signal references in the current scope.
|
|
140
|
+
* That way, the next Scope up can safely check for references in its scope.
|
|
141
|
+
*/
|
|
142
|
+
*consumeSignalReferencesInScope() {
|
|
143
|
+
yield* this.consumeReferencesInScope(this.signals);
|
|
144
|
+
this.signals = this.signals.filter((variable) => variable.references.length !== 0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Iterate through and remove the props references in the current scope. */
|
|
148
|
+
*consumePropsReferencesInScope() {
|
|
149
|
+
yield* this.consumeReferencesInScope(this.props);
|
|
150
|
+
this.props = this.props.filter((variable) => variable.references.length !== 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private *consumeReferencesInScope(
|
|
154
|
+
variables: Array<ReactiveVariable>
|
|
155
|
+
): Iterable<{ reference: Reference; declarationScope: ProgramOrFunctionNode }> {
|
|
156
|
+
for (const variable of variables) {
|
|
157
|
+
const { references } = variable;
|
|
158
|
+
const inScope: Array<Reference> = [],
|
|
159
|
+
notInScope: Array<Reference> = [];
|
|
160
|
+
references.forEach((reference) => {
|
|
161
|
+
if (this.isReferenceInCurrentScope(reference)) {
|
|
162
|
+
inScope.push(reference);
|
|
163
|
+
} else {
|
|
164
|
+
notInScope.push(reference);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
yield* inScope.map((reference) => ({
|
|
168
|
+
reference,
|
|
169
|
+
declarationScope: variable.declarationScope,
|
|
170
|
+
}));
|
|
171
|
+
// I don't think this is needed! Just a perf optimization
|
|
172
|
+
variable.references = notInScope;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Returns the function node deepest in the tree. Assumes a === b, a is inside b, or b is inside a. */
|
|
177
|
+
private findDeepestDeclarationScope = (
|
|
178
|
+
a: ProgramOrFunctionNode,
|
|
179
|
+
b: ProgramOrFunctionNode
|
|
180
|
+
): ProgramOrFunctionNode => {
|
|
181
|
+
if (a === b) return a;
|
|
182
|
+
for (let i = this.length - 1; i >= 0; i -= 1) {
|
|
183
|
+
const { node } = this[i];
|
|
184
|
+
if (a === node || b === node) {
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw new Error("This should never happen");
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Returns true if the reference is in the current scope, handling sync
|
|
193
|
+
* callbacks. Must be called on the :exit pass only.
|
|
194
|
+
*/
|
|
195
|
+
private isReferenceInCurrentScope(reference: Reference) {
|
|
196
|
+
let parentFunction = findParent(reference.identifier, isProgramOrFunctionNode);
|
|
197
|
+
while (isFunctionNode(parentFunction) && this.syncCallbacks.has(parentFunction)) {
|
|
198
|
+
parentFunction = findParent(parentFunction, isProgramOrFunctionNode);
|
|
199
|
+
}
|
|
200
|
+
return parentFunction === this.currentScope().node;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** variable references to be treated as signals, memos, derived signals, etc. */
|
|
204
|
+
private signals: Array<ReactiveVariable> = [];
|
|
205
|
+
/** variables references to be treated as props (or stores) */
|
|
206
|
+
private props: Array<ReactiveVariable> = [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const getNthDestructuredVar = (id: T.Node, n: number, context: CompatContext): Variable | null => {
|
|
210
|
+
if (id?.type === "ArrayPattern") {
|
|
211
|
+
const el = id.elements[n];
|
|
212
|
+
if (el?.type === "Identifier") {
|
|
213
|
+
return findVariable(context, el);
|
|
214
|
+
}
|
|
215
|
+
} else if (id?.type === "ObjectPattern") {
|
|
216
|
+
// {0: a, '1': b, 2: c, ...rest}
|
|
217
|
+
const el = id.properties.find((p): p is T.Property => {
|
|
218
|
+
if (p.type !== "Property") return false;
|
|
219
|
+
if (p.key.type !== "Literal") return false;
|
|
220
|
+
|
|
221
|
+
const key = p.key;
|
|
222
|
+
return key.value === n || (typeof key.value === "string" && key.value === String(n));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (el?.type === "Property" && el.value.type === "Identifier") {
|
|
226
|
+
return findVariable(context, el.value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const getReturnedVar = (id: T.Node, context: CompatContext): Variable | null => {
|
|
233
|
+
if (id.type === "Identifier") {
|
|
234
|
+
return findVariable(context, id);
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
type MessageIds =
|
|
240
|
+
| "noWrite"
|
|
241
|
+
| "untrackedReactive"
|
|
242
|
+
| "expectedFunctionGotExpression"
|
|
243
|
+
| "badSignal"
|
|
244
|
+
| "badUnnamedDerivedSignal"
|
|
245
|
+
| "shouldDestructure"
|
|
246
|
+
| "shouldAssign"
|
|
247
|
+
| "noAsyncTrackedScope"
|
|
248
|
+
| "readAfterAwait"
|
|
249
|
+
| "staleCapture"
|
|
250
|
+
| "providerValue";
|
|
251
|
+
type Options = [{ customReactiveFunctions: string[] }];
|
|
252
|
+
|
|
253
|
+
export default createRule<Options, MessageIds>({
|
|
254
|
+
meta: {
|
|
255
|
+
type: "problem",
|
|
256
|
+
docs: {
|
|
257
|
+
description:
|
|
258
|
+
"Enforce that reactivity (props, signals, memos, etc.) is properly used, so changes in those values will be tracked and update the view as expected.",
|
|
259
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/reactivity.md",
|
|
260
|
+
},
|
|
261
|
+
schema: [
|
|
262
|
+
{
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
customReactiveFunctions: {
|
|
266
|
+
description:
|
|
267
|
+
"List of function names to consider as reactive functions (allow signals to be safely passed as arguments). Supports exact names, '*' wildcards ('use*Store'), and regexes given as '/pattern/' strings. In addition, any create* or use* functions are automatically included.",
|
|
268
|
+
type: "array",
|
|
269
|
+
items: {
|
|
270
|
+
type: "string",
|
|
271
|
+
},
|
|
272
|
+
default: [],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
messages: {
|
|
279
|
+
noWrite: "The reactive variable '{{name}}' should not be reassigned or altered directly.",
|
|
280
|
+
untrackedReactive:
|
|
281
|
+
"The reactive variable '{{name}}' should be used within JSX, a tracked scope (like createEffect), or inside an event handler function, or else changes will be ignored.",
|
|
282
|
+
expectedFunctionGotExpression:
|
|
283
|
+
"The reactive variable '{{name}}' should be wrapped in a function for reactivity. This includes event handler bindings on native elements, which are not reactive like other JSX props.",
|
|
284
|
+
badSignal:
|
|
285
|
+
"The reactive variable '{{name}}' should be called as a function when used in {{where}}.",
|
|
286
|
+
badUnnamedDerivedSignal:
|
|
287
|
+
"This function should be passed to a tracked scope (like createEffect) or an event handler because it contains reactivity, or else changes will be ignored.",
|
|
288
|
+
shouldDestructure:
|
|
289
|
+
"For proper analysis, array destructuring should be used to capture the {{nth}}result of this function call.",
|
|
290
|
+
shouldAssign:
|
|
291
|
+
"For proper analysis, a variable should be used to capture the result of this function call.",
|
|
292
|
+
noAsyncTrackedScope:
|
|
293
|
+
"This tracked scope should not be async. Solid's reactivity only tracks synchronously.",
|
|
294
|
+
readAfterAwait:
|
|
295
|
+
"The reactive variable '{{name}}' is read after this computation suspends (at an 'await' or 'yield'), so changes to it won't be tracked. Read it before the first suspension point and store the result in a variable.",
|
|
296
|
+
staleCapture:
|
|
297
|
+
"'{{captured}}' captures the value of the reactive variable '{{name}}' at setup, but a returned function reads the capture later — it will never update. Call '{{name}}' inside the returned function instead, or prefix '{{captured}}' with 'initial'/'default'/'static' if a one-time snapshot is intended.",
|
|
298
|
+
providerValue:
|
|
299
|
+
"A context provider reads its 'value' prop only once, when it is created, so the reactive variable '{{name}}' will not stay up to date here. Pass the signal, memo, or store itself (or an object containing them) instead of reading it in JSX.",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
defaultOptions: [
|
|
303
|
+
{
|
|
304
|
+
customReactiveFunctions: [],
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
create(context, [options]) {
|
|
308
|
+
const warnShouldDestructure = (node: T.Node, nth?: string) =>
|
|
309
|
+
context.report({
|
|
310
|
+
node,
|
|
311
|
+
messageId: "shouldDestructure",
|
|
312
|
+
data: nth ? { nth: nth + " " } : undefined,
|
|
313
|
+
});
|
|
314
|
+
const warnShouldAssign = (node: T.Node) => context.report({ node, messageId: "shouldAssign" });
|
|
315
|
+
|
|
316
|
+
const sourceCode = getSourceCode(context);
|
|
317
|
+
|
|
318
|
+
/** Represents the lexical function stack and relevant information for each function */
|
|
319
|
+
const scopeStack = new ScopeStack();
|
|
320
|
+
const { currentScope, parentScope } = scopeStack;
|
|
321
|
+
|
|
322
|
+
/** Tracks imports from 'solid-js', handling aliases. */
|
|
323
|
+
const { matchImport, handleImportDeclaration } = trackImports(getSolidSourceRegex(context));
|
|
324
|
+
|
|
325
|
+
/** Solid 2.0 mode (settings.solid.version >= 2). */
|
|
326
|
+
const v2 = isSolidV2(context);
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* `customReactiveFunctions` entries may be exact names, glob-ish patterns using `*`
|
|
330
|
+
* wildcards, or regexes written as "/pattern/" strings.
|
|
331
|
+
*/
|
|
332
|
+
const matchesCustomReactive = createNameMatcher(options.customReactiveFunctions);
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* JSXExpressionContainers for `value={...}` props on context providers. Providers read
|
|
336
|
+
* `props.value` once, untracked, when created (in both Solid 1.x and 2.0), so reactive
|
|
337
|
+
* expressions here are frozen at their initial value. Recorded so reads inside them get
|
|
338
|
+
* a specific error message instead of the generic untracked-reactivity one.
|
|
339
|
+
*/
|
|
340
|
+
const providerValueContainers = new WeakSet<T.Node>();
|
|
341
|
+
|
|
342
|
+
/** Returns whether a name resolves to the result of a `createContext()` call in scope. */
|
|
343
|
+
const isCreatedContext = (name: string, refNode: T.Node): boolean => {
|
|
344
|
+
const variable = ASTUtils.findVariable(getScope(context, refNode), name);
|
|
345
|
+
const def = variable?.defs[0];
|
|
346
|
+
return Boolean(
|
|
347
|
+
def &&
|
|
348
|
+
def.node.type === "VariableDeclarator" &&
|
|
349
|
+
def.node.init?.type === "CallExpression" &&
|
|
350
|
+
def.node.init.callee.type === "Identifier" &&
|
|
351
|
+
matchImport("createContext", def.node.init.callee.name)
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Detects JSX element names that are context providers: `<SomeContext.Provider>` and
|
|
357
|
+
* `<SomeProvider>` by convention, plus any name resolving to a `createContext()` call —
|
|
358
|
+
* in Solid 2.0 the context object is used directly as the provider (`<MyContext value={}>`).
|
|
359
|
+
*/
|
|
360
|
+
const isProviderElementName = (name: T.JSXTagNameExpression): boolean => {
|
|
361
|
+
if (name.type === "JSXIdentifier") {
|
|
362
|
+
return name.name.endsWith("Provider") || isCreatedContext(name.name, name);
|
|
363
|
+
}
|
|
364
|
+
if (name.type === "JSXMemberExpression") {
|
|
365
|
+
return name.property.name === "Provider";
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Async/generator functions that are tracked scopes for Solid 2.0 async computations
|
|
372
|
+
* (async createMemo, function-form createSignal/createStore, etc.). Only reads before
|
|
373
|
+
* the first suspension point (`await`/`yield`) are tracked; later reads are reported.
|
|
374
|
+
*/
|
|
375
|
+
const asyncTrackedScopes = new Set<FunctionNode>();
|
|
376
|
+
const suspensionTaintCache = new Map<FunctionNode, number | null>();
|
|
377
|
+
/**
|
|
378
|
+
* If `fn` is an async computation, returns the source position after which reactive
|
|
379
|
+
* reads are no longer tracked, or null if it never suspends. When the first suspension
|
|
380
|
+
* point is inside a loop, the loop is tainted too, since later iterations read after
|
|
381
|
+
* the previous iteration's suspension.
|
|
382
|
+
*/
|
|
383
|
+
const getSuspensionTaint = (fn: ProgramOrFunctionNode): number | null => {
|
|
384
|
+
if (!isFunctionNode(fn) || !asyncTrackedScopes.has(fn)) return null;
|
|
385
|
+
const cached = suspensionTaintCache.get(fn);
|
|
386
|
+
if (cached !== undefined) return cached;
|
|
387
|
+
let suspension: T.Node | undefined;
|
|
388
|
+
traverse(fn.body as never, {
|
|
389
|
+
enter(cn) {
|
|
390
|
+
const node = cn as T.Node;
|
|
391
|
+
if (node !== fn.body && isFunctionNode(node)) {
|
|
392
|
+
this.skip(); // nested functions suspend independently
|
|
393
|
+
} else if (
|
|
394
|
+
node.type === "AwaitExpression" ||
|
|
395
|
+
node.type === "YieldExpression" ||
|
|
396
|
+
(node.type === "ForOfStatement" && node.await)
|
|
397
|
+
) {
|
|
398
|
+
suspension = node;
|
|
399
|
+
this.break();
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
fallback: "iteration",
|
|
403
|
+
});
|
|
404
|
+
let taint: number | null = null;
|
|
405
|
+
if (suspension) {
|
|
406
|
+
// the operands of the first suspension point are still evaluated synchronously
|
|
407
|
+
taint =
|
|
408
|
+
suspension.type === "ForOfStatement" ? suspension.body.range[0] : suspension.range[1];
|
|
409
|
+
let ancestor: T.Node | undefined = suspension.parent;
|
|
410
|
+
while (ancestor && ancestor !== fn) {
|
|
411
|
+
if (
|
|
412
|
+
ancestor.type === "ForStatement" ||
|
|
413
|
+
ancestor.type === "WhileStatement" ||
|
|
414
|
+
ancestor.type === "DoWhileStatement"
|
|
415
|
+
) {
|
|
416
|
+
taint = ancestor.range[0];
|
|
417
|
+
} else if (ancestor.type === "ForOfStatement" || ancestor.type === "ForInStatement") {
|
|
418
|
+
// the iterated expression is only evaluated once, before the first suspension
|
|
419
|
+
taint = ancestor.body.range[0];
|
|
420
|
+
}
|
|
421
|
+
ancestor = ancestor.parent;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
suspensionTaintCache.set(fn, taint);
|
|
425
|
+
return taint;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/** Workaround for #61 */
|
|
429
|
+
const markPropsOnCondition = (node: FunctionNode, cb: (props: T.Identifier) => boolean) => {
|
|
430
|
+
if (
|
|
431
|
+
node.params.length === 1 &&
|
|
432
|
+
node.params[0].type === "Identifier" &&
|
|
433
|
+
node.parent?.type !== "JSXExpressionContainer" && // "render props" aren't components
|
|
434
|
+
node.parent?.type !== "TemplateLiteral" && // inline functions in tagged template literals aren't components
|
|
435
|
+
cb(node.params[0])
|
|
436
|
+
) {
|
|
437
|
+
// This function is a component, consider its parameter a props
|
|
438
|
+
const propsParam = findVariable(context, node.params[0]);
|
|
439
|
+
if (propsParam) {
|
|
440
|
+
scopeStack.pushProps(propsParam, node);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/** Populates the function stack. */
|
|
446
|
+
const onFunctionEnter = (node: ProgramOrFunctionNode) => {
|
|
447
|
+
if (isFunctionNode(node)) {
|
|
448
|
+
if (scopeStack.syncCallbacks.has(node)) {
|
|
449
|
+
// Ignore sync callbacks like Array#forEach and certain Solid primitives
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
markPropsOnCondition(node, (props) => isPropsByName(props.name));
|
|
453
|
+
}
|
|
454
|
+
scopeStack.push(new ScopeStackItem(node));
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/** Returns whether a node falls under a tracked scope in the current function scope */
|
|
458
|
+
const matchTrackedScope = (trackedScope: TrackedScope, node: T.Node): boolean => {
|
|
459
|
+
switch (trackedScope.expect) {
|
|
460
|
+
case "function":
|
|
461
|
+
case "called-function":
|
|
462
|
+
return node === trackedScope.node;
|
|
463
|
+
case "expression":
|
|
464
|
+
return Boolean(
|
|
465
|
+
findInScope(node, currentScope().node, (node) => node === trackedScope.node)
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Returns true when `fn` is directly returned by the enclosing function:
|
|
472
|
+
* either the argument of a `return` statement or the whole body of an
|
|
473
|
+
* arrow function. Deliberately strict — a function that merely appears
|
|
474
|
+
* somewhere inside a returned expression (e.g. a runWithOwner callback
|
|
475
|
+
* in a returned call) is not "returned" for our purposes.
|
|
476
|
+
*/
|
|
477
|
+
const isDirectlyReturned = (fn: FunctionNode): boolean =>
|
|
478
|
+
fn.parent?.type === "ReturnStatement" ||
|
|
479
|
+
(fn.parent?.type === "ArrowFunctionExpression" && fn.parent.body === fn);
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Detects the stale-capture footgun: a signal called at a function's
|
|
483
|
+
* setup level, its result captured in a variable, and that variable read
|
|
484
|
+
* by a function the enclosing function *returns*. The returned function
|
|
485
|
+
* pretends to be live but reads a value frozen at setup. Reports and
|
|
486
|
+
* returns true when found.
|
|
487
|
+
*/
|
|
488
|
+
const checkStaleCapture = (identifier: T.Identifier): boolean => {
|
|
489
|
+
const call = identifier.parent;
|
|
490
|
+
if (call?.type !== "CallExpression" || call.callee !== identifier) return false;
|
|
491
|
+
// Find the VariableDeclarator whose init contains this call, walking up
|
|
492
|
+
// through intermediate expressions (`const t = items().length`) but
|
|
493
|
+
// never through a statement or another function.
|
|
494
|
+
let child: T.Node = call;
|
|
495
|
+
while (
|
|
496
|
+
child.parent &&
|
|
497
|
+
child.parent.type !== "VariableDeclarator" &&
|
|
498
|
+
!isFunctionNode(child.parent) &&
|
|
499
|
+
!child.parent.type.endsWith("Statement")
|
|
500
|
+
) {
|
|
501
|
+
child = child.parent;
|
|
502
|
+
}
|
|
503
|
+
const declarator = child.parent;
|
|
504
|
+
if (declarator?.type !== "VariableDeclarator" || declarator.init !== child) return false;
|
|
505
|
+
|
|
506
|
+
const currentScopeNode = currentScope().node;
|
|
507
|
+
const capturedVars = sourceCode.scopeManager?.getDeclaredVariables(declarator) ?? [];
|
|
508
|
+
for (const captured of capturedVars) {
|
|
509
|
+
// The initial/default/static naming convention opts into a one-time
|
|
510
|
+
// snapshot, same as for props reads.
|
|
511
|
+
if (/^(?:initial|default|static[A-Z])/.test(captured.name)) continue;
|
|
512
|
+
for (const reference of captured.references) {
|
|
513
|
+
if (reference.init) continue;
|
|
514
|
+
if (isCapturedInReturnedFunction(reference.identifier, currentScopeNode)) {
|
|
515
|
+
context.report({
|
|
516
|
+
node: call,
|
|
517
|
+
messageId: "staleCapture",
|
|
518
|
+
data: { name: identifier.name, captured: captured.name },
|
|
519
|
+
});
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Returns true when `node` sits inside a nested function that escapes the
|
|
529
|
+
* `boundary` function through a `return` statement (including functions
|
|
530
|
+
* embedded in returned JSX or objects).
|
|
531
|
+
*/
|
|
532
|
+
const isCapturedInReturnedFunction = (
|
|
533
|
+
node: T.Node,
|
|
534
|
+
boundary: ProgramOrFunctionNode
|
|
535
|
+
): boolean => {
|
|
536
|
+
// Find the outermost function strictly inside `boundary` containing `node`.
|
|
537
|
+
let outermost: FunctionNode | null = null;
|
|
538
|
+
let cursor: T.Node | null = node;
|
|
539
|
+
while (cursor && cursor !== boundary) {
|
|
540
|
+
if (isFunctionNode(cursor)) outermost = cursor;
|
|
541
|
+
cursor = cursor.parent ?? null;
|
|
542
|
+
}
|
|
543
|
+
if (!cursor || !outermost) return false; // not under boundary, or not captured by a closure
|
|
544
|
+
// Walk from that function to `boundary`; only non-function nodes remain
|
|
545
|
+
// in between, so a ReturnStatement found here belongs to `boundary`.
|
|
546
|
+
let child: T.Node = outermost;
|
|
547
|
+
let parent: T.Node | null = child.parent ?? null;
|
|
548
|
+
while (parent && child !== boundary) {
|
|
549
|
+
if (parent.type === "ReturnStatement") return true;
|
|
550
|
+
// A function passed as a call argument doesn't escape through a
|
|
551
|
+
// `return` below it — only the call's *result* does. Synchronous
|
|
552
|
+
// callbacks (`return items.filter((item) => item.includes(q))`) run
|
|
553
|
+
// during the computation, where the capture is fresh (#223). The
|
|
554
|
+
// same goes for a called function itself (IIFEs).
|
|
555
|
+
if (parent.type === "CallExpression" || parent.type === "NewExpression") return false;
|
|
556
|
+
if (isFunctionNode(parent)) {
|
|
557
|
+
return (
|
|
558
|
+
parent === boundary &&
|
|
559
|
+
parent.type === "ArrowFunctionExpression" &&
|
|
560
|
+
parent.body === child
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
child = parent;
|
|
564
|
+
parent = parent.parent ?? null;
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/** Inspects a specific reference of a reactive variable for correct handling. */
|
|
570
|
+
const handleTrackedScopes = (
|
|
571
|
+
identifier: T.Identifier,
|
|
572
|
+
declarationScope: ProgramOrFunctionNode
|
|
573
|
+
) => {
|
|
574
|
+
const currentScopeNode = currentScope().node;
|
|
575
|
+
// Check if the call falls outside any tracked scopes in the current scope
|
|
576
|
+
if (
|
|
577
|
+
!currentScope().trackedScopes.find((trackedScope) =>
|
|
578
|
+
matchTrackedScope(trackedScope, identifier)
|
|
579
|
+
)
|
|
580
|
+
) {
|
|
581
|
+
const matchedExpression = currentScope().trackedScopes.find((trackedScope) =>
|
|
582
|
+
matchTrackedScope({ ...trackedScope, expect: "expression" }, identifier)
|
|
583
|
+
);
|
|
584
|
+
if (declarationScope === currentScopeNode) {
|
|
585
|
+
// If the reactivity is not contained in a tracked scope, and any of
|
|
586
|
+
// the reactive variables were declared in the current scope, then we
|
|
587
|
+
// report them. When the reference is to an object in a
|
|
588
|
+
// MemberExpression (props/store) or a function call (signal), report
|
|
589
|
+
// that, otherwise the identifier.
|
|
590
|
+
let parentMemberExpression: T.MemberExpression | null = null;
|
|
591
|
+
if (identifier.parent?.type === "MemberExpression") {
|
|
592
|
+
parentMemberExpression = identifier.parent;
|
|
593
|
+
while (parentMemberExpression!.parent?.type === "MemberExpression") {
|
|
594
|
+
parentMemberExpression = parentMemberExpression!.parent;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const parentCallExpression =
|
|
598
|
+
identifier.parent?.type === "CallExpression" ? identifier.parent : null;
|
|
599
|
+
// Reads inside a context provider's `value={...}` get a specific message; the
|
|
600
|
+
// value is read once, untracked, when the provider is created.
|
|
601
|
+
const inProviderValue = Boolean(
|
|
602
|
+
findParent(identifier, (n) => providerValueContainers.has(n))
|
|
603
|
+
);
|
|
604
|
+
context.report({
|
|
605
|
+
node: parentMemberExpression ?? parentCallExpression ?? identifier,
|
|
606
|
+
messageId: inProviderValue
|
|
607
|
+
? "providerValue"
|
|
608
|
+
: matchedExpression
|
|
609
|
+
? "expectedFunctionGotExpression"
|
|
610
|
+
: "untrackedReactive",
|
|
611
|
+
data: {
|
|
612
|
+
name: parentMemberExpression
|
|
613
|
+
? sourceCode.getText(parentMemberExpression)
|
|
614
|
+
: identifier.name,
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
} else {
|
|
618
|
+
// If all of the reactive variables were declared above the current
|
|
619
|
+
// function scope, then the entire function becomes reactive with the
|
|
620
|
+
// deepest declaration scope of the reactive variables it contains.
|
|
621
|
+
// Let the next onFunctionExit up handle it.
|
|
622
|
+
if (!parentScope() || !isFunctionNode(currentScopeNode)) {
|
|
623
|
+
throw new Error("this shouldn't happen!");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Signal called at this function's setup level with its value
|
|
627
|
+
// captured by a variable that a *returned* function reads: the
|
|
628
|
+
// returned function will read a stale value forever. Report here —
|
|
629
|
+
// treating the enclosing function as a derived signal (below) is
|
|
630
|
+
// still correct, but no call-site discipline can fix the capture.
|
|
631
|
+
checkStaleCapture(identifier);
|
|
632
|
+
|
|
633
|
+
// If the current function doesn't have an associated variable, that's
|
|
634
|
+
// fine, it's being used inline (i.e. anonymous arrow function). For
|
|
635
|
+
// this to be okay, the arrow function has to be the same node as one
|
|
636
|
+
// of the tracked scopes, as we can't easily find references.
|
|
637
|
+
const pushUnnamedDerivedSignal = () =>
|
|
638
|
+
(parentScope().unnamedDerivedSignals ??= new Set()).add(currentScopeNode);
|
|
639
|
+
|
|
640
|
+
if (isDirectlyReturned(currentScopeNode)) {
|
|
641
|
+
// A function directly returned by its enclosing function is an
|
|
642
|
+
// accessor handed to the caller — the custom-primitive contract.
|
|
643
|
+
// The caller decides whether it lands in a tracked scope; there is
|
|
644
|
+
// nowhere in this file for it to match a tracked scope, so
|
|
645
|
+
// requiring one only punishes the idiomatic hook shape
|
|
646
|
+
// (`return () => signal()`). Do nothing.
|
|
647
|
+
} else if (currentScopeNode.type === "FunctionDeclaration") {
|
|
648
|
+
// get variable representing function, function node only defines one variable
|
|
649
|
+
const functionVariable: Variable | undefined =
|
|
650
|
+
sourceCode.scopeManager?.getDeclaredVariables(currentScopeNode)?.[0];
|
|
651
|
+
if (functionVariable) {
|
|
652
|
+
scopeStack.pushUniqueSignal(
|
|
653
|
+
functionVariable,
|
|
654
|
+
declarationScope // use declaration scope of a signal contained in this function
|
|
655
|
+
);
|
|
656
|
+
} else {
|
|
657
|
+
pushUnnamedDerivedSignal();
|
|
658
|
+
}
|
|
659
|
+
} else if (currentScopeNode.parent?.type === "VariableDeclarator") {
|
|
660
|
+
const declarator = currentScopeNode.parent;
|
|
661
|
+
// for nameless or arrow function expressions, use the declared variable it's assigned to
|
|
662
|
+
const functionVariable = sourceCode.scopeManager?.getDeclaredVariables(declarator)?.[0];
|
|
663
|
+
if (functionVariable) {
|
|
664
|
+
// use declaration scope of a signal contained in this scope, not the function itself
|
|
665
|
+
scopeStack.pushUniqueSignal(functionVariable, declarationScope);
|
|
666
|
+
} else {
|
|
667
|
+
pushUnnamedDerivedSignal();
|
|
668
|
+
}
|
|
669
|
+
} else if (currentScopeNode.parent?.type === "Property") {
|
|
670
|
+
// todo make this a unique props or something--for now, just ignore (unsafe)
|
|
671
|
+
} else {
|
|
672
|
+
pushUnnamedDerivedSignal();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/** Performs all analysis and reporting. */
|
|
679
|
+
const onFunctionExit = (currentScopeNode: ProgramOrFunctionNode) => {
|
|
680
|
+
// If this function is a component, add its props as a reactive variable
|
|
681
|
+
if (isFunctionNode(currentScopeNode)) {
|
|
682
|
+
markPropsOnCondition(currentScopeNode, (props) => {
|
|
683
|
+
if (
|
|
684
|
+
!isPropsByName(props.name) && // already added in markPropsOnEnter
|
|
685
|
+
currentScope().hasJSX
|
|
686
|
+
) {
|
|
687
|
+
const functionName = getFunctionName(currentScopeNode);
|
|
688
|
+
// begins with lowercase === not component
|
|
689
|
+
if (functionName && !/^[a-z]/.test(functionName)) return true;
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Ignore sync callbacks like Array#forEach and certain Solid primitives.
|
|
696
|
+
// In this case only, currentScopeNode !== currentScope().node, but we're
|
|
697
|
+
// returning early so it doesn't matter.
|
|
698
|
+
if (isFunctionNode(currentScopeNode) && scopeStack.syncCallbacks.has(currentScopeNode)) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// If this scope is an async computation (Solid 2.0), reactive reads positioned
|
|
703
|
+
// after its first suspension point resume outside the tracking window.
|
|
704
|
+
const suspensionTaint = getSuspensionTaint(currentScopeNode);
|
|
705
|
+
const isReadAfterSuspension = (identifier: T.Node): boolean =>
|
|
706
|
+
suspensionTaint != null && identifier.range[0] >= suspensionTaint;
|
|
707
|
+
|
|
708
|
+
// Iterate through all usages of (derived) signals in the current scope
|
|
709
|
+
for (const { reference, declarationScope } of scopeStack.consumeSignalReferencesInScope()) {
|
|
710
|
+
const identifier = reference.identifier;
|
|
711
|
+
if (reference.isWrite()) {
|
|
712
|
+
// don't allow reassigning signals
|
|
713
|
+
context.report({
|
|
714
|
+
node: identifier,
|
|
715
|
+
messageId: "noWrite",
|
|
716
|
+
data: {
|
|
717
|
+
name: identifier.name,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
} else if (identifier.type === "Identifier") {
|
|
721
|
+
const reportBadSignal = (where: string) =>
|
|
722
|
+
context.report({
|
|
723
|
+
node: identifier,
|
|
724
|
+
messageId: "badSignal",
|
|
725
|
+
data: { name: identifier.name, where },
|
|
726
|
+
});
|
|
727
|
+
if (
|
|
728
|
+
// This allows both calling a signal and calling a function with a signal.
|
|
729
|
+
identifier.parent?.type === "CallExpression" ||
|
|
730
|
+
// Also allow the case where we pass an array of signals, such as in a custom hook
|
|
731
|
+
(identifier.parent?.type === "ArrayExpression" &&
|
|
732
|
+
identifier.parent.parent?.type === "CallExpression")
|
|
733
|
+
) {
|
|
734
|
+
if (isReadAfterSuspension(identifier)) {
|
|
735
|
+
// In an async computation, reads after the first suspension point aren't
|
|
736
|
+
// tracked (and in Solid 2.0 may observe unpredictable state).
|
|
737
|
+
context.report({
|
|
738
|
+
node: identifier,
|
|
739
|
+
messageId: "readAfterAwait",
|
|
740
|
+
data: { name: identifier.name },
|
|
741
|
+
});
|
|
742
|
+
} else {
|
|
743
|
+
// This signal is getting called properly, analyze it.
|
|
744
|
+
handleTrackedScopes(identifier, declarationScope);
|
|
745
|
+
}
|
|
746
|
+
} else if (identifier.parent?.type === "TemplateLiteral") {
|
|
747
|
+
reportBadSignal("template literals");
|
|
748
|
+
} else if (
|
|
749
|
+
identifier.parent?.type === "BinaryExpression" &&
|
|
750
|
+
[
|
|
751
|
+
"<",
|
|
752
|
+
"<=",
|
|
753
|
+
">",
|
|
754
|
+
">=",
|
|
755
|
+
"<<",
|
|
756
|
+
">>",
|
|
757
|
+
">>>",
|
|
758
|
+
"+",
|
|
759
|
+
"-",
|
|
760
|
+
"*",
|
|
761
|
+
"/",
|
|
762
|
+
"%",
|
|
763
|
+
"**",
|
|
764
|
+
"|",
|
|
765
|
+
"^",
|
|
766
|
+
"&",
|
|
767
|
+
"in",
|
|
768
|
+
].includes(identifier.parent.operator)
|
|
769
|
+
) {
|
|
770
|
+
// We're in an arithmetic/comparison expression where using an uncalled signal wouldn't make sense
|
|
771
|
+
reportBadSignal("arithmetic or comparisons");
|
|
772
|
+
} else if (
|
|
773
|
+
identifier.parent?.type === "UnaryExpression" &&
|
|
774
|
+
["-", "+", "~"].includes(identifier.parent.operator)
|
|
775
|
+
) {
|
|
776
|
+
// We're in a unary expression where using an uncalled signal wouldn't make sense
|
|
777
|
+
reportBadSignal("unary expressions");
|
|
778
|
+
} else if (
|
|
779
|
+
identifier.parent?.type === "MemberExpression" &&
|
|
780
|
+
identifier.parent.computed &&
|
|
781
|
+
identifier.parent.property === identifier
|
|
782
|
+
) {
|
|
783
|
+
// We're using an uncalled signal to index an object or array, which doesn't make sense
|
|
784
|
+
reportBadSignal("property accesses");
|
|
785
|
+
} else if (
|
|
786
|
+
identifier.parent?.type === "JSXExpressionContainer" &&
|
|
787
|
+
!currentScope().trackedScopes.find(
|
|
788
|
+
(trackedScope) =>
|
|
789
|
+
trackedScope.node === identifier &&
|
|
790
|
+
(trackedScope.expect === "function" || trackedScope.expect === "called-function")
|
|
791
|
+
)
|
|
792
|
+
) {
|
|
793
|
+
// If the signal is in a JSXExpressionContainer that's also marked as a "function" or "called-function" tracked scope,
|
|
794
|
+
// let it be.
|
|
795
|
+
const elementOrAttribute = identifier.parent.parent;
|
|
796
|
+
if (
|
|
797
|
+
// The signal is not being called and is being used as a props.children, where calling
|
|
798
|
+
// the signal was the likely intent.
|
|
799
|
+
isJSXElementOrFragment(elementOrAttribute) ||
|
|
800
|
+
// We can't say for sure about user components, but we know for a fact that a signal
|
|
801
|
+
// should not be passed to a non-event handler DOM element attribute without calling it.
|
|
802
|
+
// In v2 mode this case is delegated to solid/no-accessor-as-prop so the same
|
|
803
|
+
// node never gets two reports.
|
|
804
|
+
(!v2 &&
|
|
805
|
+
elementOrAttribute?.type === "JSXAttribute" &&
|
|
806
|
+
elementOrAttribute.parent?.type === "JSXOpeningElement" &&
|
|
807
|
+
elementOrAttribute.parent.name.type === "JSXIdentifier" &&
|
|
808
|
+
isDOMElementName(elementOrAttribute.parent.name.name))
|
|
809
|
+
) {
|
|
810
|
+
reportBadSignal("JSX");
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// The signal is being read outside of a CallExpression. Since
|
|
815
|
+
// there's a lot of possibilities here and they're generally fine,
|
|
816
|
+
// do nothing.
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Do a similar thing with all usages of props in the current function
|
|
820
|
+
for (const { reference, declarationScope } of scopeStack.consumePropsReferencesInScope()) {
|
|
821
|
+
const identifier = reference.identifier;
|
|
822
|
+
if (reference.isWrite()) {
|
|
823
|
+
// don't allow reassigning props or stores
|
|
824
|
+
context.report({
|
|
825
|
+
node: identifier,
|
|
826
|
+
messageId: "noWrite",
|
|
827
|
+
data: {
|
|
828
|
+
name: identifier.name,
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
} else if (
|
|
832
|
+
identifier.parent?.type === "MemberExpression" &&
|
|
833
|
+
identifier.parent.object === identifier
|
|
834
|
+
) {
|
|
835
|
+
const { parent } = identifier;
|
|
836
|
+
if (parent.parent?.type === "AssignmentExpression" && parent.parent.left === parent) {
|
|
837
|
+
// don't allow writing to props or stores directly
|
|
838
|
+
context.report({
|
|
839
|
+
node: identifier,
|
|
840
|
+
messageId: "noWrite",
|
|
841
|
+
data: {
|
|
842
|
+
name: identifier.name,
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
} else if (
|
|
846
|
+
parent.property.type === "Identifier" &&
|
|
847
|
+
/^(?:initial|default|static[A-Z])/.test(parent.property.name)
|
|
848
|
+
) {
|
|
849
|
+
// We're using a prop with a name that starts with `initial` or
|
|
850
|
+
// `default`, like `props.initialCount`. We'll refrain from warning
|
|
851
|
+
// about untracked usages of these props, because the user has shown
|
|
852
|
+
// that they understand the consequences of using a reactive
|
|
853
|
+
// variable to initialize something else. Do nothing.
|
|
854
|
+
} else if (isReadAfterSuspension(identifier)) {
|
|
855
|
+
// In an async computation, reads after the first suspension point aren't
|
|
856
|
+
// tracked (and in Solid 2.0 may observe unpredictable state).
|
|
857
|
+
context.report({
|
|
858
|
+
node: identifier,
|
|
859
|
+
messageId: "readAfterAwait",
|
|
860
|
+
data: { name: identifier.name },
|
|
861
|
+
});
|
|
862
|
+
} else {
|
|
863
|
+
// The props are the object in a property read access, which
|
|
864
|
+
// should be under a tracked scope.
|
|
865
|
+
handleTrackedScopes(identifier, declarationScope);
|
|
866
|
+
}
|
|
867
|
+
} else if (
|
|
868
|
+
identifier.type === "Identifier" &&
|
|
869
|
+
(identifier.parent?.type === "AssignmentExpression" ||
|
|
870
|
+
identifier.parent?.type === "VariableDeclarator")
|
|
871
|
+
) {
|
|
872
|
+
if (
|
|
873
|
+
identifier.parent.type === "AssignmentExpression" &&
|
|
874
|
+
identifier.parent.right === identifier &&
|
|
875
|
+
identifier.parent.left.type === "MemberExpression"
|
|
876
|
+
) {
|
|
877
|
+
// `this.state = state` stores a *reference* to the proxy, not a
|
|
878
|
+
// snapshot: property reads through it are still reactive at read
|
|
879
|
+
// time. The alias escapes local analysis exactly like passing the
|
|
880
|
+
// proxy as a call argument, which is already permitted (#184).
|
|
881
|
+
// Do nothing.
|
|
882
|
+
} else {
|
|
883
|
+
// `... = props` is usually destructuring, which snapshots values instead of
|
|
884
|
+
// staying reactive. Inside a tracked scope (or a function passed to one) the
|
|
885
|
+
// destructuring re-runs on updates, so defer to the same analysis as reads.
|
|
886
|
+
handleTrackedScopes(identifier, declarationScope);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// The props are being read, but not in a MemberExpression. Since
|
|
890
|
+
// there's a lot of possibilities here and they're generally fine,
|
|
891
|
+
// do nothing.
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// If there are any unnamed derived signals, they must match a tracked
|
|
895
|
+
// scope. Usually anonymous arrow function args to createEffect,
|
|
896
|
+
// createMemo, etc.
|
|
897
|
+
const { unnamedDerivedSignals } = currentScope();
|
|
898
|
+
if (unnamedDerivedSignals) {
|
|
899
|
+
for (const node of unnamedDerivedSignals) {
|
|
900
|
+
if (
|
|
901
|
+
!currentScope().trackedScopes.find((trackedScope) =>
|
|
902
|
+
matchTrackedScope(trackedScope, node)
|
|
903
|
+
)
|
|
904
|
+
) {
|
|
905
|
+
context.report({
|
|
906
|
+
loc: getFunctionHeadLocation(node, sourceCode),
|
|
907
|
+
messageId: "badUnnamedDerivedSignal",
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Pop on exit
|
|
914
|
+
scopeStack.pop();
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
/*
|
|
918
|
+
* Sync array functions (forEach, map, reduce, reduceRight, flatMap),
|
|
919
|
+
* store update fn params (ex. setState("todos", (t) => [...t.slice(0, i()),
|
|
920
|
+
* ...t.slice(i() + 1)])), batch, onCleanup, and onError fn params, and
|
|
921
|
+
* maybe a few others don't actually create a new scope. That is, any
|
|
922
|
+
* signal/prop accesses in these functions act as if they happen in the
|
|
923
|
+
* enclosing function. Note that this means whether or not the enclosing
|
|
924
|
+
* function is a tracking scope applies to the fn param as well.
|
|
925
|
+
*
|
|
926
|
+
* Every time a sync callback is detected, we put that function node into a
|
|
927
|
+
* syncCallbacks Set<FunctionNode>. The detections must happen on the entry pass
|
|
928
|
+
* and when the function node has not yet been traversed. In onFunctionEnter, if
|
|
929
|
+
* the function node is in syncCallbacks, we don't push it onto the
|
|
930
|
+
* scopeStack. In onFunctionExit, if the function node is in syncCallbacks,
|
|
931
|
+
* we don't pop scopeStack.
|
|
932
|
+
*/
|
|
933
|
+
const checkForSyncCallbacks = (node: T.CallExpression) => {
|
|
934
|
+
if (
|
|
935
|
+
node.arguments.length === 1 &&
|
|
936
|
+
isFunctionNode(node.arguments[0]) &&
|
|
937
|
+
!node.arguments[0].async
|
|
938
|
+
) {
|
|
939
|
+
if (
|
|
940
|
+
node.callee.type === "Identifier" &&
|
|
941
|
+
matchImport(["batch", "produce", "flush"], node.callee.name)
|
|
942
|
+
) {
|
|
943
|
+
// These Solid APIs take callbacks that run in the current scope
|
|
944
|
+
scopeStack.syncCallbacks.add(node.arguments[0]);
|
|
945
|
+
} else if (
|
|
946
|
+
node.callee.type === "MemberExpression" &&
|
|
947
|
+
!node.callee.computed &&
|
|
948
|
+
node.callee.object.type !== "ObjectExpression" &&
|
|
949
|
+
/^(?:forEach|map|flatMap|reduce|reduceRight|find|findIndex|filter|every|some)$/.test(
|
|
950
|
+
node.callee.property.name
|
|
951
|
+
)
|
|
952
|
+
) {
|
|
953
|
+
// These common array methods (or likely array methods) take synchronous callbacks
|
|
954
|
+
scopeStack.syncCallbacks.add(node.arguments[0]);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (node.callee.type === "Identifier") {
|
|
958
|
+
if (
|
|
959
|
+
matchImport(
|
|
960
|
+
["createSignal", "createStore", "createOptimistic", "createOptimisticStore"],
|
|
961
|
+
node.callee.name
|
|
962
|
+
) &&
|
|
963
|
+
node.parent?.type === "VariableDeclarator"
|
|
964
|
+
) {
|
|
965
|
+
// Allow using reactive variables in state setter if the current scope is tracked.
|
|
966
|
+
// ex. const [state, setState] = createStore({ ... });
|
|
967
|
+
// setState(() => ({ preferredName: state.firstName, lastName: "Milner" }));
|
|
968
|
+
const setter = getNthDestructuredVar(node.parent.id, 1, context);
|
|
969
|
+
if (setter) {
|
|
970
|
+
for (const reference of setter.references) {
|
|
971
|
+
const { identifier } = reference;
|
|
972
|
+
if (
|
|
973
|
+
!reference.init &&
|
|
974
|
+
reference.isRead() &&
|
|
975
|
+
identifier.parent?.type === "CallExpression"
|
|
976
|
+
) {
|
|
977
|
+
for (const arg of identifier.parent.arguments) {
|
|
978
|
+
if (isFunctionNode(arg) && !arg.async) {
|
|
979
|
+
scopeStack.syncCallbacks.add(arg);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
} else if (matchImport(["mapArray", "indexArray", "repeat"], node.callee.name)) {
|
|
986
|
+
const arg1 = node.arguments[1];
|
|
987
|
+
if (isFunctionNode(arg1)) {
|
|
988
|
+
scopeStack.syncCallbacks.add(arg1);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
// Handle IIFEs
|
|
993
|
+
if (isFunctionNode(node.callee)) {
|
|
994
|
+
scopeStack.syncCallbacks.add(node.callee);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
/** Checks VariableDeclarators, AssignmentExpressions, and CallExpressions for reactivity. */
|
|
999
|
+
const checkForReactiveAssignment = (
|
|
1000
|
+
id: T.BindingName | T.AssignmentExpression["left"] | null,
|
|
1001
|
+
init: T.Node
|
|
1002
|
+
) => {
|
|
1003
|
+
init = ignoreTransparentWrappers(init);
|
|
1004
|
+
|
|
1005
|
+
// Mark return values of certain functions as reactive
|
|
1006
|
+
if (init.type === "CallExpression" && init.callee.type === "Identifier") {
|
|
1007
|
+
const { callee } = init;
|
|
1008
|
+
if (matchImport(["createSignal", "useTransition", "createOptimistic"], callee.name)) {
|
|
1009
|
+
const signal = id && getNthDestructuredVar(id, 0, context);
|
|
1010
|
+
if (signal) {
|
|
1011
|
+
scopeStack.pushSignal(signal, currentScope().node);
|
|
1012
|
+
} else {
|
|
1013
|
+
warnShouldDestructure(id ?? init, "first");
|
|
1014
|
+
}
|
|
1015
|
+
} else if (matchImport(["createMemo", "createSelector"], callee.name)) {
|
|
1016
|
+
const memo = id && getReturnedVar(id, context);
|
|
1017
|
+
// memos act like signals
|
|
1018
|
+
if (memo) {
|
|
1019
|
+
scopeStack.pushSignal(memo, currentScope().node);
|
|
1020
|
+
} else {
|
|
1021
|
+
warnShouldAssign(id ?? init);
|
|
1022
|
+
}
|
|
1023
|
+
} else if (matchImport(["createStore", "createOptimisticStore"], callee.name)) {
|
|
1024
|
+
const store = id && getNthDestructuredVar(id, 0, context);
|
|
1025
|
+
// stores act like props
|
|
1026
|
+
if (store) {
|
|
1027
|
+
scopeStack.pushProps(store, currentScope().node);
|
|
1028
|
+
} else {
|
|
1029
|
+
warnShouldDestructure(id ?? init, "first");
|
|
1030
|
+
}
|
|
1031
|
+
} else if (matchImport(["mergeProps", "merge", "omit"], callee.name)) {
|
|
1032
|
+
// Solid 2.0 renames mergeProps to merge; omit (which replaces splitProps)
|
|
1033
|
+
// also returns a single props object
|
|
1034
|
+
const merged = id && getReturnedVar(id, context);
|
|
1035
|
+
if (merged) {
|
|
1036
|
+
scopeStack.pushProps(merged, currentScope().node);
|
|
1037
|
+
} else {
|
|
1038
|
+
warnShouldAssign(id ?? init);
|
|
1039
|
+
}
|
|
1040
|
+
} else if (matchImport("splitProps", callee.name)) {
|
|
1041
|
+
// splitProps can return an unbounded array of props variables, though it's most often two
|
|
1042
|
+
if (id?.type === "ArrayPattern") {
|
|
1043
|
+
const vars = id.elements
|
|
1044
|
+
.map((_, i) => getNthDestructuredVar(id, i, context))
|
|
1045
|
+
.filter(Boolean) as Array<Variable>;
|
|
1046
|
+
if (vars.length === 0) {
|
|
1047
|
+
warnShouldDestructure(id);
|
|
1048
|
+
} else {
|
|
1049
|
+
vars.forEach((variable) => {
|
|
1050
|
+
scopeStack.pushProps(variable, currentScope().node);
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
} else {
|
|
1054
|
+
// if it's returned as an array, treat that as a props object
|
|
1055
|
+
const vars = id && getReturnedVar(id, context);
|
|
1056
|
+
if (vars) {
|
|
1057
|
+
scopeStack.pushProps(vars, currentScope().node);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
} else if (matchImport("createResource", callee.name)) {
|
|
1061
|
+
// createResource return value has reactive .loading and .error
|
|
1062
|
+
const resourceReturn = id && getNthDestructuredVar(id, 0, context);
|
|
1063
|
+
if (resourceReturn) {
|
|
1064
|
+
scopeStack.pushProps(resourceReturn, currentScope().node);
|
|
1065
|
+
}
|
|
1066
|
+
} else if (matchImport(["createMutable", "createProjection"], callee.name)) {
|
|
1067
|
+
// createProjection (Solid 2.0) returns a readonly derived store
|
|
1068
|
+
const mutable = id && getReturnedVar(id, context);
|
|
1069
|
+
if (mutable) {
|
|
1070
|
+
scopeStack.pushProps(mutable, currentScope().node);
|
|
1071
|
+
}
|
|
1072
|
+
} else if (matchImport("mapArray", callee.name)) {
|
|
1073
|
+
const arg1 = init.arguments[1];
|
|
1074
|
+
if (
|
|
1075
|
+
isFunctionNode(arg1) &&
|
|
1076
|
+
arg1.params.length >= 2 &&
|
|
1077
|
+
arg1.params[1].type === "Identifier"
|
|
1078
|
+
) {
|
|
1079
|
+
const indexSignal = findVariable(context, arg1.params[1]);
|
|
1080
|
+
if (indexSignal) {
|
|
1081
|
+
scopeStack.pushSignal(indexSignal);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
} else if (matchImport("indexArray", callee.name)) {
|
|
1085
|
+
const arg1 = init.arguments[1];
|
|
1086
|
+
if (
|
|
1087
|
+
isFunctionNode(arg1) &&
|
|
1088
|
+
arg1.params.length >= 1 &&
|
|
1089
|
+
arg1.params[0].type === "Identifier"
|
|
1090
|
+
) {
|
|
1091
|
+
const valueSignal = findVariable(context, arg1.params[0]);
|
|
1092
|
+
if (valueSignal) {
|
|
1093
|
+
scopeStack.pushSignal(valueSignal);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const checkForTrackedScopes = (
|
|
1101
|
+
node:
|
|
1102
|
+
| T.JSXExpressionContainer
|
|
1103
|
+
| T.JSXSpreadAttribute
|
|
1104
|
+
| T.CallExpression
|
|
1105
|
+
| T.VariableDeclarator
|
|
1106
|
+
| T.AssignmentExpression
|
|
1107
|
+
| T.TaggedTemplateExpression
|
|
1108
|
+
| T.NewExpression
|
|
1109
|
+
) => {
|
|
1110
|
+
const pushTrackedScope = (
|
|
1111
|
+
node: T.Node,
|
|
1112
|
+
expect: TrackedScope["expect"],
|
|
1113
|
+
allowAsync = false
|
|
1114
|
+
) => {
|
|
1115
|
+
currentScope().trackedScopes.push({ node, expect });
|
|
1116
|
+
if (
|
|
1117
|
+
expect !== "called-function" &&
|
|
1118
|
+
isFunctionNode(node) &&
|
|
1119
|
+
(node.async || node.generator)
|
|
1120
|
+
) {
|
|
1121
|
+
if (allowAsync) {
|
|
1122
|
+
// Async computations are first-class in Solid 2.0 (async createMemo,
|
|
1123
|
+
// function-form createSignal/createStore, etc.). Reads before the first
|
|
1124
|
+
// suspension point are tracked normally; reads after it get a targeted
|
|
1125
|
+
// readAfterAwait report in onFunctionExit instead of a blanket warning.
|
|
1126
|
+
asyncTrackedScopes.add(node);
|
|
1127
|
+
} else if (node.async) {
|
|
1128
|
+
// From the docs: "[Solid's] approach only tracks synchronously. If you
|
|
1129
|
+
// have a setTimeout or use an async function in your Effect the code
|
|
1130
|
+
// that executes async after the fact won't be tracked."
|
|
1131
|
+
context.report({
|
|
1132
|
+
node,
|
|
1133
|
+
messageId: "noAsyncTrackedScope",
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
// given some expression, mark any functions within it as tracking scopes, and do not traverse
|
|
1139
|
+
// those functions
|
|
1140
|
+
const permissivelyTrackNode = (node: T.Node) => {
|
|
1141
|
+
traverse(node as any, {
|
|
1142
|
+
enter(cn) {
|
|
1143
|
+
const childNode = cn as T.Node;
|
|
1144
|
+
const traced = trace(childNode, context);
|
|
1145
|
+
// when referencing a function or something that could be a derived signal, track it
|
|
1146
|
+
if (
|
|
1147
|
+
isFunctionNode(traced) ||
|
|
1148
|
+
(traced.type === "Identifier" &&
|
|
1149
|
+
traced.parent.type !== "MemberExpression" &&
|
|
1150
|
+
!(traced.parent.type === "CallExpression" && traced.parent.callee === traced)) ||
|
|
1151
|
+
// memo accessors traced to their initializing call, e.g. passing a variable
|
|
1152
|
+
// holding a createMemo result — treat like passing a signal
|
|
1153
|
+
(traced.type === "CallExpression" &&
|
|
1154
|
+
traced.callee.type === "Identifier" &&
|
|
1155
|
+
matchImport(
|
|
1156
|
+
["createMemo", "createSelector", "children", "createProjection"],
|
|
1157
|
+
traced.callee.name
|
|
1158
|
+
))
|
|
1159
|
+
) {
|
|
1160
|
+
pushTrackedScope(childNode, "called-function");
|
|
1161
|
+
this.skip(); // poor-man's `findInScope`: don't enter child scopes
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
fallback: "iteration", // Don't crash when encounter unknown node.
|
|
1165
|
+
});
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
if (node.type === "JSXExpressionContainer") {
|
|
1169
|
+
if (
|
|
1170
|
+
node.parent?.type === "JSXAttribute" &&
|
|
1171
|
+
sourceCode.getText(node.parent.name).startsWith("on") &&
|
|
1172
|
+
node.parent.parent?.type === "JSXOpeningElement" &&
|
|
1173
|
+
node.parent.parent.name.type === "JSXIdentifier" &&
|
|
1174
|
+
isDOMElementName(node.parent.parent.name.name)
|
|
1175
|
+
) {
|
|
1176
|
+
// Expect a function if the attribute is like onClick={}, onclick={}, on:click={}, or
|
|
1177
|
+
// custom events such as on-click={}.
|
|
1178
|
+
// From the docs:
|
|
1179
|
+
// Events are never rebound and the bindings are not reactive, as it is expensive to
|
|
1180
|
+
// attach and detach listeners. Since event handlers are called like any other function
|
|
1181
|
+
// each time an event fires, there is no need for reactivity; simply shortcut your handler
|
|
1182
|
+
// if desired.
|
|
1183
|
+
// What this means here is we actually do consider an event handler a tracked scope
|
|
1184
|
+
// expecting a function, i.e. it's okay to use changing props/signals in the body of the
|
|
1185
|
+
// function, even though the changes don't affect when the handler will run. This is what
|
|
1186
|
+
// "called-function" represents—not quite a tracked scope, but a place where it's okay to
|
|
1187
|
+
// read reactive values.
|
|
1188
|
+
pushTrackedScope(node.expression, "called-function");
|
|
1189
|
+
} else if (
|
|
1190
|
+
node.parent?.type === "JSXAttribute" &&
|
|
1191
|
+
node.parent.name.type === "JSXNamespacedName" &&
|
|
1192
|
+
node.parent.name.namespace.name === "use" &&
|
|
1193
|
+
isFunctionNode(node.expression)
|
|
1194
|
+
) {
|
|
1195
|
+
// With a `use:` hook, assume that a function passed is a called function.
|
|
1196
|
+
pushTrackedScope(node.expression, "called-function");
|
|
1197
|
+
} else if (
|
|
1198
|
+
node.parent?.type === "JSXAttribute" &&
|
|
1199
|
+
node.parent.name.name === "value" &&
|
|
1200
|
+
node.parent.parent?.type === "JSXOpeningElement" &&
|
|
1201
|
+
isProviderElementName(node.parent.parent.name)
|
|
1202
|
+
) {
|
|
1203
|
+
// Context providers read `props.value` once, untracked, when they are created (in
|
|
1204
|
+
// both Solid 1.x and 2.0), so a reactive expression here is frozen at its initial
|
|
1205
|
+
// value. Don't push a tracked scope; record the container so reads inside it get
|
|
1206
|
+
// the specific `providerValue` message instead of the generic one.
|
|
1207
|
+
providerValueContainers.add(node);
|
|
1208
|
+
} else if (
|
|
1209
|
+
node.parent?.type === "JSXAttribute" &&
|
|
1210
|
+
node.parent.name?.type === "JSXIdentifier" &&
|
|
1211
|
+
/^static[A-Z]/.test(node.parent.name.name) &&
|
|
1212
|
+
node.parent.parent?.type === "JSXOpeningElement" &&
|
|
1213
|
+
node.parent.parent.name.type === "JSXIdentifier" &&
|
|
1214
|
+
!isDOMElementName(node.parent.parent.name.name)
|
|
1215
|
+
) {
|
|
1216
|
+
// A caller is passing a value to a prop prefixed with `static` in a component, i.e.
|
|
1217
|
+
// `<Box staticName={...} />`. Since we're considering these props as static in the component
|
|
1218
|
+
// we shouldn't allow passing reactive values to them, as this isn't just ignoring reactivity
|
|
1219
|
+
// like initial*/default*; this is disabling it altogether as a convention. Do nothing.
|
|
1220
|
+
} else if (
|
|
1221
|
+
node.parent?.type === "JSXAttribute" &&
|
|
1222
|
+
node.parent.name.name === "ref" &&
|
|
1223
|
+
isFunctionNode(node.expression)
|
|
1224
|
+
) {
|
|
1225
|
+
// Callback/function refs are called when an element is created but before it is connected
|
|
1226
|
+
// to the DOM. This is semantically a "called function", so it's fine to read reactive
|
|
1227
|
+
// variables here.
|
|
1228
|
+
pushTrackedScope(node.expression, "called-function");
|
|
1229
|
+
} else if (isJSXElementOrFragment(node.parent) && isFunctionNode(node.expression)) {
|
|
1230
|
+
pushTrackedScope(node.expression, "function"); // functions inline in JSX containers will be tracked
|
|
1231
|
+
} else {
|
|
1232
|
+
pushTrackedScope(node.expression, "expression");
|
|
1233
|
+
}
|
|
1234
|
+
} else if (node.type === "JSXSpreadAttribute") {
|
|
1235
|
+
// allow <div {...props.nestedProps} />; {...props} is already ignored
|
|
1236
|
+
pushTrackedScope(node.argument, "expression");
|
|
1237
|
+
} else if (node.type === "NewExpression") {
|
|
1238
|
+
const {
|
|
1239
|
+
callee,
|
|
1240
|
+
arguments: { 0: arg0 },
|
|
1241
|
+
} = node;
|
|
1242
|
+
if (
|
|
1243
|
+
callee.type === "Identifier" &&
|
|
1244
|
+
arg0 &&
|
|
1245
|
+
// Observers from Standard Web APIs
|
|
1246
|
+
[
|
|
1247
|
+
"IntersectionObserver",
|
|
1248
|
+
"MutationObserver",
|
|
1249
|
+
"PerformanceObserver",
|
|
1250
|
+
"ReportingObserver",
|
|
1251
|
+
"ResizeObserver",
|
|
1252
|
+
].includes(callee.name)
|
|
1253
|
+
) {
|
|
1254
|
+
// Observers callbacks are NOT tracked scopes. However, they
|
|
1255
|
+
// don't need to react to updates to reactive variables; it's okay
|
|
1256
|
+
// to poll the current value. Consider them called-function tracked
|
|
1257
|
+
// scopes for our purposes.
|
|
1258
|
+
pushTrackedScope(arg0, "called-function");
|
|
1259
|
+
}
|
|
1260
|
+
} else if (node.type === "CallExpression") {
|
|
1261
|
+
if (node.callee.type === "Identifier") {
|
|
1262
|
+
const {
|
|
1263
|
+
callee,
|
|
1264
|
+
arguments: { 0: arg0, 1: arg1 },
|
|
1265
|
+
} = node;
|
|
1266
|
+
if (
|
|
1267
|
+
matchImport(
|
|
1268
|
+
[
|
|
1269
|
+
"createMemo",
|
|
1270
|
+
"children",
|
|
1271
|
+
"createEffect",
|
|
1272
|
+
"createRenderEffect",
|
|
1273
|
+
"createDeferred",
|
|
1274
|
+
"createComputed",
|
|
1275
|
+
"createSelector",
|
|
1276
|
+
"mapArray",
|
|
1277
|
+
"indexArray",
|
|
1278
|
+
"observable",
|
|
1279
|
+
// Solid 2.0
|
|
1280
|
+
"createTrackedEffect",
|
|
1281
|
+
"isPending",
|
|
1282
|
+
"latest",
|
|
1283
|
+
"resolve",
|
|
1284
|
+
"deep",
|
|
1285
|
+
"repeat",
|
|
1286
|
+
"createErrorBoundary",
|
|
1287
|
+
"createLoadingBoundary",
|
|
1288
|
+
"createRevealOrder",
|
|
1289
|
+
],
|
|
1290
|
+
callee.name
|
|
1291
|
+
)
|
|
1292
|
+
) {
|
|
1293
|
+
// Solid 2 computations may settle asynchronously. This does not
|
|
1294
|
+
// extend dependency tracking beyond the first suspension point.
|
|
1295
|
+
pushTrackedScope(
|
|
1296
|
+
arg0,
|
|
1297
|
+
"function",
|
|
1298
|
+
Boolean(matchImport("createMemo", callee.name)) ||
|
|
1299
|
+
(v2 && Boolean(matchImport(["createEffect", "createRenderEffect"], callee.name)))
|
|
1300
|
+
);
|
|
1301
|
+
if (
|
|
1302
|
+
matchImport(["createErrorBoundary", "createLoadingBoundary"], callee.name) &&
|
|
1303
|
+
arg1
|
|
1304
|
+
) {
|
|
1305
|
+
// Boundary fallbacks (Solid 2.0) are reactive accessors; the error fallback
|
|
1306
|
+
// also receives (err, reset) arguments
|
|
1307
|
+
pushTrackedScope(arg1, "called-function");
|
|
1308
|
+
}
|
|
1309
|
+
if (matchImport("createEffect", callee.name) && isFunctionNode(arg1)) {
|
|
1310
|
+
// Solid 2.0 split effects: createEffect(compute, effect) runs the second
|
|
1311
|
+
// function with the computed value, untracked. In Solid 1.x the second
|
|
1312
|
+
// argument is an initial value, so a function there is safe to treat as a
|
|
1313
|
+
// called function under both semantics.
|
|
1314
|
+
pushTrackedScope(arg1, "called-function");
|
|
1315
|
+
}
|
|
1316
|
+
} else if (matchImport("untrack", callee.name) && arg0) {
|
|
1317
|
+
// untrack's callback is called synchronously but explicitly NOT
|
|
1318
|
+
// tracked: reads inside are sanctioned, and an async callback is
|
|
1319
|
+
// harmless (there's no subscription to lose), so it must not get
|
|
1320
|
+
// the async-tracked-scope or read-after-await treatment (#188).
|
|
1321
|
+
pushTrackedScope(arg0, "called-function");
|
|
1322
|
+
} else if (matchImport("createResource", callee.name)) {
|
|
1323
|
+
// createResource(fetcher), createResource(fetcher, options),
|
|
1324
|
+
// createResource(source, fetcher), or createResource(source, fetcher, options).
|
|
1325
|
+
// Only when the second argument is a function is the first a reactive source
|
|
1326
|
+
// (a sync tracked scope). The fetcher itself is not tracked — it receives the
|
|
1327
|
+
// source's value as an argument and may be async.
|
|
1328
|
+
const arg1IsFunction = arg1 && isFunctionNode(trace(arg1, context));
|
|
1329
|
+
if (arg1IsFunction) {
|
|
1330
|
+
pushTrackedScope(arg0, "function");
|
|
1331
|
+
pushTrackedScope(arg1, "called-function");
|
|
1332
|
+
} else if (arg0) {
|
|
1333
|
+
pushTrackedScope(arg0, "called-function");
|
|
1334
|
+
}
|
|
1335
|
+
} else if (matchImport(["mergeProps", "merge"], callee.name)) {
|
|
1336
|
+
// mergeProps (Solid 1.x) and merge (Solid 2.0) wrap function sources in
|
|
1337
|
+
// createMemo, so function arguments are genuinely tracked scopes.
|
|
1338
|
+
for (const arg of node.arguments) {
|
|
1339
|
+
if (arg.type === "SpreadElement") continue;
|
|
1340
|
+
if (isFunctionNode(trace(arg, context))) {
|
|
1341
|
+
pushTrackedScope(arg, "function");
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
} else if (
|
|
1345
|
+
matchImport(
|
|
1346
|
+
[
|
|
1347
|
+
"createSignal",
|
|
1348
|
+
"createStore",
|
|
1349
|
+
"createProjection",
|
|
1350
|
+
"createOptimistic",
|
|
1351
|
+
"createOptimisticStore",
|
|
1352
|
+
],
|
|
1353
|
+
callee.name
|
|
1354
|
+
) &&
|
|
1355
|
+
isFunctionNode(arg0)
|
|
1356
|
+
) {
|
|
1357
|
+
// Solid 2.0 function-form derived primitives: the function argument is a
|
|
1358
|
+
// tracked scope, and may be async (async computations are first-class; only
|
|
1359
|
+
// reads before the first `await` are tracked). In Solid 1.x a function passed
|
|
1360
|
+
// to createSignal is just a stored value, so treating it as a tracked scope
|
|
1361
|
+
// is permissive under both semantics.
|
|
1362
|
+
pushTrackedScope(arg0, "function", true);
|
|
1363
|
+
} else if (
|
|
1364
|
+
matchImport(["onMount", "onCleanup", "onError", "onSettled", "action"], callee.name) ||
|
|
1365
|
+
[
|
|
1366
|
+
// Timers
|
|
1367
|
+
"setInterval",
|
|
1368
|
+
"setTimeout",
|
|
1369
|
+
"setImmediate",
|
|
1370
|
+
"requestAnimationFrame",
|
|
1371
|
+
"requestIdleCallback",
|
|
1372
|
+
].includes(callee.name)
|
|
1373
|
+
) {
|
|
1374
|
+
// on* and timers are NOT tracked scopes. However, they
|
|
1375
|
+
// don't need to react to updates to reactive variables; it's okay
|
|
1376
|
+
// to poll the current value. Consider them called-function tracked
|
|
1377
|
+
// scopes for our purposes.
|
|
1378
|
+
pushTrackedScope(arg0, "called-function");
|
|
1379
|
+
} else if (matchImport("on", callee.name)) {
|
|
1380
|
+
// on accepts a signal or an array of signals as its first argument,
|
|
1381
|
+
// and a tracking function as its second
|
|
1382
|
+
if (arg0) {
|
|
1383
|
+
if (arg0.type === "ArrayExpression") {
|
|
1384
|
+
arg0.elements.forEach((element) => {
|
|
1385
|
+
if (element && element?.type !== "SpreadElement") {
|
|
1386
|
+
pushTrackedScope(element, "function");
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
} else {
|
|
1390
|
+
pushTrackedScope(arg0, "function");
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (arg1) {
|
|
1394
|
+
// Since dependencies are known, function can be async
|
|
1395
|
+
pushTrackedScope(arg1, "called-function");
|
|
1396
|
+
}
|
|
1397
|
+
} else if (matchImport("createStore", callee.name) && arg0?.type === "ObjectExpression") {
|
|
1398
|
+
for (const property of arg0.properties) {
|
|
1399
|
+
if (
|
|
1400
|
+
property.type === "Property" &&
|
|
1401
|
+
property.kind === "get" &&
|
|
1402
|
+
isFunctionNode(property.value)
|
|
1403
|
+
) {
|
|
1404
|
+
pushTrackedScope(property.value, "function");
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
} else if (matchImport("runWithOwner", callee.name)) {
|
|
1408
|
+
// runWithOwner(owner, fn) only creates a tracked scope if `owner =
|
|
1409
|
+
// getOwner()` runs in a tracked scope. If owner is a variable,
|
|
1410
|
+
// attempt to detect if it's a tracked scope or not, but if this
|
|
1411
|
+
// can't be done, assume it's a tracked scope.
|
|
1412
|
+
if (arg1) {
|
|
1413
|
+
let isTrackedScope = true;
|
|
1414
|
+
const owner = arg0.type === "Identifier" && findVariable(context, arg0);
|
|
1415
|
+
if (owner) {
|
|
1416
|
+
const decl = owner.defs[0];
|
|
1417
|
+
if (
|
|
1418
|
+
decl &&
|
|
1419
|
+
decl.node.type === "VariableDeclarator" &&
|
|
1420
|
+
decl.node.init?.type === "CallExpression" &&
|
|
1421
|
+
decl.node.init.callee.type === "Identifier" &&
|
|
1422
|
+
matchImport("getOwner", decl.node.init.callee.name)
|
|
1423
|
+
) {
|
|
1424
|
+
// Check if the function in which getOwner() is called is a tracked scope. If the scopeStack
|
|
1425
|
+
// has moved on from that scope already, assume it's tracked, since that's less intrusive.
|
|
1426
|
+
const ownerFunction = findParent(decl.node, isProgramOrFunctionNode);
|
|
1427
|
+
const scopeStackIndex = scopeStack.findIndex(
|
|
1428
|
+
({ node }) => ownerFunction === node
|
|
1429
|
+
);
|
|
1430
|
+
if (
|
|
1431
|
+
(scopeStackIndex >= 1 &&
|
|
1432
|
+
!scopeStack[scopeStackIndex - 1].trackedScopes.some(
|
|
1433
|
+
(trackedScope) =>
|
|
1434
|
+
trackedScope.expect === "function" && trackedScope.node === ownerFunction
|
|
1435
|
+
)) ||
|
|
1436
|
+
scopeStackIndex === 0
|
|
1437
|
+
) {
|
|
1438
|
+
isTrackedScope = false;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (isTrackedScope) {
|
|
1443
|
+
pushTrackedScope(arg1, "function");
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
} else if (
|
|
1447
|
+
/^(?:use|create)[A-Z]/.test(callee.name) ||
|
|
1448
|
+
matchesCustomReactive(callee.name)
|
|
1449
|
+
) {
|
|
1450
|
+
// Custom hooks parameters may or may not be tracking scopes, no way to know.
|
|
1451
|
+
// Assume all identifier/function arguments are tracked scopes, and use "called-function"
|
|
1452
|
+
// to allow async handlers (permissive). Assume non-resolvable args are reactive expressions.
|
|
1453
|
+
for (const arg of node.arguments) {
|
|
1454
|
+
permissivelyTrackNode(arg);
|
|
1455
|
+
}
|
|
1456
|
+
} else if (
|
|
1457
|
+
parentScope()?.trackedScopes.some(
|
|
1458
|
+
(trackedScope) =>
|
|
1459
|
+
(trackedScope.expect === "function" || trackedScope.expect === "called-function") &&
|
|
1460
|
+
trackedScope.node === currentScope().node
|
|
1461
|
+
)
|
|
1462
|
+
) {
|
|
1463
|
+
// An unknown call inside a tracked scope (e.g. a helper called in an effect
|
|
1464
|
+
// callback). Functions passed to it either run synchronously — still inside the
|
|
1465
|
+
// tracked scope — or run later, where polling current values is fine, like any
|
|
1466
|
+
// called function. Known Solid APIs that intentionally decline to track their
|
|
1467
|
+
// callbacks (like runWithOwner with an untracked owner) match earlier branches.
|
|
1468
|
+
for (const arg of node.arguments) {
|
|
1469
|
+
if (isFunctionNode(arg)) {
|
|
1470
|
+
pushTrackedScope(arg, "called-function");
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
} else if (node.callee.type === "MemberExpression") {
|
|
1475
|
+
const { property } = node.callee;
|
|
1476
|
+
if (
|
|
1477
|
+
property.type === "Identifier" &&
|
|
1478
|
+
property.name === "addEventListener" &&
|
|
1479
|
+
node.arguments.length >= 2
|
|
1480
|
+
) {
|
|
1481
|
+
// Like `on*` event handlers, mark all `addEventListener` listeners as called functions.
|
|
1482
|
+
pushTrackedScope(node.arguments[1], "called-function");
|
|
1483
|
+
} else if (
|
|
1484
|
+
property.type === "Identifier" &&
|
|
1485
|
+
node.callee.object.type === "Identifier" &&
|
|
1486
|
+
["window", "globalThis", "self"].includes(node.callee.object.name) &&
|
|
1487
|
+
[
|
|
1488
|
+
"setInterval",
|
|
1489
|
+
"setTimeout",
|
|
1490
|
+
"setImmediate",
|
|
1491
|
+
"requestAnimationFrame",
|
|
1492
|
+
"requestIdleCallback",
|
|
1493
|
+
].includes(property.name)
|
|
1494
|
+
) {
|
|
1495
|
+
// `window.setTimeout` etc. behave exactly like the bare timer globals above:
|
|
1496
|
+
// callbacks are called functions, free to poll current reactive values.
|
|
1497
|
+
if (node.arguments[0]) {
|
|
1498
|
+
pushTrackedScope(node.arguments[0], "called-function");
|
|
1499
|
+
}
|
|
1500
|
+
} else if (
|
|
1501
|
+
property.type === "Identifier" &&
|
|
1502
|
+
(/^(?:use|create)[A-Z]/.test(property.name) || matchesCustomReactive(property.name))
|
|
1503
|
+
) {
|
|
1504
|
+
// Handle custom hook parameters for property access custom hooks
|
|
1505
|
+
for (const arg of node.arguments) {
|
|
1506
|
+
permissivelyTrackNode(arg);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
} else if (node.type === "VariableDeclarator") {
|
|
1511
|
+
// Solid 1.3 createReactive (renamed createReaction?) returns a track
|
|
1512
|
+
// function, a tracked scope expecting a reactive function. All of the
|
|
1513
|
+
// track function's references where it's called push a tracked scope.
|
|
1514
|
+
if (node.init?.type === "CallExpression" && node.init.callee.type === "Identifier") {
|
|
1515
|
+
if (matchImport(["createReactive", "createReaction"], node.init.callee.name)) {
|
|
1516
|
+
const track = getReturnedVar(node.id, context);
|
|
1517
|
+
if (track) {
|
|
1518
|
+
for (const reference of track.references) {
|
|
1519
|
+
if (
|
|
1520
|
+
!reference.init &&
|
|
1521
|
+
reference.isReadOnly() &&
|
|
1522
|
+
reference.identifier.parent?.type === "CallExpression" &&
|
|
1523
|
+
reference.identifier.parent.callee === reference.identifier
|
|
1524
|
+
) {
|
|
1525
|
+
const arg0 = reference.identifier.parent.arguments[0];
|
|
1526
|
+
if (arg0) {
|
|
1527
|
+
pushTrackedScope(arg0, "function");
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (isFunctionNode(node.init.arguments[0])) {
|
|
1533
|
+
pushTrackedScope(node.init.arguments[0], "called-function");
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
} else if (node.type === "AssignmentExpression") {
|
|
1538
|
+
if (
|
|
1539
|
+
node.left.type === "MemberExpression" &&
|
|
1540
|
+
node.left.property.type === "Identifier" &&
|
|
1541
|
+
isFunctionNode(node.right) &&
|
|
1542
|
+
/^on[a-z]+$/.test(node.left.property.name)
|
|
1543
|
+
) {
|
|
1544
|
+
// To allow (questionable) code like the following example:
|
|
1545
|
+
// ref.oninput = () = {
|
|
1546
|
+
// if (!errors[ref.name]) return;
|
|
1547
|
+
// ...
|
|
1548
|
+
// }
|
|
1549
|
+
// where event handlers are manually attached to refs, detect these
|
|
1550
|
+
// scenarios and mark the right hand sides as tracked scopes expecting
|
|
1551
|
+
// functions.
|
|
1552
|
+
pushTrackedScope(node.right, "called-function");
|
|
1553
|
+
}
|
|
1554
|
+
} else if (node.type === "TaggedTemplateExpression") {
|
|
1555
|
+
for (const expression of node.quasi.expressions) {
|
|
1556
|
+
if (isFunctionNode(expression)) {
|
|
1557
|
+
// ex. css`color: ${props => props.color}`. Use "called-function" to allow async handlers (permissive)
|
|
1558
|
+
pushTrackedScope(expression, "called-function");
|
|
1559
|
+
|
|
1560
|
+
// exception case: add a reactive variable within checkForTrackedScopes when a param is props
|
|
1561
|
+
for (const param of expression.params) {
|
|
1562
|
+
if (param.type === "Identifier" && isPropsByName(param.name)) {
|
|
1563
|
+
const variable = findVariable(context, param);
|
|
1564
|
+
if (variable) scopeStack.pushProps(variable, currentScope().node);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
return {
|
|
1573
|
+
ImportDeclaration: handleImportDeclaration,
|
|
1574
|
+
JSXExpressionContainer(node: T.JSXExpressionContainer) {
|
|
1575
|
+
checkForTrackedScopes(node);
|
|
1576
|
+
},
|
|
1577
|
+
JSXSpreadAttribute(node: T.JSXSpreadAttribute) {
|
|
1578
|
+
checkForTrackedScopes(node);
|
|
1579
|
+
},
|
|
1580
|
+
CallExpression(node: T.CallExpression) {
|
|
1581
|
+
checkForTrackedScopes(node);
|
|
1582
|
+
checkForSyncCallbacks(node);
|
|
1583
|
+
|
|
1584
|
+
// ensure calls to reactive primitives use the results. Directly-returned calls
|
|
1585
|
+
// (`return createMemo(...)` or as an arrow body) hand their result to the caller,
|
|
1586
|
+
// like a custom primitive, so there is nothing to capture in this scope.
|
|
1587
|
+
const parent = node.parent && ignoreTransparentWrappers(node.parent, true);
|
|
1588
|
+
if (
|
|
1589
|
+
parent?.type !== "AssignmentExpression" &&
|
|
1590
|
+
parent?.type !== "VariableDeclarator" &&
|
|
1591
|
+
parent?.type !== "ReturnStatement" &&
|
|
1592
|
+
!(parent?.type === "ArrowFunctionExpression" && parent.body === node)
|
|
1593
|
+
) {
|
|
1594
|
+
if (parent?.type === "CallExpression" && parent.arguments.includes(node)) {
|
|
1595
|
+
// A primitive call passed to a wrapper, like
|
|
1596
|
+
// `const [state, setState] = makePersisted(createSignal(false))`
|
|
1597
|
+
// (#190). The dominant ecosystem contract passes the tuple through,
|
|
1598
|
+
// so if the outermost wrapper's result is captured, analyze the
|
|
1599
|
+
// primitive as if assigned there directly. If the result isn't
|
|
1600
|
+
// captured, the tuple escaped into the wrapper — nothing can be
|
|
1601
|
+
// concluded, so stay silent rather than warn.
|
|
1602
|
+
let outer: T.Node = parent;
|
|
1603
|
+
let outerParent = outer.parent && ignoreTransparentWrappers(outer.parent, true);
|
|
1604
|
+
while (
|
|
1605
|
+
outerParent?.type === "CallExpression" &&
|
|
1606
|
+
outerParent.arguments.includes(outer as T.CallExpressionArgument)
|
|
1607
|
+
) {
|
|
1608
|
+
outer = outerParent;
|
|
1609
|
+
outerParent = outer.parent && ignoreTransparentWrappers(outer.parent, true);
|
|
1610
|
+
}
|
|
1611
|
+
if (outerParent?.type === "VariableDeclarator") {
|
|
1612
|
+
checkForReactiveAssignment(outerParent.id, node);
|
|
1613
|
+
} else if (
|
|
1614
|
+
outerParent?.type === "AssignmentExpression" &&
|
|
1615
|
+
outerParent.left.type !== "MemberExpression"
|
|
1616
|
+
) {
|
|
1617
|
+
checkForReactiveAssignment(outerParent.left, node);
|
|
1618
|
+
}
|
|
1619
|
+
} else {
|
|
1620
|
+
checkForReactiveAssignment(null, node);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1624
|
+
NewExpression(node: T.NewExpression) {
|
|
1625
|
+
checkForTrackedScopes(node);
|
|
1626
|
+
},
|
|
1627
|
+
VariableDeclarator(node: T.VariableDeclarator) {
|
|
1628
|
+
if (node.init) {
|
|
1629
|
+
checkForReactiveAssignment(node.id, node.init);
|
|
1630
|
+
checkForTrackedScopes(node);
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
AssignmentExpression(node: T.AssignmentExpression) {
|
|
1634
|
+
if (node.left.type !== "MemberExpression") {
|
|
1635
|
+
checkForReactiveAssignment(node.left, node.right);
|
|
1636
|
+
}
|
|
1637
|
+
checkForTrackedScopes(node);
|
|
1638
|
+
},
|
|
1639
|
+
TaggedTemplateExpression(node: T.TaggedTemplateExpression) {
|
|
1640
|
+
checkForTrackedScopes(node);
|
|
1641
|
+
},
|
|
1642
|
+
"JSXElement > JSXExpressionContainer > :function"(node: T.Node) {
|
|
1643
|
+
if (
|
|
1644
|
+
isFunctionNode(node) &&
|
|
1645
|
+
node.parent?.type === "JSXExpressionContainer" &&
|
|
1646
|
+
node.parent.parent?.type === "JSXElement"
|
|
1647
|
+
) {
|
|
1648
|
+
const element = node.parent.parent;
|
|
1649
|
+
|
|
1650
|
+
if (element.openingElement.name.type === "JSXIdentifier") {
|
|
1651
|
+
const tagName = element.openingElement.name.name;
|
|
1652
|
+
if (matchImport("For", tagName)) {
|
|
1653
|
+
// In Solid 2.0, <For> callback params depend on the `keyed` prop:
|
|
1654
|
+
// - absent, bare `keyed`, or `keyed={true}`: (item, index-accessor), same as 1.x
|
|
1655
|
+
// - `keyed={false}`: (item-accessor, index), the old <Index> shape
|
|
1656
|
+
// - `keyed={fn}`: (item-accessor, index-accessor)
|
|
1657
|
+
const keyedAttr = element.openingElement.attributes.find(
|
|
1658
|
+
(attr): attr is T.JSXAttribute =>
|
|
1659
|
+
attr.type === "JSXAttribute" &&
|
|
1660
|
+
attr.name.type === "JSXIdentifier" &&
|
|
1661
|
+
attr.name.name === "keyed"
|
|
1662
|
+
);
|
|
1663
|
+
let itemIsSignal = false;
|
|
1664
|
+
let indexIsSignal = true;
|
|
1665
|
+
if (keyedAttr && keyedAttr.value?.type === "JSXExpressionContainer") {
|
|
1666
|
+
const keyedValue = keyedAttr.value.expression;
|
|
1667
|
+
if (keyedValue.type === "Literal" && keyedValue.value === false) {
|
|
1668
|
+
itemIsSignal = true;
|
|
1669
|
+
indexIsSignal = false;
|
|
1670
|
+
} else if (!(keyedValue.type === "Literal" && keyedValue.value === true)) {
|
|
1671
|
+
// key function: both params are accessors
|
|
1672
|
+
itemIsSignal = true;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
if (itemIsSignal && node.params.length >= 1 && node.params[0].type === "Identifier") {
|
|
1676
|
+
const item = findVariable(context, node.params[0]);
|
|
1677
|
+
if (item) {
|
|
1678
|
+
scopeStack.pushSignal(item, currentScope().node);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (
|
|
1682
|
+
indexIsSignal &&
|
|
1683
|
+
node.params.length === 2 &&
|
|
1684
|
+
node.params[1].type === "Identifier"
|
|
1685
|
+
) {
|
|
1686
|
+
// Mark `index` in `<For>{(item, index) => <div /></For>` as a signal
|
|
1687
|
+
const index = findVariable(context, node.params[1]);
|
|
1688
|
+
if (index) {
|
|
1689
|
+
scopeStack.pushSignal(index, currentScope().node);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
} else if (
|
|
1693
|
+
matchImport("Index", tagName) &&
|
|
1694
|
+
node.params.length >= 1 &&
|
|
1695
|
+
node.params[0].type === "Identifier"
|
|
1696
|
+
) {
|
|
1697
|
+
// Mark `item` in `<Index>{(item, index) => <div />}</Index>` as a signal
|
|
1698
|
+
const item = findVariable(context, node.params[0]);
|
|
1699
|
+
if (item) {
|
|
1700
|
+
scopeStack.pushSignal(item, currentScope().node);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
},
|
|
1706
|
+
|
|
1707
|
+
/* Function enter/exit */
|
|
1708
|
+
FunctionExpression: onFunctionEnter,
|
|
1709
|
+
ArrowFunctionExpression: onFunctionEnter,
|
|
1710
|
+
FunctionDeclaration: onFunctionEnter,
|
|
1711
|
+
Program: onFunctionEnter,
|
|
1712
|
+
"FunctionExpression:exit": onFunctionExit,
|
|
1713
|
+
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
1714
|
+
"FunctionDeclaration:exit": onFunctionExit,
|
|
1715
|
+
"Program:exit": onFunctionExit,
|
|
1716
|
+
|
|
1717
|
+
/* Detect JSX for adding props */
|
|
1718
|
+
JSXElement() {
|
|
1719
|
+
if (scopeStack.length) {
|
|
1720
|
+
currentScope().hasJSX = true;
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
JSXFragment() {
|
|
1724
|
+
if (scopeStack.length) {
|
|
1725
|
+
currentScope().hasJSX = true;
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
};
|
|
1729
|
+
},
|
|
1730
|
+
});
|