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