@@ -32,7 +32,7 @@ import {
32
32
isProgramOrFunctionNode ,
33
33
} from "../utils" ;
34
34
35
- const { findVariable, getFunctionHeadLocation, getFunctionNameWithKind } = ASTUtils ;
35
+ const { findVariable, getFunctionHeadLocation } = ASTUtils ;
36
36
37
37
type Scope = TSESLint . Scope . Scope ;
38
38
type Variable = TSESLint . Scope . Variable ;
@@ -58,13 +58,20 @@ interface ReactiveVariable {
58
58
}
59
59
60
60
interface TrackedScope {
61
+ /**
62
+ * The root node, usually a function or JSX expression container, to allow
63
+ * reactive variables under.
64
+ */
61
65
node : T . Node ;
62
- expect : "function" | "expression" ;
66
+ /**
67
+ * The reactive variable should be one of these types:
68
+ * - "function": synchronous function or signal variable
69
+ * - "event-handler": synchronous or asynchronous function like a timer or
70
+ * event handler that isn't really a tracked scope but acts like one
71
+ * - "expression": some value containing reactivity somewhere
72
+ */
73
+ expect : "function" | "event-handler" | "expression" ;
63
74
}
64
- const TrackedScope = ( node : T . Node , expect : "function" | "expression" ) : TrackedScope => ( {
65
- node,
66
- expect,
67
- } ) ;
68
75
69
76
class ScopeStackItem {
70
77
/** the node for the current scope, or program if global scope */
@@ -246,10 +253,10 @@ type MessageIds =
246
253
| "noWrite"
247
254
| "untrackedReactive"
248
255
| "badSignal"
249
- | "badProps"
250
256
| "badUnnamedDerivedSignal"
251
257
| "shouldDestructure"
252
- | "shouldAssign" ;
258
+ | "shouldAssign"
259
+ | "noAsyncTrackedScope" ;
253
260
254
261
const rule : TSESLint . RuleModule < MessageIds , [ ] > = {
255
262
meta : {
@@ -263,15 +270,17 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
263
270
fixable : "code" ,
264
271
schema : [ ] ,
265
272
messages : {
266
- noWrite : "The reactive variable {{ name }} should not be reassigned or altered." ,
267
- untrackedReactive : "The reactive variable {{ name }} should be used within a tracked scope." ,
268
- badSignal : "This variable should be called as a function when used in JSX." ,
269
- badProps :
270
- "This props or store should only be passed to functions or used for property access." ,
271
- badUnnamedDerivedSignal : "This function should be passed to a tracked scope." ,
273
+ noWrite : "The reactive variable '{{ name}}' should not be reassigned or altered." ,
274
+ untrackedReactive : "The reactive variable '{{ name}}' should be used within a tracked scope." ,
275
+ badSignal :
276
+ "The reactive variable '{{name}}' should be called as a function when used in JSX." ,
277
+ badUnnamedDerivedSignal :
278
+ "This function should be passed to a tracked scope because it contains reactivity ." ,
272
279
shouldDestructure :
273
280
"Array destructuring should be used to capture the {{nth}}result of this function call." ,
274
281
shouldAssign : "A variable should be used to capture the result of this function call." ,
282
+ noAsyncTrackedScope :
283
+ "This tracked scope should not be async. Solid's reactivity only tracks synchronously." ,
275
284
} ,
276
285
} ,
277
286
create ( context ) {
@@ -302,6 +311,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
302
311
const matchTrackedScope = ( trackedScope : TrackedScope , node : T . Node ) : boolean => {
303
312
switch ( trackedScope . expect ) {
304
313
case "function" :
314
+ case "event-handler" :
305
315
return node === trackedScope . node ;
306
316
case "expression" :
307
317
return Boolean (
@@ -443,6 +453,9 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
443
453
context . report ( {
444
454
node : identifier ,
445
455
messageId : "badSignal" ,
456
+ data : {
457
+ name : identifier . name ,
458
+ } ,
446
459
} ) ;
447
460
}
448
461
// The signal is being read outside of a CallExpression. Since
@@ -666,7 +679,18 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
666
679
| T . VariableDeclarator
667
680
| T . AssignmentExpression
668
681
) => {
669
- const { trackedScopes } = currentScope ( ) ;
682
+ const pushTrackedScope = ( node : T . Node , expect : TrackedScope [ "expect" ] ) => {
683
+ currentScope ( ) . trackedScopes . push ( { node, expect } ) ;
684
+ if ( expect !== "event-handler" && isFunctionNode ( node ) && node . async ) {
685
+ // From the docs: "[Solid's] approach only tracks synchronously. If you
686
+ // have a setTimeout or use an async function in your Effect the code
687
+ // that executes async after the fact won't be tracked."
688
+ context . report ( {
689
+ node,
690
+ messageId : "noAsyncTrackedScope" ,
691
+ } ) ;
692
+ }
693
+ } ;
670
694
if ( node . type === "JSXExpressionContainer" ) {
671
695
// Expect a function if the attribute is like onClick={} or on:click={}.
672
696
// From the docs: Events are never rebound and the bindings are not
@@ -683,7 +707,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
683
707
sourceCode . getText ( node . parent . name ) . match ( / ^ o n [: A -Z ] / )
684
708
? "function"
685
709
: "expression" ;
686
- trackedScopes . push ( TrackedScope ( node . expression , expect ) ) ;
710
+ pushTrackedScope ( node . expression , expect ) ;
687
711
} else if ( node . type === "CallExpression" && node . callee . type === "Identifier" ) {
688
712
const callee = node . callee ;
689
713
const arg0 = node . arguments [ 0 ] ;
@@ -694,8 +718,6 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
694
718
"createEffect" ,
695
719
"onMount" ,
696
720
"createRenderEffect" ,
697
- "untrack" ,
698
- "batch" ,
699
721
"createDeferred" ,
700
722
"createComputed" ,
701
723
"createSelector" ,
@@ -704,23 +726,36 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
704
726
) {
705
727
// createEffect, createMemo, etc. fn arg, and createResource optional
706
728
// `source` first argument may be a signal
707
- trackedScopes . push ( TrackedScope ( arg0 , "function" ) ) ;
729
+ pushTrackedScope ( arg0 , "function" ) ;
730
+ } else if (
731
+ [
732
+ "setInterval" ,
733
+ "setTimeout" ,
734
+ "setImmediate" ,
735
+ "requestAnimationFrame" ,
736
+ "requestIdleCallback" ,
737
+ ] . includes ( callee . name )
738
+ ) {
739
+ // Timers are NOT tracked scopes. However, they don't need to react
740
+ // to updates to reactive variables; it's okay to poll the current
741
+ // value of a signal. Consider them tracked scopes for our purposes.
742
+ pushTrackedScope ( arg0 , "event-handler" ) ;
708
743
} else if ( callee . name === "createMutable" && arg0 ) {
709
- trackedScopes . push ( TrackedScope ( arg0 , "expression" ) ) ;
744
+ pushTrackedScope ( arg0 , "expression" ) ;
710
745
} else if ( callee . name === "on" ) {
711
746
// on accepts a signal or an array of signals as its first argument,
712
747
// and a tracking function as its second
713
748
if ( arg0 ) {
714
749
if ( arg0 . type === "ArrayExpression" ) {
715
750
arg0 . elements . forEach ( ( element ) => {
716
- trackedScopes . push ( TrackedScope ( element , "function" ) ) ;
751
+ pushTrackedScope ( element , "function" ) ;
717
752
} ) ;
718
753
} else {
719
- trackedScopes . push ( TrackedScope ( arg0 , "function" ) ) ;
754
+ pushTrackedScope ( arg0 , "function" ) ;
720
755
}
721
756
}
722
757
if ( node . arguments [ 1 ] ) {
723
- trackedScopes . push ( TrackedScope ( node . arguments [ 1 ] , "function" ) ) ;
758
+ pushTrackedScope ( node . arguments [ 1 ] , "function" ) ;
724
759
}
725
760
} else if ( callee . name === "runWithOwner" ) {
726
761
// runWithOwner(owner, fn) only creates a tracked scope if `owner =
@@ -758,7 +793,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
758
793
}
759
794
}
760
795
if ( isTrackedScope ) {
761
- trackedScopes . push ( TrackedScope ( node . arguments [ 1 ] , "function" ) ) ;
796
+ pushTrackedScope ( node . arguments [ 1 ] , "function" ) ;
762
797
}
763
798
}
764
799
}
@@ -778,7 +813,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
778
813
reference . identifier . parent . callee === reference . identifier
779
814
) {
780
815
const arg0 = reference . identifier . parent . arguments [ 0 ] ;
781
- arg0 && trackedScopes . push ( TrackedScope ( arg0 , "function" ) ) ;
816
+ arg0 && pushTrackedScope ( arg0 , "function" ) ;
782
817
}
783
818
}
784
819
}
@@ -799,7 +834,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
799
834
// where event handlers are manually attached to refs, detect these
800
835
// scenarios and mark the right hand sides as tracked scopes expecting
801
836
// functions.
802
- trackedScopes . push ( TrackedScope ( node . right , "function" ) ) ;
837
+ pushTrackedScope ( node . right , "event-handler" ) ;
803
838
}
804
839
}
805
840
} ;
0 commit comments