@murky-web/oxlint-plugin-solid 0.0.1
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/README.md +62 -0
- package/package.json +44 -0
- package/src/compat.mjs +53 -0
- package/src/index.mjs +56 -0
- package/src/rules/components_return_once.mjs +202 -0
- package/src/rules/event_handlers.mjs +298 -0
- package/src/rules/imports.mjs +205 -0
- package/src/rules/jsx_no_duplicate_props.mjs +87 -0
- package/src/rules/jsx_no_script_url.mjs +54 -0
- package/src/rules/jsx_no_undef.mjs +217 -0
- package/src/rules/jsx_uses_vars.mjs +55 -0
- package/src/rules/no_array_handlers.mjs +53 -0
- package/src/rules/no_destructure.mjs +210 -0
- package/src/rules/no_innerhtml.mjs +145 -0
- package/src/rules/no_proxy_apis.mjs +96 -0
- package/src/rules/no_react_deps.mjs +65 -0
- package/src/rules/no_react_specific_props.mjs +71 -0
- package/src/rules/no_unknown_namespaces.mjs +100 -0
- package/src/rules/prefer_arrow_components.mjs +411 -0
- package/src/rules/prefer_classlist.mjs +89 -0
- package/src/rules/prefer_for.mjs +92 -0
- package/src/rules/prefer_show.mjs +92 -0
- package/src/rules/reactivity.mjs +1300 -0
- package/src/rules/self_closing_comp.mjs +153 -0
- package/src/rules/style_prop.mjs +155 -0
- package/src/rules/validate_jsx_nesting.mjs +16 -0
- package/src/utils.mjs +337 -0
|
@@ -0,0 +1,1300 @@
|
|
|
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
|
+
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
|
|
6
|
+
import { traverse } from "estraverse";
|
|
7
|
+
|
|
8
|
+
import { findVariable, getSourceCode } from "../compat.mjs";
|
|
9
|
+
import {
|
|
10
|
+
findParent,
|
|
11
|
+
findInScope,
|
|
12
|
+
isPropsByName,
|
|
13
|
+
isFunctionNode,
|
|
14
|
+
isProgramOrFunctionNode,
|
|
15
|
+
trackImports,
|
|
16
|
+
isDOMElementName,
|
|
17
|
+
ignoreTransparentWrappers,
|
|
18
|
+
getFunctionName,
|
|
19
|
+
isJSXElementOrFragment,
|
|
20
|
+
trace,
|
|
21
|
+
} from "../utils.mjs";
|
|
22
|
+
const { getFunctionHeadLocation } = ASTUtils;
|
|
23
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
24
|
+
class ScopeStackItem {
|
|
25
|
+
/** the node for the current scope, or program if global scope */
|
|
26
|
+
node;
|
|
27
|
+
/**
|
|
28
|
+
* nodes whose descendants in the current scope are allowed to be reactive.
|
|
29
|
+
* JSXExpressionContainers can be any expression containing reactivity, while
|
|
30
|
+
* function nodes/identifiers are typically arguments to solid-js primitives
|
|
31
|
+
* and should match a tracked scope exactly.
|
|
32
|
+
*/
|
|
33
|
+
trackedScopes = [];
|
|
34
|
+
/** nameless functions with reactivity, should exactly match a tracked scope */
|
|
35
|
+
unnamedDerivedSignals = new Set();
|
|
36
|
+
/** switched to true by time of :exit if JSX is detected in the current scope */
|
|
37
|
+
hasJSX = false;
|
|
38
|
+
constructor(node) {
|
|
39
|
+
this.node = node;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
class ScopeStack extends Array {
|
|
43
|
+
currentScope = () => this[this.length - 1];
|
|
44
|
+
parentScope = () => this[this.length - 2];
|
|
45
|
+
/** Add references to a signal, memo, derived signal, etc. */
|
|
46
|
+
pushSignal(variable, declarationScope = this.currentScope().node) {
|
|
47
|
+
this.signals.push({
|
|
48
|
+
references: variable.references.filter(
|
|
49
|
+
(reference) => !reference.init,
|
|
50
|
+
),
|
|
51
|
+
variable,
|
|
52
|
+
declarationScope,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Add references to a signal, merging with existing references if the
|
|
57
|
+
* variable is the same. Derived signals are special; they don't use the
|
|
58
|
+
* declaration scope of the function, but rather the minimum declaration scope
|
|
59
|
+
* of any signals they contain.
|
|
60
|
+
*/
|
|
61
|
+
pushUniqueSignal(variable, declarationScope) {
|
|
62
|
+
const foundSignal = this.signals.find((s) => s.variable === variable);
|
|
63
|
+
if (!foundSignal) {
|
|
64
|
+
this.pushSignal(variable, declarationScope);
|
|
65
|
+
} else {
|
|
66
|
+
foundSignal.declarationScope = this.findDeepestDeclarationScope(
|
|
67
|
+
foundSignal.declarationScope,
|
|
68
|
+
declarationScope,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Add references to a props or store. */
|
|
73
|
+
pushProps(variable, declarationScope = this.currentScope().node) {
|
|
74
|
+
this.props.push({
|
|
75
|
+
references: variable.references.filter(
|
|
76
|
+
(reference) => !reference.init,
|
|
77
|
+
),
|
|
78
|
+
variable,
|
|
79
|
+
declarationScope,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Function callbacks that run synchronously and don't create a new scope. */
|
|
83
|
+
syncCallbacks = new Set();
|
|
84
|
+
/**
|
|
85
|
+
* Iterate through and remove the signal references in the current scope.
|
|
86
|
+
* That way, the next Scope up can safely check for references in its scope.
|
|
87
|
+
*/
|
|
88
|
+
*consumeSignalReferencesInScope() {
|
|
89
|
+
yield* this.consumeReferencesInScope(this.signals);
|
|
90
|
+
this.signals = this.signals.filter(
|
|
91
|
+
(variable) => variable.references.length !== 0,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
/** Iterate through and remove the props references in the current scope. */
|
|
95
|
+
*consumePropsReferencesInScope() {
|
|
96
|
+
yield* this.consumeReferencesInScope(this.props);
|
|
97
|
+
this.props = this.props.filter(
|
|
98
|
+
(variable) => variable.references.length !== 0,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
*consumeReferencesInScope(variables) {
|
|
102
|
+
for (const variable of variables) {
|
|
103
|
+
const { references } = variable;
|
|
104
|
+
const inScope = [],
|
|
105
|
+
notInScope = [];
|
|
106
|
+
references.forEach((reference) => {
|
|
107
|
+
if (this.isReferenceInCurrentScope(reference)) {
|
|
108
|
+
inScope.push(reference);
|
|
109
|
+
} else {
|
|
110
|
+
notInScope.push(reference);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
yield* inScope.map((reference) => ({
|
|
114
|
+
reference,
|
|
115
|
+
declarationScope: variable.declarationScope,
|
|
116
|
+
}));
|
|
117
|
+
// I don't think this is needed! Just a perf optimization
|
|
118
|
+
variable.references = notInScope;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Returns the function node deepest in the tree. Assumes a === b, a is inside b, or b is inside a. */
|
|
122
|
+
findDeepestDeclarationScope = (a, b) => {
|
|
123
|
+
if (a === b) return a;
|
|
124
|
+
for (let i = this.length - 1; i >= 0; i -= 1) {
|
|
125
|
+
const { node } = this[i];
|
|
126
|
+
if (a === node || b === node) {
|
|
127
|
+
return node;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw new Error("This should never happen");
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Returns true if the reference is in the current scope, handling sync
|
|
134
|
+
* callbacks. Must be called on the :exit pass only.
|
|
135
|
+
*/
|
|
136
|
+
isReferenceInCurrentScope(reference) {
|
|
137
|
+
let parentFunction = findParent(
|
|
138
|
+
reference.identifier,
|
|
139
|
+
isProgramOrFunctionNode,
|
|
140
|
+
);
|
|
141
|
+
while (
|
|
142
|
+
isFunctionNode(parentFunction) &&
|
|
143
|
+
this.syncCallbacks.has(parentFunction)
|
|
144
|
+
) {
|
|
145
|
+
parentFunction = findParent(
|
|
146
|
+
parentFunction,
|
|
147
|
+
isProgramOrFunctionNode,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return parentFunction === this.currentScope().node;
|
|
151
|
+
}
|
|
152
|
+
/** variable references to be treated as signals, memos, derived signals, etc. */
|
|
153
|
+
signals = [];
|
|
154
|
+
/** variables references to be treated as props (or stores) */
|
|
155
|
+
props = [];
|
|
156
|
+
}
|
|
157
|
+
const getNthDestructuredVar = (id, n, context) => {
|
|
158
|
+
if (id?.type === "ArrayPattern") {
|
|
159
|
+
const el = id.elements[n];
|
|
160
|
+
if (el?.type === "Identifier") {
|
|
161
|
+
return findVariable(context, el);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
166
|
+
const getReturnedVar = (id, context) => {
|
|
167
|
+
if (id.type === "Identifier") {
|
|
168
|
+
return findVariable(context, id);
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
172
|
+
export default createRule({
|
|
173
|
+
meta: {
|
|
174
|
+
type: "problem",
|
|
175
|
+
docs: {
|
|
176
|
+
description:
|
|
177
|
+
"Enforce that reactivity (props, signals, memos, etc.) is properly used, so changes in those values will be tracked and update the view as expected.",
|
|
178
|
+
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/reactivity.md",
|
|
179
|
+
},
|
|
180
|
+
schema: [
|
|
181
|
+
{
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
customReactiveFunctions: {
|
|
185
|
+
description:
|
|
186
|
+
"List of function names to consider as reactive functions (allow signals to be safely passed as arguments). In addition, any create* or use* functions are automatically included.",
|
|
187
|
+
type: "array",
|
|
188
|
+
items: {
|
|
189
|
+
type: "string",
|
|
190
|
+
},
|
|
191
|
+
default: [],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
additionalProperties: false,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
messages: {
|
|
198
|
+
noWrite:
|
|
199
|
+
"The reactive variable '{{name}}' should not be reassigned or altered directly.",
|
|
200
|
+
untrackedReactive:
|
|
201
|
+
"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.",
|
|
202
|
+
expectedFunctionGotExpression:
|
|
203
|
+
"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.",
|
|
204
|
+
badSignal:
|
|
205
|
+
"The reactive variable '{{name}}' should be called as a function when used in {{where}}.",
|
|
206
|
+
badUnnamedDerivedSignal:
|
|
207
|
+
"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.",
|
|
208
|
+
shouldDestructure:
|
|
209
|
+
"For proper analysis, array destructuring should be used to capture the {{nth}}result of this function call.",
|
|
210
|
+
shouldAssign:
|
|
211
|
+
"For proper analysis, a variable should be used to capture the result of this function call.",
|
|
212
|
+
noAsyncTrackedScope:
|
|
213
|
+
"This tracked scope should not be async. Solid's reactivity only tracks synchronously.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
defaultOptions: [
|
|
217
|
+
{
|
|
218
|
+
customReactiveFunctions: [],
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
create(context, [options]) {
|
|
222
|
+
const warnShouldDestructure = (node, nth) =>
|
|
223
|
+
context.report({
|
|
224
|
+
node,
|
|
225
|
+
messageId: "shouldDestructure",
|
|
226
|
+
data: nth ? { nth: nth + " " } : undefined,
|
|
227
|
+
});
|
|
228
|
+
const warnShouldAssign = (node) =>
|
|
229
|
+
context.report({ node, messageId: "shouldAssign" });
|
|
230
|
+
const sourceCode = getSourceCode(context);
|
|
231
|
+
/** Represents the lexical function stack and relevant information for each function */
|
|
232
|
+
const scopeStack = new ScopeStack();
|
|
233
|
+
const { currentScope, parentScope } = scopeStack;
|
|
234
|
+
/** Tracks imports from 'solid-js', handling aliases. */
|
|
235
|
+
const { matchImport, handleImportDeclaration } = trackImports();
|
|
236
|
+
/** Workaround for #61 */
|
|
237
|
+
const markPropsOnCondition = (node, cb) => {
|
|
238
|
+
if (
|
|
239
|
+
node.params.length === 1 &&
|
|
240
|
+
node.params[0].type === "Identifier" &&
|
|
241
|
+
node.parent?.type !== "JSXExpressionContainer" && // "render props" aren't components
|
|
242
|
+
node.parent?.type !== "TemplateLiteral" && // inline functions in tagged template literals aren't components
|
|
243
|
+
cb(node.params[0])
|
|
244
|
+
) {
|
|
245
|
+
// This function is a component, consider its parameter a props
|
|
246
|
+
const propsParam = findVariable(context, node.params[0]);
|
|
247
|
+
if (propsParam) {
|
|
248
|
+
scopeStack.pushProps(propsParam, node);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
/** Populates the function stack. */
|
|
253
|
+
const onFunctionEnter = (node) => {
|
|
254
|
+
if (isFunctionNode(node)) {
|
|
255
|
+
if (scopeStack.syncCallbacks.has(node)) {
|
|
256
|
+
// Ignore sync callbacks like Array#forEach and certain Solid primitives
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
markPropsOnCondition(node, (props) =>
|
|
260
|
+
isPropsByName(props.name),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
scopeStack.push(new ScopeStackItem(node));
|
|
264
|
+
};
|
|
265
|
+
/** Returns whether a node falls under a tracked scope in the current function scope */
|
|
266
|
+
const matchTrackedScope = (trackedScope, node) => {
|
|
267
|
+
switch (trackedScope.expect) {
|
|
268
|
+
case "function":
|
|
269
|
+
case "called-function":
|
|
270
|
+
return node === trackedScope.node;
|
|
271
|
+
case "expression":
|
|
272
|
+
return Boolean(
|
|
273
|
+
findInScope(
|
|
274
|
+
node,
|
|
275
|
+
currentScope().node,
|
|
276
|
+
(node) => node === trackedScope.node,
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
/** Inspects a specific reference of a reactive variable for correct handling. */
|
|
282
|
+
const handleTrackedScopes = (identifier, declarationScope) => {
|
|
283
|
+
const currentScopeNode = currentScope().node;
|
|
284
|
+
// Check if the call falls outside any tracked scopes in the current scope
|
|
285
|
+
if (
|
|
286
|
+
!currentScope().trackedScopes.find((trackedScope) =>
|
|
287
|
+
matchTrackedScope(trackedScope, identifier),
|
|
288
|
+
)
|
|
289
|
+
) {
|
|
290
|
+
const matchedExpression = currentScope().trackedScopes.find(
|
|
291
|
+
(trackedScope) =>
|
|
292
|
+
matchTrackedScope(
|
|
293
|
+
{ ...trackedScope, expect: "expression" },
|
|
294
|
+
identifier,
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
if (declarationScope === currentScopeNode) {
|
|
298
|
+
// If the reactivity is not contained in a tracked scope, and any of
|
|
299
|
+
// the reactive variables were declared in the current scope, then we
|
|
300
|
+
// report them. When the reference is to an object in a
|
|
301
|
+
// MemberExpression (props/store) or a function call (signal), report
|
|
302
|
+
// that, otherwise the identifier.
|
|
303
|
+
let parentMemberExpression = null;
|
|
304
|
+
if (identifier.parent?.type === "MemberExpression") {
|
|
305
|
+
parentMemberExpression = identifier.parent;
|
|
306
|
+
while (
|
|
307
|
+
parentMemberExpression.parent?.type ===
|
|
308
|
+
"MemberExpression"
|
|
309
|
+
) {
|
|
310
|
+
parentMemberExpression =
|
|
311
|
+
parentMemberExpression.parent;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const parentCallExpression =
|
|
315
|
+
identifier.parent?.type === "CallExpression"
|
|
316
|
+
? identifier.parent
|
|
317
|
+
: null;
|
|
318
|
+
context.report({
|
|
319
|
+
node:
|
|
320
|
+
parentMemberExpression ??
|
|
321
|
+
parentCallExpression ??
|
|
322
|
+
identifier,
|
|
323
|
+
messageId: matchedExpression
|
|
324
|
+
? "expectedFunctionGotExpression"
|
|
325
|
+
: "untrackedReactive",
|
|
326
|
+
data: {
|
|
327
|
+
name: parentMemberExpression
|
|
328
|
+
? sourceCode.getText(parentMemberExpression)
|
|
329
|
+
: identifier.name,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
} else {
|
|
333
|
+
// If all of the reactive variables were declared above the current
|
|
334
|
+
// function scope, then the entire function becomes reactive with the
|
|
335
|
+
// deepest declaration scope of the reactive variables it contains.
|
|
336
|
+
// Let the next onFunctionExit up handle it.
|
|
337
|
+
if (!parentScope() || !isFunctionNode(currentScopeNode)) {
|
|
338
|
+
throw new Error("this shouldn't happen!");
|
|
339
|
+
}
|
|
340
|
+
// If the current function doesn't have an associated variable, that's
|
|
341
|
+
// fine, it's being used inline (i.e. anonymous arrow function). For
|
|
342
|
+
// this to be okay, the arrow function has to be the same node as one
|
|
343
|
+
// of the tracked scopes, as we can't easily find references.
|
|
344
|
+
const pushUnnamedDerivedSignal = () =>
|
|
345
|
+
(parentScope().unnamedDerivedSignals ??= new Set()).add(
|
|
346
|
+
currentScopeNode,
|
|
347
|
+
);
|
|
348
|
+
if (currentScopeNode.type === "FunctionDeclaration") {
|
|
349
|
+
// get variable representing function, function node only defines one variable
|
|
350
|
+
const functionVariable =
|
|
351
|
+
sourceCode.scopeManager?.getDeclaredVariables(
|
|
352
|
+
currentScopeNode,
|
|
353
|
+
)?.[0];
|
|
354
|
+
if (functionVariable) {
|
|
355
|
+
scopeStack.pushUniqueSignal(
|
|
356
|
+
functionVariable,
|
|
357
|
+
declarationScope, // use declaration scope of a signal contained in this function
|
|
358
|
+
);
|
|
359
|
+
} else {
|
|
360
|
+
pushUnnamedDerivedSignal();
|
|
361
|
+
}
|
|
362
|
+
} else if (
|
|
363
|
+
currentScopeNode.parent?.type === "VariableDeclarator"
|
|
364
|
+
) {
|
|
365
|
+
const declarator = currentScopeNode.parent;
|
|
366
|
+
// for nameless or arrow function expressions, use the declared variable it's assigned to
|
|
367
|
+
const functionVariable =
|
|
368
|
+
sourceCode.scopeManager?.getDeclaredVariables(
|
|
369
|
+
declarator,
|
|
370
|
+
)?.[0];
|
|
371
|
+
if (functionVariable) {
|
|
372
|
+
// use declaration scope of a signal contained in this scope, not the function itself
|
|
373
|
+
scopeStack.pushUniqueSignal(
|
|
374
|
+
functionVariable,
|
|
375
|
+
declarationScope,
|
|
376
|
+
);
|
|
377
|
+
} else {
|
|
378
|
+
pushUnnamedDerivedSignal();
|
|
379
|
+
}
|
|
380
|
+
} else if (currentScopeNode.parent?.type === "Property") {
|
|
381
|
+
// todo make this a unique props or something--for now, just ignore (unsafe)
|
|
382
|
+
} else {
|
|
383
|
+
pushUnnamedDerivedSignal();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
/** Performs all analysis and reporting. */
|
|
389
|
+
const onFunctionExit = (currentScopeNode) => {
|
|
390
|
+
// If this function is a component, add its props as a reactive variable
|
|
391
|
+
if (isFunctionNode(currentScopeNode)) {
|
|
392
|
+
markPropsOnCondition(currentScopeNode, (props) => {
|
|
393
|
+
if (
|
|
394
|
+
!isPropsByName(props.name) && // already added in markPropsOnEnter
|
|
395
|
+
currentScope().hasJSX
|
|
396
|
+
) {
|
|
397
|
+
const functionName = getFunctionName(currentScopeNode);
|
|
398
|
+
// begins with lowercase === not component
|
|
399
|
+
if (functionName && !/^[a-z]/.test(functionName))
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
// Ignore sync callbacks like Array#forEach and certain Solid primitives.
|
|
406
|
+
// In this case only, currentScopeNode !== currentScope().node, but we're
|
|
407
|
+
// returning early so it doesn't matter.
|
|
408
|
+
if (
|
|
409
|
+
isFunctionNode(currentScopeNode) &&
|
|
410
|
+
scopeStack.syncCallbacks.has(currentScopeNode)
|
|
411
|
+
) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// Iterate through all usages of (derived) signals in the current scope
|
|
415
|
+
for (const {
|
|
416
|
+
reference,
|
|
417
|
+
declarationScope,
|
|
418
|
+
} of scopeStack.consumeSignalReferencesInScope()) {
|
|
419
|
+
const identifier = reference.identifier;
|
|
420
|
+
if (reference.isWrite()) {
|
|
421
|
+
// don't allow reassigning signals
|
|
422
|
+
context.report({
|
|
423
|
+
node: identifier,
|
|
424
|
+
messageId: "noWrite",
|
|
425
|
+
data: {
|
|
426
|
+
name: identifier.name,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
} else if (identifier.type === "Identifier") {
|
|
430
|
+
const reportBadSignal = (where) =>
|
|
431
|
+
context.report({
|
|
432
|
+
node: identifier,
|
|
433
|
+
messageId: "badSignal",
|
|
434
|
+
data: { name: identifier.name, where },
|
|
435
|
+
});
|
|
436
|
+
if (
|
|
437
|
+
// This allows both calling a signal and calling a function with a signal.
|
|
438
|
+
identifier.parent?.type === "CallExpression" ||
|
|
439
|
+
// Also allow the case where we pass an array of signals, such as in a custom hook
|
|
440
|
+
(identifier.parent?.type === "ArrayExpression" &&
|
|
441
|
+
identifier.parent.parent?.type === "CallExpression")
|
|
442
|
+
) {
|
|
443
|
+
// This signal is getting called properly, analyze it.
|
|
444
|
+
handleTrackedScopes(identifier, declarationScope);
|
|
445
|
+
} else if (identifier.parent?.type === "TemplateLiteral") {
|
|
446
|
+
reportBadSignal("template literals");
|
|
447
|
+
} else if (
|
|
448
|
+
identifier.parent?.type === "BinaryExpression" &&
|
|
449
|
+
[
|
|
450
|
+
"<",
|
|
451
|
+
"<=",
|
|
452
|
+
">",
|
|
453
|
+
">=",
|
|
454
|
+
"<<",
|
|
455
|
+
">>",
|
|
456
|
+
">>>",
|
|
457
|
+
"+",
|
|
458
|
+
"-",
|
|
459
|
+
"*",
|
|
460
|
+
"/",
|
|
461
|
+
"%",
|
|
462
|
+
"**",
|
|
463
|
+
"|",
|
|
464
|
+
"^",
|
|
465
|
+
"&",
|
|
466
|
+
"in",
|
|
467
|
+
].includes(identifier.parent.operator)
|
|
468
|
+
) {
|
|
469
|
+
// We're in an arithmetic/comparison expression where using an uncalled signal wouldn't make sense
|
|
470
|
+
reportBadSignal("arithmetic or comparisons");
|
|
471
|
+
} else if (
|
|
472
|
+
identifier.parent?.type === "UnaryExpression" &&
|
|
473
|
+
["-", "+", "~"].includes(identifier.parent.operator)
|
|
474
|
+
) {
|
|
475
|
+
// We're in a unary expression where using an uncalled signal wouldn't make sense
|
|
476
|
+
reportBadSignal("unary expressions");
|
|
477
|
+
} else if (
|
|
478
|
+
identifier.parent?.type === "MemberExpression" &&
|
|
479
|
+
identifier.parent.computed &&
|
|
480
|
+
identifier.parent.property === identifier
|
|
481
|
+
) {
|
|
482
|
+
// We're using an uncalled signal to index an object or array, which doesn't make sense
|
|
483
|
+
reportBadSignal("property accesses");
|
|
484
|
+
} else if (
|
|
485
|
+
identifier.parent?.type === "JSXExpressionContainer" &&
|
|
486
|
+
!currentScope().trackedScopes.find(
|
|
487
|
+
(trackedScope) =>
|
|
488
|
+
trackedScope.node === identifier &&
|
|
489
|
+
(trackedScope.expect === "function" ||
|
|
490
|
+
trackedScope.expect === "called-function"),
|
|
491
|
+
)
|
|
492
|
+
) {
|
|
493
|
+
// If the signal is in a JSXExpressionContainer that's also marked as a "function" or "called-function" tracked scope,
|
|
494
|
+
// let it be.
|
|
495
|
+
const elementOrAttribute = identifier.parent.parent;
|
|
496
|
+
if (
|
|
497
|
+
// The signal is not being called and is being used as a props.children, where calling
|
|
498
|
+
// the signal was the likely intent.
|
|
499
|
+
isJSXElementOrFragment(elementOrAttribute) ||
|
|
500
|
+
// We can't say for sure about user components, but we know for a fact that a signal
|
|
501
|
+
// should not be passed to a non-event handler DOM element attribute without calling it.
|
|
502
|
+
(elementOrAttribute?.type === "JSXAttribute" &&
|
|
503
|
+
elementOrAttribute.parent?.type ===
|
|
504
|
+
"JSXOpeningElement" &&
|
|
505
|
+
elementOrAttribute.parent.name.type ===
|
|
506
|
+
"JSXIdentifier" &&
|
|
507
|
+
isDOMElementName(
|
|
508
|
+
elementOrAttribute.parent.name.name,
|
|
509
|
+
))
|
|
510
|
+
) {
|
|
511
|
+
reportBadSignal("JSX");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// The signal is being read outside of a CallExpression. Since
|
|
516
|
+
// there's a lot of possibilities here and they're generally fine,
|
|
517
|
+
// do nothing.
|
|
518
|
+
}
|
|
519
|
+
// Do a similar thing with all usages of props in the current function
|
|
520
|
+
for (const {
|
|
521
|
+
reference,
|
|
522
|
+
declarationScope,
|
|
523
|
+
} of scopeStack.consumePropsReferencesInScope()) {
|
|
524
|
+
const identifier = reference.identifier;
|
|
525
|
+
if (reference.isWrite()) {
|
|
526
|
+
// don't allow reassigning props or stores
|
|
527
|
+
context.report({
|
|
528
|
+
node: identifier,
|
|
529
|
+
messageId: "noWrite",
|
|
530
|
+
data: {
|
|
531
|
+
name: identifier.name,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
} else if (
|
|
535
|
+
identifier.parent?.type === "MemberExpression" &&
|
|
536
|
+
identifier.parent.object === identifier
|
|
537
|
+
) {
|
|
538
|
+
const { parent } = identifier;
|
|
539
|
+
if (
|
|
540
|
+
parent.parent?.type === "AssignmentExpression" &&
|
|
541
|
+
parent.parent.left === parent
|
|
542
|
+
) {
|
|
543
|
+
// don't allow writing to props or stores directly
|
|
544
|
+
context.report({
|
|
545
|
+
node: identifier,
|
|
546
|
+
messageId: "noWrite",
|
|
547
|
+
data: {
|
|
548
|
+
name: identifier.name,
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
} else if (
|
|
552
|
+
parent.property.type === "Identifier" &&
|
|
553
|
+
/^(?:initial|default|static[A-Z])/.test(
|
|
554
|
+
parent.property.name,
|
|
555
|
+
)
|
|
556
|
+
) {
|
|
557
|
+
// We're using a prop with a name that starts with `initial` or
|
|
558
|
+
// `default`, like `props.initialCount`. We'll refrain from warning
|
|
559
|
+
// about untracked usages of these props, because the user has shown
|
|
560
|
+
// that they understand the consequences of using a reactive
|
|
561
|
+
// variable to initialize something else. Do nothing.
|
|
562
|
+
} else {
|
|
563
|
+
// The props are the object in a property read access, which
|
|
564
|
+
// should be under a tracked scope.
|
|
565
|
+
handleTrackedScopes(identifier, declarationScope);
|
|
566
|
+
}
|
|
567
|
+
} else if (
|
|
568
|
+
identifier.parent?.type === "AssignmentExpression" ||
|
|
569
|
+
identifier.parent?.type === "VariableDeclarator"
|
|
570
|
+
) {
|
|
571
|
+
// There's no reason to allow `... = props`, it's usually destructuring, which breaks reactivity.
|
|
572
|
+
context.report({
|
|
573
|
+
node: identifier,
|
|
574
|
+
messageId: "untrackedReactive",
|
|
575
|
+
data: { name: identifier.name },
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
// The props are being read, but not in a MemberExpression. Since
|
|
579
|
+
// there's a lot of possibilities here and they're generally fine,
|
|
580
|
+
// do nothing.
|
|
581
|
+
}
|
|
582
|
+
// If there are any unnamed derived signals, they must match a tracked
|
|
583
|
+
// scope. Usually anonymous arrow function args to createEffect,
|
|
584
|
+
// createMemo, etc.
|
|
585
|
+
const { unnamedDerivedSignals } = currentScope();
|
|
586
|
+
if (unnamedDerivedSignals) {
|
|
587
|
+
for (const node of unnamedDerivedSignals) {
|
|
588
|
+
if (
|
|
589
|
+
!currentScope().trackedScopes.find((trackedScope) =>
|
|
590
|
+
matchTrackedScope(trackedScope, node),
|
|
591
|
+
)
|
|
592
|
+
) {
|
|
593
|
+
context.report({
|
|
594
|
+
loc: getFunctionHeadLocation(node, sourceCode),
|
|
595
|
+
messageId: "badUnnamedDerivedSignal",
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// Pop on exit
|
|
601
|
+
scopeStack.pop();
|
|
602
|
+
};
|
|
603
|
+
/*
|
|
604
|
+
* Sync array functions (forEach, map, reduce, reduceRight, flatMap),
|
|
605
|
+
* store update fn params (ex. setState("todos", (t) => [...t.slice(0, i()),
|
|
606
|
+
* ...t.slice(i() + 1)])), batch, onCleanup, and onError fn params, and
|
|
607
|
+
* maybe a few others don't actually create a new scope. That is, any
|
|
608
|
+
* signal/prop accesses in these functions act as if they happen in the
|
|
609
|
+
* enclosing function. Note that this means whether or not the enclosing
|
|
610
|
+
* function is a tracking scope applies to the fn param as well.
|
|
611
|
+
*
|
|
612
|
+
* Every time a sync callback is detected, we put that function node into a
|
|
613
|
+
* syncCallbacks Set<FunctionNode>. The detections must happen on the entry pass
|
|
614
|
+
* and when the function node has not yet been traversed. In onFunctionEnter, if
|
|
615
|
+
* the function node is in syncCallbacks, we don't push it onto the
|
|
616
|
+
* scopeStack. In onFunctionExit, if the function node is in syncCallbacks,
|
|
617
|
+
* we don't pop scopeStack.
|
|
618
|
+
*/
|
|
619
|
+
const checkForSyncCallbacks = (node) => {
|
|
620
|
+
if (
|
|
621
|
+
node.arguments.length === 1 &&
|
|
622
|
+
isFunctionNode(node.arguments[0]) &&
|
|
623
|
+
!node.arguments[0].async
|
|
624
|
+
) {
|
|
625
|
+
if (
|
|
626
|
+
node.callee.type === "Identifier" &&
|
|
627
|
+
matchImport(["batch", "produce"], node.callee.name)
|
|
628
|
+
) {
|
|
629
|
+
// These Solid APIs take callbacks that run in the current scope
|
|
630
|
+
scopeStack.syncCallbacks.add(node.arguments[0]);
|
|
631
|
+
} else if (
|
|
632
|
+
node.callee.type === "MemberExpression" &&
|
|
633
|
+
!node.callee.computed &&
|
|
634
|
+
node.callee.object.type !== "ObjectExpression" &&
|
|
635
|
+
/^(?:forEach|map|flatMap|reduce|reduceRight|find|findIndex|filter|every|some)$/.test(
|
|
636
|
+
node.callee.property.name,
|
|
637
|
+
)
|
|
638
|
+
) {
|
|
639
|
+
// These common array methods (or likely array methods) take synchronous callbacks
|
|
640
|
+
scopeStack.syncCallbacks.add(node.arguments[0]);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (node.callee.type === "Identifier") {
|
|
644
|
+
if (
|
|
645
|
+
matchImport(
|
|
646
|
+
["createSignal", "createStore"],
|
|
647
|
+
node.callee.name,
|
|
648
|
+
) &&
|
|
649
|
+
node.parent?.type === "VariableDeclarator"
|
|
650
|
+
) {
|
|
651
|
+
// Allow using reactive variables in state setter if the current scope is tracked.
|
|
652
|
+
// ex. const [state, setState] = createStore({ ... });
|
|
653
|
+
// setState(() => ({ preferredName: state.firstName, lastName: "Milner" }));
|
|
654
|
+
const setter = getNthDestructuredVar(
|
|
655
|
+
node.parent.id,
|
|
656
|
+
1,
|
|
657
|
+
context,
|
|
658
|
+
);
|
|
659
|
+
if (setter) {
|
|
660
|
+
for (const reference of setter.references) {
|
|
661
|
+
const { identifier } = reference;
|
|
662
|
+
if (
|
|
663
|
+
!reference.init &&
|
|
664
|
+
reference.isRead() &&
|
|
665
|
+
identifier.parent?.type === "CallExpression"
|
|
666
|
+
) {
|
|
667
|
+
for (const arg of identifier.parent.arguments) {
|
|
668
|
+
if (isFunctionNode(arg) && !arg.async) {
|
|
669
|
+
scopeStack.syncCallbacks.add(arg);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} else if (
|
|
676
|
+
matchImport(["mapArray", "indexArray"], node.callee.name)
|
|
677
|
+
) {
|
|
678
|
+
const arg1 = node.arguments[1];
|
|
679
|
+
if (isFunctionNode(arg1)) {
|
|
680
|
+
scopeStack.syncCallbacks.add(arg1);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// Handle IIFEs
|
|
685
|
+
if (isFunctionNode(node.callee)) {
|
|
686
|
+
scopeStack.syncCallbacks.add(node.callee);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
/** Checks VariableDeclarators, AssignmentExpressions, and CallExpressions for reactivity. */
|
|
690
|
+
const checkForReactiveAssignment = (id, init) => {
|
|
691
|
+
init = ignoreTransparentWrappers(init);
|
|
692
|
+
// Mark return values of certain functions as reactive
|
|
693
|
+
if (
|
|
694
|
+
init.type === "CallExpression" &&
|
|
695
|
+
init.callee.type === "Identifier"
|
|
696
|
+
) {
|
|
697
|
+
const { callee } = init;
|
|
698
|
+
if (
|
|
699
|
+
matchImport(["createSignal", "useTransition"], callee.name)
|
|
700
|
+
) {
|
|
701
|
+
const signal = id && getNthDestructuredVar(id, 0, context);
|
|
702
|
+
if (signal) {
|
|
703
|
+
scopeStack.pushSignal(signal, currentScope().node);
|
|
704
|
+
} else {
|
|
705
|
+
warnShouldDestructure(id ?? init, "first");
|
|
706
|
+
}
|
|
707
|
+
} else if (
|
|
708
|
+
matchImport(["createMemo", "createSelector"], callee.name)
|
|
709
|
+
) {
|
|
710
|
+
const memo = id && getReturnedVar(id, context);
|
|
711
|
+
// memos act like signals
|
|
712
|
+
if (memo) {
|
|
713
|
+
scopeStack.pushSignal(memo, currentScope().node);
|
|
714
|
+
} else {
|
|
715
|
+
warnShouldAssign(id ?? init);
|
|
716
|
+
}
|
|
717
|
+
} else if (matchImport("createStore", callee.name)) {
|
|
718
|
+
const store = id && getNthDestructuredVar(id, 0, context);
|
|
719
|
+
// stores act like props
|
|
720
|
+
if (store) {
|
|
721
|
+
scopeStack.pushProps(store, currentScope().node);
|
|
722
|
+
} else {
|
|
723
|
+
warnShouldDestructure(id ?? init, "first");
|
|
724
|
+
}
|
|
725
|
+
} else if (matchImport("mergeProps", callee.name)) {
|
|
726
|
+
const merged = id && getReturnedVar(id, context);
|
|
727
|
+
if (merged) {
|
|
728
|
+
scopeStack.pushProps(merged, currentScope().node);
|
|
729
|
+
} else {
|
|
730
|
+
warnShouldAssign(id ?? init);
|
|
731
|
+
}
|
|
732
|
+
} else if (matchImport("splitProps", callee.name)) {
|
|
733
|
+
// splitProps can return an unbounded array of props variables, though it's most often two
|
|
734
|
+
if (id?.type === "ArrayPattern") {
|
|
735
|
+
const vars = id.elements
|
|
736
|
+
.map((_, i) =>
|
|
737
|
+
getNthDestructuredVar(id, i, context),
|
|
738
|
+
)
|
|
739
|
+
.filter(Boolean);
|
|
740
|
+
if (vars.length === 0) {
|
|
741
|
+
warnShouldDestructure(id);
|
|
742
|
+
} else {
|
|
743
|
+
vars.forEach((variable) => {
|
|
744
|
+
scopeStack.pushProps(
|
|
745
|
+
variable,
|
|
746
|
+
currentScope().node,
|
|
747
|
+
);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
// if it's returned as an array, treat that as a props object
|
|
752
|
+
const vars = id && getReturnedVar(id, context);
|
|
753
|
+
if (vars) {
|
|
754
|
+
scopeStack.pushProps(vars, currentScope().node);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} else if (matchImport("createResource", callee.name)) {
|
|
758
|
+
// createResource return value has reactive .loading and .error
|
|
759
|
+
const resourceReturn =
|
|
760
|
+
id && getNthDestructuredVar(id, 0, context);
|
|
761
|
+
if (resourceReturn) {
|
|
762
|
+
scopeStack.pushProps(
|
|
763
|
+
resourceReturn,
|
|
764
|
+
currentScope().node,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
} else if (matchImport("createMutable", callee.name)) {
|
|
768
|
+
const mutable = id && getReturnedVar(id, context);
|
|
769
|
+
if (mutable) {
|
|
770
|
+
scopeStack.pushProps(mutable, currentScope().node);
|
|
771
|
+
}
|
|
772
|
+
} else if (matchImport("mapArray", callee.name)) {
|
|
773
|
+
const arg1 = init.arguments[1];
|
|
774
|
+
if (
|
|
775
|
+
isFunctionNode(arg1) &&
|
|
776
|
+
arg1.params.length >= 2 &&
|
|
777
|
+
arg1.params[1].type === "Identifier"
|
|
778
|
+
) {
|
|
779
|
+
const indexSignal = findVariable(
|
|
780
|
+
context,
|
|
781
|
+
arg1.params[1],
|
|
782
|
+
);
|
|
783
|
+
if (indexSignal) {
|
|
784
|
+
scopeStack.pushSignal(indexSignal);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} else if (matchImport("indexArray", callee.name)) {
|
|
788
|
+
const arg1 = init.arguments[1];
|
|
789
|
+
if (
|
|
790
|
+
isFunctionNode(arg1) &&
|
|
791
|
+
arg1.params.length >= 1 &&
|
|
792
|
+
arg1.params[0].type === "Identifier"
|
|
793
|
+
) {
|
|
794
|
+
const valueSignal = findVariable(
|
|
795
|
+
context,
|
|
796
|
+
arg1.params[0],
|
|
797
|
+
);
|
|
798
|
+
if (valueSignal) {
|
|
799
|
+
scopeStack.pushSignal(valueSignal);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
const checkForTrackedScopes = (node) => {
|
|
806
|
+
const pushTrackedScope = (node, expect) => {
|
|
807
|
+
currentScope().trackedScopes.push({ node, expect });
|
|
808
|
+
if (
|
|
809
|
+
expect !== "called-function" &&
|
|
810
|
+
isFunctionNode(node) &&
|
|
811
|
+
node.async
|
|
812
|
+
) {
|
|
813
|
+
// From the docs: "[Solid's] approach only tracks synchronously. If you
|
|
814
|
+
// have a setTimeout or use an async function in your Effect the code
|
|
815
|
+
// that executes async after the fact won't be tracked."
|
|
816
|
+
context.report({
|
|
817
|
+
node,
|
|
818
|
+
messageId: "noAsyncTrackedScope",
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
// given some expression, mark any functions within it as tracking scopes, and do not traverse
|
|
823
|
+
// those functions
|
|
824
|
+
const permissivelyTrackNode = (node) => {
|
|
825
|
+
traverse(node, {
|
|
826
|
+
enter(cn) {
|
|
827
|
+
const childNode = cn;
|
|
828
|
+
const traced = trace(childNode, context);
|
|
829
|
+
// when referencing a function or something that could be a derived signal, track it
|
|
830
|
+
if (
|
|
831
|
+
isFunctionNode(traced) ||
|
|
832
|
+
(traced.type === "Identifier" &&
|
|
833
|
+
traced.parent.type !== "MemberExpression" &&
|
|
834
|
+
!(
|
|
835
|
+
traced.parent.type === "CallExpression" &&
|
|
836
|
+
traced.parent.callee === traced
|
|
837
|
+
))
|
|
838
|
+
) {
|
|
839
|
+
pushTrackedScope(childNode, "called-function");
|
|
840
|
+
this.skip(); // poor-man's `findInScope`: don't enter child scopes
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
fallback: "iteration", // Don't crash when encounter unknown node.
|
|
844
|
+
});
|
|
845
|
+
};
|
|
846
|
+
if (node.type === "JSXExpressionContainer") {
|
|
847
|
+
if (
|
|
848
|
+
node.parent?.type === "JSXAttribute" &&
|
|
849
|
+
sourceCode.getText(node.parent.name).startsWith("on") &&
|
|
850
|
+
node.parent.parent?.type === "JSXOpeningElement" &&
|
|
851
|
+
node.parent.parent.name.type === "JSXIdentifier" &&
|
|
852
|
+
isDOMElementName(node.parent.parent.name.name)
|
|
853
|
+
) {
|
|
854
|
+
// Expect a function if the attribute is like onClick={}, onclick={}, on:click={}, or
|
|
855
|
+
// custom events such as on-click={}.
|
|
856
|
+
// From the docs:
|
|
857
|
+
// Events are never rebound and the bindings are not reactive, as it is expensive to
|
|
858
|
+
// attach and detach listeners. Since event handlers are called like any other function
|
|
859
|
+
// each time an event fires, there is no need for reactivity; simply shortcut your handler
|
|
860
|
+
// if desired.
|
|
861
|
+
// What this means here is we actually do consider an event handler a tracked scope
|
|
862
|
+
// expecting a function, i.e. it's okay to use changing props/signals in the body of the
|
|
863
|
+
// function, even though the changes don't affect when the handler will run. This is what
|
|
864
|
+
// "called-function" represents—not quite a tracked scope, but a place where it's okay to
|
|
865
|
+
// read reactive values.
|
|
866
|
+
pushTrackedScope(node.expression, "called-function");
|
|
867
|
+
} else if (
|
|
868
|
+
node.parent?.type === "JSXAttribute" &&
|
|
869
|
+
node.parent.name.type === "JSXNamespacedName" &&
|
|
870
|
+
node.parent.name.namespace.name === "use" &&
|
|
871
|
+
isFunctionNode(node.expression)
|
|
872
|
+
) {
|
|
873
|
+
// With a `use:` hook, assume that a function passed is a called function.
|
|
874
|
+
pushTrackedScope(node.expression, "called-function");
|
|
875
|
+
} else if (
|
|
876
|
+
node.parent?.type === "JSXAttribute" &&
|
|
877
|
+
node.parent.name.name === "value" &&
|
|
878
|
+
node.parent.parent?.type === "JSXOpeningElement" &&
|
|
879
|
+
((node.parent.parent.name.type === "JSXIdentifier" &&
|
|
880
|
+
node.parent.parent.name.name.endsWith("Provider")) ||
|
|
881
|
+
(node.parent.parent.name.type ===
|
|
882
|
+
"JSXMemberExpression" &&
|
|
883
|
+
node.parent.parent.name.property.name ===
|
|
884
|
+
"Provider"))
|
|
885
|
+
) {
|
|
886
|
+
// From the docs: "The value passed to provider is passed to useContext as is. That means
|
|
887
|
+
// wrapping as a reactive expression will not work. You should pass in Signals and Stores
|
|
888
|
+
// directly instead of accessing them in the JSX."
|
|
889
|
+
// For `<SomeContext.Provider value={}>` or `<SomeProvider value={}>`, do nothing, the
|
|
890
|
+
// rule will warn later.
|
|
891
|
+
// TODO: add some kind of "anti- tracked scope" that still warns but enhances the error
|
|
892
|
+
// message if matched.
|
|
893
|
+
} else if (
|
|
894
|
+
node.parent?.type === "JSXAttribute" &&
|
|
895
|
+
node.parent.name?.type === "JSXIdentifier" &&
|
|
896
|
+
/^static[A-Z]/.test(node.parent.name.name) &&
|
|
897
|
+
node.parent.parent?.type === "JSXOpeningElement" &&
|
|
898
|
+
node.parent.parent.name.type === "JSXIdentifier" &&
|
|
899
|
+
!isDOMElementName(node.parent.parent.name.name)
|
|
900
|
+
) {
|
|
901
|
+
// A caller is passing a value to a prop prefixed with `static` in a component, i.e.
|
|
902
|
+
// `<Box staticName={...} />`. Since we're considering these props as static in the component
|
|
903
|
+
// we shouldn't allow passing reactive values to them, as this isn't just ignoring reactivity
|
|
904
|
+
// like initial*/default*; this is disabling it altogether as a convention. Do nothing.
|
|
905
|
+
} else if (
|
|
906
|
+
node.parent?.type === "JSXAttribute" &&
|
|
907
|
+
node.parent.name.name === "ref" &&
|
|
908
|
+
isFunctionNode(node.expression)
|
|
909
|
+
) {
|
|
910
|
+
// Callback/function refs are called when an element is created but before it is connected
|
|
911
|
+
// to the DOM. This is semantically a "called function", so it's fine to read reactive
|
|
912
|
+
// variables here.
|
|
913
|
+
pushTrackedScope(node.expression, "called-function");
|
|
914
|
+
} else if (
|
|
915
|
+
isJSXElementOrFragment(node.parent) &&
|
|
916
|
+
isFunctionNode(node.expression)
|
|
917
|
+
) {
|
|
918
|
+
pushTrackedScope(node.expression, "function"); // functions inline in JSX containers will be tracked
|
|
919
|
+
} else {
|
|
920
|
+
pushTrackedScope(node.expression, "expression");
|
|
921
|
+
}
|
|
922
|
+
} else if (node.type === "JSXSpreadAttribute") {
|
|
923
|
+
// allow <div {...props.nestedProps} />; {...props} is already ignored
|
|
924
|
+
pushTrackedScope(node.argument, "expression");
|
|
925
|
+
} else if (node.type === "NewExpression") {
|
|
926
|
+
const {
|
|
927
|
+
callee,
|
|
928
|
+
arguments: { 0: arg0 },
|
|
929
|
+
} = node;
|
|
930
|
+
if (
|
|
931
|
+
callee.type === "Identifier" &&
|
|
932
|
+
arg0 &&
|
|
933
|
+
// Observers from Standard Web APIs
|
|
934
|
+
[
|
|
935
|
+
"IntersectionObserver",
|
|
936
|
+
"MutationObserver",
|
|
937
|
+
"PerformanceObserver",
|
|
938
|
+
"ReportingObserver",
|
|
939
|
+
"ResizeObserver",
|
|
940
|
+
].includes(callee.name)
|
|
941
|
+
) {
|
|
942
|
+
// Observers callbacks are NOT tracked scopes. However, they
|
|
943
|
+
// don't need to react to updates to reactive variables; it's okay
|
|
944
|
+
// to poll the current value. Consider them called-function tracked
|
|
945
|
+
// scopes for our purposes.
|
|
946
|
+
pushTrackedScope(arg0, "called-function");
|
|
947
|
+
}
|
|
948
|
+
} else if (node.type === "CallExpression") {
|
|
949
|
+
if (node.callee.type === "Identifier") {
|
|
950
|
+
const {
|
|
951
|
+
callee,
|
|
952
|
+
arguments: { 0: arg0, 1: arg1 },
|
|
953
|
+
} = node;
|
|
954
|
+
if (
|
|
955
|
+
matchImport(
|
|
956
|
+
[
|
|
957
|
+
"createMemo",
|
|
958
|
+
"children",
|
|
959
|
+
"createEffect",
|
|
960
|
+
"createRenderEffect",
|
|
961
|
+
"createDeferred",
|
|
962
|
+
"createComputed",
|
|
963
|
+
"createSelector",
|
|
964
|
+
"untrack",
|
|
965
|
+
"mapArray",
|
|
966
|
+
"indexArray",
|
|
967
|
+
"observable",
|
|
968
|
+
],
|
|
969
|
+
callee.name,
|
|
970
|
+
) ||
|
|
971
|
+
(matchImport("createResource", callee.name) &&
|
|
972
|
+
node.arguments.length >= 2)
|
|
973
|
+
) {
|
|
974
|
+
// createEffect, createMemo, etc. fn arg, and createResource optional
|
|
975
|
+
// `source` first argument may be a signal
|
|
976
|
+
pushTrackedScope(arg0, "function");
|
|
977
|
+
} else if (
|
|
978
|
+
matchImport(
|
|
979
|
+
["onMount", "onCleanup", "onError"],
|
|
980
|
+
callee.name,
|
|
981
|
+
) ||
|
|
982
|
+
[
|
|
983
|
+
// Timers
|
|
984
|
+
"setInterval",
|
|
985
|
+
"setTimeout",
|
|
986
|
+
"setImmediate",
|
|
987
|
+
"requestAnimationFrame",
|
|
988
|
+
"requestIdleCallback",
|
|
989
|
+
].includes(callee.name)
|
|
990
|
+
) {
|
|
991
|
+
// on* and timers are NOT tracked scopes. However, they
|
|
992
|
+
// don't need to react to updates to reactive variables; it's okay
|
|
993
|
+
// to poll the current value. Consider them called-function tracked
|
|
994
|
+
// scopes for our purposes.
|
|
995
|
+
pushTrackedScope(arg0, "called-function");
|
|
996
|
+
} else if (matchImport("on", callee.name)) {
|
|
997
|
+
// on accepts a signal or an array of signals as its first argument,
|
|
998
|
+
// and a tracking function as its second
|
|
999
|
+
if (arg0) {
|
|
1000
|
+
if (arg0.type === "ArrayExpression") {
|
|
1001
|
+
arg0.elements.forEach((element) => {
|
|
1002
|
+
if (
|
|
1003
|
+
element &&
|
|
1004
|
+
element?.type !== "SpreadElement"
|
|
1005
|
+
) {
|
|
1006
|
+
pushTrackedScope(element, "function");
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
} else {
|
|
1010
|
+
pushTrackedScope(arg0, "function");
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (arg1) {
|
|
1014
|
+
// Since dependencies are known, function can be async
|
|
1015
|
+
pushTrackedScope(arg1, "called-function");
|
|
1016
|
+
}
|
|
1017
|
+
} else if (
|
|
1018
|
+
matchImport("createStore", callee.name) &&
|
|
1019
|
+
arg0?.type === "ObjectExpression"
|
|
1020
|
+
) {
|
|
1021
|
+
for (const property of arg0.properties) {
|
|
1022
|
+
if (
|
|
1023
|
+
property.type === "Property" &&
|
|
1024
|
+
property.kind === "get" &&
|
|
1025
|
+
isFunctionNode(property.value)
|
|
1026
|
+
) {
|
|
1027
|
+
pushTrackedScope(property.value, "function");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
} else if (matchImport("runWithOwner", callee.name)) {
|
|
1031
|
+
// runWithOwner(owner, fn) only creates a tracked scope if `owner =
|
|
1032
|
+
// getOwner()` runs in a tracked scope. If owner is a variable,
|
|
1033
|
+
// attempt to detect if it's a tracked scope or not, but if this
|
|
1034
|
+
// can't be done, assume it's a tracked scope.
|
|
1035
|
+
if (arg1) {
|
|
1036
|
+
let isTrackedScope = true;
|
|
1037
|
+
const owner =
|
|
1038
|
+
arg0.type === "Identifier" &&
|
|
1039
|
+
findVariable(context, arg0);
|
|
1040
|
+
if (owner) {
|
|
1041
|
+
const decl = owner.defs[0];
|
|
1042
|
+
if (
|
|
1043
|
+
decl &&
|
|
1044
|
+
decl.node.type === "VariableDeclarator" &&
|
|
1045
|
+
decl.node.init?.type === "CallExpression" &&
|
|
1046
|
+
decl.node.init.callee.type ===
|
|
1047
|
+
"Identifier" &&
|
|
1048
|
+
matchImport(
|
|
1049
|
+
"getOwner",
|
|
1050
|
+
decl.node.init.callee.name,
|
|
1051
|
+
)
|
|
1052
|
+
) {
|
|
1053
|
+
// Check if the function in which getOwner() is called is a tracked scope. If the scopeStack
|
|
1054
|
+
// has moved on from that scope already, assume it's tracked, since that's less intrusive.
|
|
1055
|
+
const ownerFunction = findParent(
|
|
1056
|
+
decl.node,
|
|
1057
|
+
isProgramOrFunctionNode,
|
|
1058
|
+
);
|
|
1059
|
+
const scopeStackIndex =
|
|
1060
|
+
scopeStack.findIndex(
|
|
1061
|
+
({ node }) =>
|
|
1062
|
+
ownerFunction === node,
|
|
1063
|
+
);
|
|
1064
|
+
if (
|
|
1065
|
+
(scopeStackIndex >= 1 &&
|
|
1066
|
+
!scopeStack[
|
|
1067
|
+
scopeStackIndex - 1
|
|
1068
|
+
].trackedScopes.some(
|
|
1069
|
+
(trackedScope) =>
|
|
1070
|
+
trackedScope.expect ===
|
|
1071
|
+
"function" &&
|
|
1072
|
+
trackedScope.node ===
|
|
1073
|
+
ownerFunction,
|
|
1074
|
+
)) ||
|
|
1075
|
+
scopeStackIndex === 0
|
|
1076
|
+
) {
|
|
1077
|
+
isTrackedScope = false;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (isTrackedScope) {
|
|
1082
|
+
pushTrackedScope(arg1, "function");
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
} else if (
|
|
1086
|
+
/^(?:use|create)[A-Z]/.test(callee.name) ||
|
|
1087
|
+
options.customReactiveFunctions.includes(callee.name)
|
|
1088
|
+
) {
|
|
1089
|
+
// Custom hooks parameters may or may not be tracking scopes, no way to know.
|
|
1090
|
+
// Assume all identifier/function arguments are tracked scopes, and use "called-function"
|
|
1091
|
+
// to allow async handlers (permissive). Assume non-resolvable args are reactive expressions.
|
|
1092
|
+
for (const arg of node.arguments) {
|
|
1093
|
+
permissivelyTrackNode(arg);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
} else if (node.callee.type === "MemberExpression") {
|
|
1097
|
+
const { property } = node.callee;
|
|
1098
|
+
if (
|
|
1099
|
+
property.type === "Identifier" &&
|
|
1100
|
+
property.name === "addEventListener" &&
|
|
1101
|
+
node.arguments.length >= 2
|
|
1102
|
+
) {
|
|
1103
|
+
// Like `on*` event handlers, mark all `addEventListener` listeners as called functions.
|
|
1104
|
+
pushTrackedScope(node.arguments[1], "called-function");
|
|
1105
|
+
} else if (
|
|
1106
|
+
property.type === "Identifier" &&
|
|
1107
|
+
(/^(?:use|create)[A-Z]/.test(property.name) ||
|
|
1108
|
+
options.customReactiveFunctions.includes(
|
|
1109
|
+
property.name,
|
|
1110
|
+
))
|
|
1111
|
+
) {
|
|
1112
|
+
// Handle custom hook parameters for property access custom hooks
|
|
1113
|
+
for (const arg of node.arguments) {
|
|
1114
|
+
permissivelyTrackNode(arg);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
} else if (node.type === "VariableDeclarator") {
|
|
1119
|
+
// Solid 1.3 createReactive (renamed createReaction?) returns a track
|
|
1120
|
+
// function, a tracked scope expecting a reactive function. All of the
|
|
1121
|
+
// track function's references where it's called push a tracked scope.
|
|
1122
|
+
if (
|
|
1123
|
+
node.init?.type === "CallExpression" &&
|
|
1124
|
+
node.init.callee.type === "Identifier"
|
|
1125
|
+
) {
|
|
1126
|
+
if (
|
|
1127
|
+
matchImport(
|
|
1128
|
+
["createReactive", "createReaction"],
|
|
1129
|
+
node.init.callee.name,
|
|
1130
|
+
)
|
|
1131
|
+
) {
|
|
1132
|
+
const track = getReturnedVar(node.id, context);
|
|
1133
|
+
if (track) {
|
|
1134
|
+
for (const reference of track.references) {
|
|
1135
|
+
if (
|
|
1136
|
+
!reference.init &&
|
|
1137
|
+
reference.isReadOnly() &&
|
|
1138
|
+
reference.identifier.parent?.type ===
|
|
1139
|
+
"CallExpression" &&
|
|
1140
|
+
reference.identifier.parent.callee ===
|
|
1141
|
+
reference.identifier
|
|
1142
|
+
) {
|
|
1143
|
+
const arg0 =
|
|
1144
|
+
reference.identifier.parent
|
|
1145
|
+
.arguments[0];
|
|
1146
|
+
if (arg0) {
|
|
1147
|
+
pushTrackedScope(arg0, "function");
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (isFunctionNode(node.init.arguments[0])) {
|
|
1153
|
+
pushTrackedScope(
|
|
1154
|
+
node.init.arguments[0],
|
|
1155
|
+
"called-function",
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
} else if (node.type === "AssignmentExpression") {
|
|
1161
|
+
if (
|
|
1162
|
+
node.left.type === "MemberExpression" &&
|
|
1163
|
+
node.left.property.type === "Identifier" &&
|
|
1164
|
+
isFunctionNode(node.right) &&
|
|
1165
|
+
/^on[a-z]+$/.test(node.left.property.name)
|
|
1166
|
+
) {
|
|
1167
|
+
// To allow (questionable) code like the following example:
|
|
1168
|
+
// ref.oninput = () = {
|
|
1169
|
+
// if (!errors[ref.name]) return;
|
|
1170
|
+
// ...
|
|
1171
|
+
// }
|
|
1172
|
+
// where event handlers are manually attached to refs, detect these
|
|
1173
|
+
// scenarios and mark the right hand sides as tracked scopes expecting
|
|
1174
|
+
// functions.
|
|
1175
|
+
pushTrackedScope(node.right, "called-function");
|
|
1176
|
+
}
|
|
1177
|
+
} else if (node.type === "TaggedTemplateExpression") {
|
|
1178
|
+
for (const expression of node.quasi.expressions) {
|
|
1179
|
+
if (isFunctionNode(expression)) {
|
|
1180
|
+
// ex. css`color: ${props => props.color}`. Use "called-function" to allow async handlers (permissive)
|
|
1181
|
+
pushTrackedScope(expression, "called-function");
|
|
1182
|
+
// exception case: add a reactive variable within checkForTrackedScopes when a param is props
|
|
1183
|
+
for (const param of expression.params) {
|
|
1184
|
+
if (
|
|
1185
|
+
param.type === "Identifier" &&
|
|
1186
|
+
isPropsByName(param.name)
|
|
1187
|
+
) {
|
|
1188
|
+
const variable = findVariable(context, param);
|
|
1189
|
+
if (variable)
|
|
1190
|
+
scopeStack.pushProps(
|
|
1191
|
+
variable,
|
|
1192
|
+
currentScope().node,
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
return {
|
|
1201
|
+
ImportDeclaration: handleImportDeclaration,
|
|
1202
|
+
JSXExpressionContainer(node) {
|
|
1203
|
+
checkForTrackedScopes(node);
|
|
1204
|
+
},
|
|
1205
|
+
JSXSpreadAttribute(node) {
|
|
1206
|
+
checkForTrackedScopes(node);
|
|
1207
|
+
},
|
|
1208
|
+
CallExpression(node) {
|
|
1209
|
+
checkForTrackedScopes(node);
|
|
1210
|
+
checkForSyncCallbacks(node);
|
|
1211
|
+
// ensure calls to reactive primitives use the results.
|
|
1212
|
+
const parent =
|
|
1213
|
+
node.parent && ignoreTransparentWrappers(node.parent, true);
|
|
1214
|
+
if (
|
|
1215
|
+
parent?.type !== "AssignmentExpression" &&
|
|
1216
|
+
parent?.type !== "VariableDeclarator"
|
|
1217
|
+
) {
|
|
1218
|
+
checkForReactiveAssignment(null, node);
|
|
1219
|
+
}
|
|
1220
|
+
},
|
|
1221
|
+
NewExpression(node) {
|
|
1222
|
+
checkForTrackedScopes(node);
|
|
1223
|
+
},
|
|
1224
|
+
VariableDeclarator(node) {
|
|
1225
|
+
if (node.init) {
|
|
1226
|
+
checkForReactiveAssignment(node.id, node.init);
|
|
1227
|
+
checkForTrackedScopes(node);
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
AssignmentExpression(node) {
|
|
1231
|
+
if (node.left.type !== "MemberExpression") {
|
|
1232
|
+
checkForReactiveAssignment(node.left, node.right);
|
|
1233
|
+
}
|
|
1234
|
+
checkForTrackedScopes(node);
|
|
1235
|
+
},
|
|
1236
|
+
TaggedTemplateExpression(node) {
|
|
1237
|
+
checkForTrackedScopes(node);
|
|
1238
|
+
},
|
|
1239
|
+
"JSXElement > JSXExpressionContainer > :function"(node) {
|
|
1240
|
+
if (
|
|
1241
|
+
isFunctionNode(node) &&
|
|
1242
|
+
node.parent?.type === "JSXExpressionContainer" &&
|
|
1243
|
+
node.parent.parent?.type === "JSXElement"
|
|
1244
|
+
) {
|
|
1245
|
+
const element = node.parent.parent;
|
|
1246
|
+
if (element.openingElement.name.type === "JSXIdentifier") {
|
|
1247
|
+
const tagName = element.openingElement.name.name;
|
|
1248
|
+
if (
|
|
1249
|
+
matchImport("For", tagName) &&
|
|
1250
|
+
node.params.length === 2 &&
|
|
1251
|
+
node.params[1].type === "Identifier"
|
|
1252
|
+
) {
|
|
1253
|
+
// Mark `index` in `<For>{(item, index) => <div /></For>` as a signal
|
|
1254
|
+
const index = findVariable(context, node.params[1]);
|
|
1255
|
+
if (index) {
|
|
1256
|
+
scopeStack.pushSignal(
|
|
1257
|
+
index,
|
|
1258
|
+
currentScope().node,
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
} else if (
|
|
1262
|
+
matchImport("Index", tagName) &&
|
|
1263
|
+
node.params.length >= 1 &&
|
|
1264
|
+
node.params[0].type === "Identifier"
|
|
1265
|
+
) {
|
|
1266
|
+
// Mark `item` in `<Index>{(item, index) => <div />}</Index>` as a signal
|
|
1267
|
+
const item = findVariable(context, node.params[0]);
|
|
1268
|
+
if (item) {
|
|
1269
|
+
scopeStack.pushSignal(
|
|
1270
|
+
item,
|
|
1271
|
+
currentScope().node,
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
/* Function enter/exit */
|
|
1279
|
+
FunctionExpression: onFunctionEnter,
|
|
1280
|
+
ArrowFunctionExpression: onFunctionEnter,
|
|
1281
|
+
FunctionDeclaration: onFunctionEnter,
|
|
1282
|
+
Program: onFunctionEnter,
|
|
1283
|
+
"FunctionExpression:exit": onFunctionExit,
|
|
1284
|
+
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
1285
|
+
"FunctionDeclaration:exit": onFunctionExit,
|
|
1286
|
+
"Program:exit": onFunctionExit,
|
|
1287
|
+
/* Detect JSX for adding props */
|
|
1288
|
+
JSXElement() {
|
|
1289
|
+
if (scopeStack.length) {
|
|
1290
|
+
currentScope().hasJSX = true;
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
JSXFragment() {
|
|
1294
|
+
if (scopeStack.length) {
|
|
1295
|
+
currentScope().hasJSX = true;
|
|
1296
|
+
}
|
|
1297
|
+
},
|
|
1298
|
+
};
|
|
1299
|
+
},
|
|
1300
|
+
});
|